aboutsummaryrefslogtreecommitdiffstats
path: root/packages/0x.js/test
diff options
context:
space:
mode:
authorFabio Berger <me@fabioberger.com>2017-11-13 11:17:18 +0800
committerFabio Berger <me@fabioberger.com>2017-11-13 11:17:18 +0800
commitc4ee2d73865a1444c079b9e2836b7630a0adf03e (patch)
treeb9c7794e7022fb189675d914f5fe58dcabd67dec /packages/0x.js/test
parenta74ec0effa818a86233fe64cb0dad2c61bbb4bb6 (diff)
downloaddexon-sol-tools-c4ee2d73865a1444c079b9e2836b7630a0adf03e.tar
dexon-sol-tools-c4ee2d73865a1444c079b9e2836b7630a0adf03e.tar.gz
dexon-sol-tools-c4ee2d73865a1444c079b9e2836b7630a0adf03e.tar.bz2
dexon-sol-tools-c4ee2d73865a1444c079b9e2836b7630a0adf03e.tar.lz
dexon-sol-tools-c4ee2d73865a1444c079b9e2836b7630a0adf03e.tar.xz
dexon-sol-tools-c4ee2d73865a1444c079b9e2836b7630a0adf03e.tar.zst
dexon-sol-tools-c4ee2d73865a1444c079b9e2836b7630a0adf03e.zip
Switch over to Lerna + Yarn Workspaces setup for a mono-repo approach
Diffstat (limited to 'packages/0x.js/test')
-rw-r--r--packages/0x.js/test/0x.js_test.ts259
-rw-r--r--packages/0x.js/test/artifacts_test.ts49
-rw-r--r--packages/0x.js/test/assert_test.ts34
-rw-r--r--packages/0x.js/test/ether_token_wrapper_test.ts111
-rw-r--r--packages/0x.js/test/event_watcher_test.ts127
-rw-r--r--packages/0x.js/test/exchange_transfer_simulator_test.ts87
-rw-r--r--packages/0x.js/test/exchange_wrapper_test.ts824
-rw-r--r--packages/0x.js/test/order_state_watcher_test.ts356
-rw-r--r--packages/0x.js/test/order_validation_test.ts327
-rw-r--r--packages/0x.js/test/subscription_test.ts95
-rw-r--r--packages/0x.js/test/token_registry_wrapper_test.ts123
-rw-r--r--packages/0x.js/test/token_transfer_proxy_wrapper_test.ts31
-rw-r--r--packages/0x.js/test/token_wrapper_test.ts477
-rw-r--r--packages/0x.js/test/utils/blockchain_lifecycle.ts26
-rw-r--r--packages/0x.js/test/utils/chai_setup.ts13
-rw-r--r--packages/0x.js/test/utils/constants.ts8
-rw-r--r--packages/0x.js/test/utils/fill_scenarios.ts114
-rw-r--r--packages/0x.js/test/utils/order_factory.ts42
-rw-r--r--packages/0x.js/test/utils/report_callback_errors.ts14
-rw-r--r--packages/0x.js/test/utils/rpc.ts57
-rw-r--r--packages/0x.js/test/utils/token_utils.ts24
-rw-r--r--packages/0x.js/test/utils/web3_factory.ts31
-rw-r--r--packages/0x.js/test/web3_wrapper_test.ts29
23 files changed, 3258 insertions, 0 deletions
diff --git a/packages/0x.js/test/0x.js_test.ts b/packages/0x.js/test/0x.js_test.ts
new file mode 100644
index 000000000..d56acc38b
--- /dev/null
+++ b/packages/0x.js/test/0x.js_test.ts
@@ -0,0 +1,259 @@
+import * as _ from 'lodash';
+import * as chai from 'chai';
+import {chaiSetup} from './utils/chai_setup';
+import 'mocha';
+import BigNumber from 'bignumber.js';
+import * as Sinon from 'sinon';
+import {ZeroEx, Order, ZeroExError, LogWithDecodedArgs, ApprovalContractEventArgs, TokenEvents} from '../src';
+import {constants} from './utils/constants';
+import {TokenUtils} from './utils/token_utils';
+import {web3Factory} from './utils/web3_factory';
+import {BlockchainLifecycle} from './utils/blockchain_lifecycle';
+
+const blockchainLifecycle = new BlockchainLifecycle();
+chaiSetup.configure();
+const expect = chai.expect;
+
+describe('ZeroEx library', () => {
+ const web3 = web3Factory.create();
+ const zeroEx = new ZeroEx(web3.currentProvider);
+ describe('#setProvider', () => {
+ it('overrides provider in nested web3s and invalidates contractInstances', async () => {
+ // Instantiate the contract instances with the current provider
+ await (zeroEx.exchange as any)._getExchangeContractAsync();
+ await (zeroEx.tokenRegistry as any)._getTokenRegistryContractAsync();
+ expect((zeroEx.exchange as any)._exchangeContractIfExists).to.not.be.undefined();
+ expect((zeroEx.tokenRegistry as any)._tokenRegistryContractIfExists).to.not.be.undefined();
+
+ const newProvider = web3Factory.getRpcProvider();
+ // Add property to newProvider so that we can differentiate it from old provider
+ (newProvider as any).zeroExTestId = 1;
+ await zeroEx.setProviderAsync(newProvider);
+
+ // Check that contractInstances with old provider are removed after provider update
+ expect((zeroEx.exchange as any)._exchangeContractIfExists).to.be.undefined();
+ expect((zeroEx.tokenRegistry as any)._tokenRegistryContractIfExists).to.be.undefined();
+
+ // Check that all nested web3 wrapper instances return the updated provider
+ const nestedWeb3WrapperProvider = (zeroEx as any)._web3Wrapper.getCurrentProvider();
+ expect((nestedWeb3WrapperProvider as any).zeroExTestId).to.be.a('number');
+ const exchangeWeb3WrapperProvider = (zeroEx.exchange as any)._web3Wrapper.getCurrentProvider();
+ expect((exchangeWeb3WrapperProvider as any).zeroExTestId).to.be.a('number');
+ const tokenRegistryWeb3WrapperProvider = (zeroEx.tokenRegistry as any)._web3Wrapper.getCurrentProvider();
+ expect((tokenRegistryWeb3WrapperProvider as any).zeroExTestId).to.be.a('number');
+ });
+ });
+ describe('#isValidSignature', () => {
+ // The Exchange smart contract `isValidSignature` method only validates orderHashes and assumes
+ // the length of the data is exactly 32 bytes. Thus for these tests, we use data of this size.
+ const dataHex = '0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b0';
+ const signature = {
+ v: 27,
+ r: '0x61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc33',
+ s: '0x40349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254',
+ };
+ const address = '0x5409ed021d9299bf6814279a6a1411a7e866a631';
+ it('should return false if the data doesn\'t pertain to the signature & address', async () => {
+ expect(ZeroEx.isValidSignature('0x0', signature, address)).to.be.false();
+ return expect(
+ (zeroEx.exchange as any)._isValidSignatureUsingContractCallAsync('0x0', signature, address),
+ ).to.become(false);
+ });
+ it('should return false if the address doesn\'t pertain to the signature & data', async () => {
+ const validUnrelatedAddress = '0x8b0292b11a196601ed2ce54b665cafeca0347d42';
+ expect(ZeroEx.isValidSignature(dataHex, signature, validUnrelatedAddress)).to.be.false();
+ return expect(
+ (zeroEx.exchange as any)._isValidSignatureUsingContractCallAsync(dataHex, signature,
+ validUnrelatedAddress),
+ ).to.become(false);
+ });
+ it('should return false if the signature doesn\'t pertain to the dataHex & address', async () => {
+ const wrongSignature = _.assign({}, signature, {v: 28});
+ expect(ZeroEx.isValidSignature(dataHex, wrongSignature, address)).to.be.false();
+ return expect(
+ (zeroEx.exchange as any)._isValidSignatureUsingContractCallAsync(dataHex, wrongSignature, address),
+ ).to.become(false);
+ });
+ it('should return true if the signature does pertain to the dataHex & address', async () => {
+ const isValidSignatureLocal = ZeroEx.isValidSignature(dataHex, signature, address);
+ expect(isValidSignatureLocal).to.be.true();
+ const isValidSignatureOnContract = await (zeroEx.exchange as any)
+ ._isValidSignatureUsingContractCallAsync(dataHex, signature, address);
+ return expect(isValidSignatureOnContract).to.be.true();
+ });
+ });
+ describe('#generateSalt', () => {
+ it('generates different salts', () => {
+ const equal = ZeroEx.generatePseudoRandomSalt().eq(ZeroEx.generatePseudoRandomSalt());
+ expect(equal).to.be.false();
+ });
+ it('generates salt in range [0..2^256)', () => {
+ const salt = ZeroEx.generatePseudoRandomSalt();
+ expect(salt.greaterThanOrEqualTo(0)).to.be.true();
+ const twoPow256 = new BigNumber(2).pow(256);
+ expect(salt.lessThan(twoPow256)).to.be.true();
+ });
+ });
+ describe('#isValidOrderHash', () => {
+ it('returns false if the value is not a hex string', () => {
+ const isValid = ZeroEx.isValidOrderHash('not a hex');
+ expect(isValid).to.be.false();
+ });
+ it('returns false if the length is wrong', () => {
+ const isValid = ZeroEx.isValidOrderHash('0xdeadbeef');
+ expect(isValid).to.be.false();
+ });
+ it('returns true if order hash is correct', () => {
+ const isValid = ZeroEx.isValidOrderHash('0x' + Array(65).join('0'));
+ expect(isValid).to.be.true();
+ });
+ });
+ describe('#toUnitAmount', () => {
+ it('Should return the expected unit amount for the decimals passed in', () => {
+ const baseUnitAmount = new BigNumber(1000000000);
+ const decimals = 6;
+ const unitAmount = ZeroEx.toUnitAmount(baseUnitAmount, decimals);
+ const expectedUnitAmount = new BigNumber(1000);
+ expect(unitAmount).to.be.bignumber.equal(expectedUnitAmount);
+ });
+ });
+ describe('#toBaseUnitAmount', () => {
+ it('Should return the expected base unit amount for the decimals passed in', () => {
+ const unitAmount = new BigNumber(1000);
+ const decimals = 6;
+ const baseUnitAmount = ZeroEx.toBaseUnitAmount(unitAmount, decimals);
+ const expectedUnitAmount = new BigNumber(1000000000);
+ expect(baseUnitAmount).to.be.bignumber.equal(expectedUnitAmount);
+ });
+ });
+ describe('#getOrderHashHex', () => {
+ const expectedOrderHash = '0x39da987067a3c9e5f1617694f1301326ba8c8b0498ebef5df4863bed394e3c83';
+ const fakeExchangeContractAddress = '0xb69e673309512a9d726f87304c6984054f87a93b';
+ const order: Order = {
+ maker: constants.NULL_ADDRESS,
+ taker: constants.NULL_ADDRESS,
+ feeRecipient: constants.NULL_ADDRESS,
+ makerTokenAddress: constants.NULL_ADDRESS,
+ takerTokenAddress: constants.NULL_ADDRESS,
+ exchangeContractAddress: fakeExchangeContractAddress,
+ salt: new BigNumber(0),
+ makerFee: new BigNumber(0),
+ takerFee: new BigNumber(0),
+ makerTokenAmount: new BigNumber(0),
+ takerTokenAmount: new BigNumber(0),
+ expirationUnixTimestampSec: new BigNumber(0),
+ };
+ it('calculates the order hash', async () => {
+ const orderHash = ZeroEx.getOrderHashHex(order);
+ expect(orderHash).to.be.equal(expectedOrderHash);
+ });
+ });
+ describe('#signOrderHashAsync', () => {
+ let stubs: Sinon.SinonStub[] = [];
+ let makerAddress: string;
+ before(async () => {
+ const availableAddreses = await zeroEx.getAvailableAddressesAsync();
+ makerAddress = availableAddreses[0];
+ });
+ afterEach(() => {
+ // clean up any stubs after the test has completed
+ _.each(stubs, s => s.restore());
+ stubs = [];
+ });
+ it('Should return the correct ECSignature', async () => {
+ const orderHash = '0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b0';
+ const expectedECSignature = {
+ v: 27,
+ r: '0x61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc33',
+ s: '0x40349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254',
+ };
+ const ecSignature = await zeroEx.signOrderHashAsync(orderHash, makerAddress);
+ expect(ecSignature).to.deep.equal(expectedECSignature);
+ });
+ it('should return the correct ECSignature for signatureHex concatenated as R + S + V', async () => {
+ const orderHash = '0x34decbedc118904df65f379a175bb39ca18209d6ce41d5ed549d54e6e0a95004';
+ // tslint:disable-next-line: max-line-length
+ const signature = '0x22109d11d79cb8bf96ed88625e1cd9558800c4073332a9a02857499883ee5ce3050aa3cc1f2c435e67e114cdce54b9527b4f50548342401bc5d2b77adbdacb021b';
+ const expectedECSignature = {
+ v: 27,
+ r: '0x22109d11d79cb8bf96ed88625e1cd9558800c4073332a9a02857499883ee5ce3',
+ s: '0x050aa3cc1f2c435e67e114cdce54b9527b4f50548342401bc5d2b77adbdacb02',
+ };
+ stubs = [
+ Sinon.stub((zeroEx as any)._web3Wrapper, 'signTransactionAsync')
+ .returns(Promise.resolve(signature)),
+ Sinon.stub(ZeroEx, 'isValidSignature').returns(true),
+ ];
+
+ const ecSignature = await zeroEx.signOrderHashAsync(orderHash, makerAddress);
+ expect(ecSignature).to.deep.equal(expectedECSignature);
+ });
+ it('should return the correct ECSignature for signatureHex concatenated as V + R + S', async () => {
+ const orderHash = '0xc793e33ffded933b76f2f48d9aa3339fc090399d5e7f5dec8d3660f5480793f7';
+ // tslint:disable-next-line: max-line-length
+ const signature = '0x1bc80bedc6756722672753413efdd749b5adbd4fd552595f59c13427407ee9aee02dea66f25a608bbae457e020fb6decb763deb8b7192abab624997242da248960';
+ const expectedECSignature = {
+ v: 27,
+ r: '0xc80bedc6756722672753413efdd749b5adbd4fd552595f59c13427407ee9aee0',
+ s: '0x2dea66f25a608bbae457e020fb6decb763deb8b7192abab624997242da248960',
+ };
+ stubs = [
+ Sinon.stub((zeroEx as any)._web3Wrapper, 'signTransactionAsync')
+ .returns(Promise.resolve(signature)),
+ Sinon.stub(ZeroEx, 'isValidSignature').returns(true),
+ ];
+
+ const ecSignature = await zeroEx.signOrderHashAsync(orderHash, makerAddress);
+ expect(ecSignature).to.deep.equal(expectedECSignature);
+ });
+ });
+ describe('#awaitTransactionMinedAsync', () => {
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ it('returns transaction receipt with decoded logs', async () => {
+ const availableAddresses = await zeroEx.getAvailableAddressesAsync();
+ const coinbase = availableAddresses[0];
+ const tokens = await zeroEx.tokenRegistry.getTokensAsync();
+ const tokenUtils = new TokenUtils(tokens);
+ const zrxTokenAddress = tokenUtils.getProtocolTokenOrThrow().address;
+ const proxyAddress = await zeroEx.proxy.getContractAddressAsync();
+ const txHash = await zeroEx.token.setUnlimitedProxyAllowanceAsync(zrxTokenAddress, coinbase);
+ const txReceiptWithDecodedLogs = await zeroEx.awaitTransactionMinedAsync(txHash);
+ const log = txReceiptWithDecodedLogs.logs[0] as LogWithDecodedArgs<ApprovalContractEventArgs>;
+ expect(log.event).to.be.equal(TokenEvents.Approval);
+ expect(log.args._owner).to.be.equal(coinbase);
+ expect(log.args._spender).to.be.equal(proxyAddress);
+ expect(log.args._value).to.be.bignumber.equal(zeroEx.token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS);
+ });
+ });
+ describe('#config', () => {
+ it('allows to specify exchange contract address', async () => {
+ const config = {
+ exchangeContractAddress: ZeroEx.NULL_ADDRESS,
+ };
+ const zeroExWithWrongExchangeAddress = new ZeroEx(web3.currentProvider, config);
+ return expect(zeroExWithWrongExchangeAddress.exchange.getContractAddressAsync())
+ .to.be.rejectedWith(ZeroExError.ContractDoesNotExist);
+ });
+ it('allows to specify ether token contract address', async () => {
+ const config = {
+ etherTokenContractAddress: ZeroEx.NULL_ADDRESS,
+ };
+ const zeroExWithWrongEtherTokenAddress = new ZeroEx(web3.currentProvider, config);
+ return expect(zeroExWithWrongEtherTokenAddress.etherToken.getContractAddressAsync())
+ .to.be.rejectedWith(ZeroExError.ContractDoesNotExist);
+ });
+ it('allows to specify token registry token contract address', async () => {
+ const config = {
+ tokenRegistryContractAddress: ZeroEx.NULL_ADDRESS,
+ };
+ const zeroExWithWrongTokenRegistryAddress = new ZeroEx(web3.currentProvider, config);
+ return expect(zeroExWithWrongTokenRegistryAddress.tokenRegistry.getContractAddressAsync())
+ .to.be.rejectedWith(ZeroExError.ContractDoesNotExist);
+ });
+ });
+});
diff --git a/packages/0x.js/test/artifacts_test.ts b/packages/0x.js/test/artifacts_test.ts
new file mode 100644
index 000000000..b2866a1d6
--- /dev/null
+++ b/packages/0x.js/test/artifacts_test.ts
@@ -0,0 +1,49 @@
+import * as fs from 'fs';
+import * as chai from 'chai';
+import {chaiSetup} from './utils/chai_setup';
+import HDWalletProvider = require('truffle-hdwallet-provider');
+import {ZeroEx} from '../src';
+import {constants} from './utils/constants';
+
+chaiSetup.configure();
+const expect = chai.expect;
+
+// 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 packageJSONContent = fs.readFileSync('package.json', 'utf-8');
+ const packageJSON = JSON.parse(packageJSONContent);
+ const mnemonic = packageJSON.config.mnemonic;
+ const web3Provider = new HDWalletProvider(mnemonic, kovanRpcUrl);
+ const zeroEx = new ZeroEx(web3Provider);
+ it('token registry contract is deployed', async () => {
+ await (zeroEx.tokenRegistry as any)._getTokenRegistryContractAsync();
+ }).timeout(TIMEOUT);
+ it('proxy contract is deployed', async () => {
+ await (zeroEx.token as any)._getTokenTransferProxyAddressAsync();
+ }).timeout(TIMEOUT);
+ it('exchange contract is deployed', async () => {
+ await zeroEx.exchange.getContractAddressAsync();
+ }).timeout(TIMEOUT);
+ });
+ describe('contracts are deployed on ropsten', () => {
+ const ropstenRpcUrl = constants.ROPSTEN_RPC_URL;
+ const packageJSONContent = fs.readFileSync('package.json', 'utf-8');
+ const packageJSON = JSON.parse(packageJSONContent);
+ const mnemonic = packageJSON.config.mnemonic;
+ const web3Provider = new HDWalletProvider(mnemonic, ropstenRpcUrl);
+ const zeroEx = new ZeroEx(web3Provider);
+ it('token registry contract is deployed', async () => {
+ await (zeroEx.tokenRegistry as any)._getTokenRegistryContractAsync();
+ }).timeout(TIMEOUT);
+ it('proxy contract is deployed', async () => {
+ await (zeroEx.token as any)._getTokenTransferProxyAddressAsync();
+ }).timeout(TIMEOUT);
+ it('exchange contract is deployed', async () => {
+ await zeroEx.exchange.getContractAddressAsync();
+ }).timeout(TIMEOUT);
+ });
+});
diff --git a/packages/0x.js/test/assert_test.ts b/packages/0x.js/test/assert_test.ts
new file mode 100644
index 000000000..bfca95d9c
--- /dev/null
+++ b/packages/0x.js/test/assert_test.ts
@@ -0,0 +1,34 @@
+import * as chai from 'chai';
+import 'mocha';
+import {ZeroEx} from '../src';
+import {assert} from '../src/utils/assert';
+import {web3Factory} from './utils/web3_factory';
+
+const expect = chai.expect;
+
+describe('Assertion library', () => {
+ const web3 = web3Factory.create();
+ const zeroEx = new ZeroEx(web3.currentProvider);
+ describe('#isSenderAddressHexAsync', () => {
+ it('throws when address is invalid', async () => {
+ const address = '0xdeadbeef';
+ const varName = 'address';
+ return expect(assert.isSenderAddressAsync(varName, address, (zeroEx as any)._web3Wrapper))
+ .to.be.rejectedWith(`Expected ${varName} to be of type ETHAddressHex, encountered: ${address}`);
+ });
+ it('throws when address is unavailable', async () => {
+ const validUnrelatedAddress = '0x8b0292b11a196601eddce54b665cafeca0347d42';
+ const varName = 'address';
+ return expect(assert.isSenderAddressAsync(varName, validUnrelatedAddress, (zeroEx as any)._web3Wrapper))
+ .to.be.rejectedWith(
+ `Specified ${varName} ${validUnrelatedAddress} isn't available through the supplied web3 provider`,
+ );
+ });
+ it('doesn\'t throw if address is available', async () => {
+ const availableAddress = (await zeroEx.getAvailableAddressesAsync())[0];
+ const varName = 'address';
+ return expect(assert.isSenderAddressAsync(varName, availableAddress, (zeroEx as any)._web3Wrapper))
+ .to.become(undefined);
+ });
+ });
+});
diff --git a/packages/0x.js/test/ether_token_wrapper_test.ts b/packages/0x.js/test/ether_token_wrapper_test.ts
new file mode 100644
index 000000000..ba679d1a1
--- /dev/null
+++ b/packages/0x.js/test/ether_token_wrapper_test.ts
@@ -0,0 +1,111 @@
+import 'mocha';
+import * as chai from 'chai';
+import {chaiSetup} from './utils/chai_setup';
+import * as Web3 from 'web3';
+import BigNumber from 'bignumber.js';
+import {web3Factory} from './utils/web3_factory';
+import {ZeroEx, ZeroExError} from '../src';
+import {BlockchainLifecycle} from './utils/blockchain_lifecycle';
+
+chaiSetup.configure();
+const expect = chai.expect;
+const blockchainLifecycle = new BlockchainLifecycle();
+
+// 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 = 62237;
+
+describe('EtherTokenWrapper', () => {
+ let web3: Web3;
+ let zeroEx: ZeroEx;
+ let userAddresses: string[];
+ let addressWithETH: string;
+ let wethContractAddress: string;
+ let depositWeiAmount: BigNumber;
+ let decimalPlaces: number;
+ const gasPrice = new BigNumber(1);
+ const zeroExConfig = {
+ gasPrice,
+ };
+ before(async () => {
+ web3 = web3Factory.create();
+ zeroEx = new ZeroEx(web3.currentProvider, zeroExConfig);
+ userAddresses = await zeroEx.getAvailableAddressesAsync();
+ addressWithETH = userAddresses[0];
+ wethContractAddress = await zeroEx.etherToken.getContractAddressAsync();
+ depositWeiAmount = (zeroEx as any)._web3Wrapper.toWei(new BigNumber(5));
+ decimalPlaces = 7;
+ });
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ describe('#depositAsync', () => {
+ it('should successfully deposit ETH and issue Wrapped ETH tokens', async () => {
+ const preETHBalance = await (zeroEx as any)._web3Wrapper.getBalanceInWeiAsync(addressWithETH);
+ const preWETHBalance = await zeroEx.token.getBalanceAsync(wethContractAddress, addressWithETH);
+ expect(preETHBalance).to.be.bignumber.gt(0);
+ expect(preWETHBalance).to.be.bignumber.equal(0);
+
+ const txHash = await zeroEx.etherToken.depositAsync(depositWeiAmount, addressWithETH);
+ await zeroEx.awaitTransactionMinedAsync(txHash);
+
+ const postETHBalanceInWei = await (zeroEx as any)._web3Wrapper.getBalanceInWeiAsync(addressWithETH);
+ const postWETHBalanceInBaseUnits = await zeroEx.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 (zeroEx as any)._web3Wrapper.getBalanceInWeiAsync(addressWithETH);
+
+ const extraETHBalance = (zeroEx as any)._web3Wrapper.toWei(5, 'ether');
+ const overETHBalanceinWei = preETHBalance.add(extraETHBalance);
+
+ return expect(
+ zeroEx.etherToken.depositAsync(overETHBalanceinWei, addressWithETH),
+ ).to.be.rejectedWith(ZeroExError.InsufficientEthBalanceForDeposit);
+ });
+ });
+ describe('#withdrawAsync', () => {
+ it('should successfully withdraw ETH in return for Wrapped ETH tokens', async () => {
+ const ETHBalanceInWei = await (zeroEx as any)._web3Wrapper.getBalanceInWeiAsync(addressWithETH);
+
+ await zeroEx.etherToken.depositAsync(depositWeiAmount, addressWithETH);
+
+ const expectedPreETHBalance = ETHBalanceInWei.minus(depositWeiAmount);
+ const preETHBalance = await (zeroEx as any)._web3Wrapper.getBalanceInWeiAsync(addressWithETH);
+ const preWETHBalance = await zeroEx.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 zeroEx.etherToken.withdrawAsync(depositWeiAmount, addressWithETH);
+ await zeroEx.awaitTransactionMinedAsync(txHash);
+
+ const postETHBalance = await (zeroEx as any)._web3Wrapper.getBalanceInWeiAsync(addressWithETH);
+ const postWETHBalanceInBaseUnits = await zeroEx.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 withdrawl', async () => {
+ const preWETHBalance = await zeroEx.token.getBalanceAsync(wethContractAddress, addressWithETH);
+ expect(preWETHBalance).to.be.bignumber.equal(0);
+
+ const overWETHBalance = preWETHBalance.add(999999999);
+
+ return expect(
+ zeroEx.etherToken.withdrawAsync(overWETHBalance, addressWithETH),
+ ).to.be.rejectedWith(ZeroExError.InsufficientWEthBalanceForWithdrawal);
+ });
+ });
+});
diff --git a/packages/0x.js/test/event_watcher_test.ts b/packages/0x.js/test/event_watcher_test.ts
new file mode 100644
index 000000000..b4164fe63
--- /dev/null
+++ b/packages/0x.js/test/event_watcher_test.ts
@@ -0,0 +1,127 @@
+import 'mocha';
+import * as chai from 'chai';
+import * as _ from 'lodash';
+import * as Sinon from 'sinon';
+import * as Web3 from 'web3';
+import BigNumber from 'bignumber.js';
+import {chaiSetup} from './utils/chai_setup';
+import {web3Factory} from './utils/web3_factory';
+import {Web3Wrapper} from '../src/web3_wrapper';
+import {EventWatcher} from '../src/order_watcher/event_watcher';
+import {
+ ZeroEx,
+ LogEvent,
+ DecodedLogEvent,
+} from '../src';
+import {DoneCallback} from '../src/types';
+
+chaiSetup.configure();
+const expect = chai.expect;
+
+describe('EventWatcher', () => {
+ let web3: Web3;
+ let stubs: Sinon.SinonStub[] = [];
+ let eventWatcher: EventWatcher;
+ let web3Wrapper: Web3Wrapper;
+ const numConfirmations = 0;
+ const logA: Web3.LogEntry = {
+ address: '0x71d271f8b14adef568f8f28f1587ce7271ac4ca5',
+ blockHash: null,
+ blockNumber: null,
+ data: '',
+ logIndex: null,
+ topics: [],
+ transactionHash: '0x004881d38cd4a8f72f1a0d68c8b9b8124504706041ff37019c1d1ed6bfda8e17',
+ transactionIndex: 0,
+ };
+ const logB: Web3.LogEntry = {
+ address: '0x8d12a197cb00d4747a1fe03395095ce2a5cc6819',
+ blockHash: null,
+ blockNumber: null,
+ data: '',
+ logIndex: null,
+ topics: [ '0xf341246adaac6f497bc2a656f546ab9e182111d630394f0c57c710a59a2cb567' ],
+ transactionHash: '0x01ef3c048b18d9b09ea195b4ed94cf8dd5f3d857a1905ff886b152cfb1166f25',
+ transactionIndex: 0,
+ };
+ const logC: Web3.LogEntry = {
+ address: '0x1d271f8b174adef58f1587ce68f8f27271ac4ca5',
+ blockHash: null,
+ blockNumber: null,
+ data: '',
+ logIndex: null,
+ topics: [ '0xf341246adaac6f497bc2a656f546ab9e182111d630394f0c57c710a59a2cb567' ],
+ transactionHash: '0x01ef3c048b18d9b09ea195b4ed94cf8dd5f3d857a1905ff886b152cfb1166f25',
+ transactionIndex: 0,
+ };
+ before(async () => {
+ web3 = web3Factory.create();
+ const pollingIntervalMs = 10;
+ web3Wrapper = new Web3Wrapper(web3.currentProvider);
+ eventWatcher = new EventWatcher(web3Wrapper, pollingIntervalMs);
+ });
+ afterEach(() => {
+ // clean up any stubs after the test has completed
+ _.each(stubs, s => s.restore());
+ stubs = [];
+ eventWatcher.unsubscribe();
+ });
+ it('correctly emits initial log events', (done: DoneCallback) => {
+ const logs: Web3.LogEntry[] = [logA, logB];
+ const expectedLogEvents = [
+ {
+ removed: false,
+ ...logA,
+ },
+ {
+ removed: false,
+ ...logB,
+ },
+ ];
+ const getLogsStub = Sinon.stub(web3Wrapper, 'getLogsAsync');
+ getLogsStub.onCall(0).returns(logs);
+ stubs.push(getLogsStub);
+ const callback = (event: LogEvent) => {
+ const expectedLogEvent = expectedLogEvents.shift();
+ expect(event).to.be.deep.equal(expectedLogEvent);
+ if (_.isEmpty(expectedLogEvents)) {
+ done();
+ }
+ };
+ eventWatcher.subscribe(callback);
+ });
+ it('correctly computes the difference and emits only changes', (done: DoneCallback) => {
+ const initialLogs: Web3.LogEntry[] = [logA, logB];
+ const changedLogs: Web3.LogEntry[] = [logA, logC];
+ const expectedLogEvents = [
+ {
+ removed: false,
+ ...logA,
+ },
+ {
+ removed: false,
+ ...logB,
+ },
+ {
+ removed: true,
+ ...logB,
+ },
+ {
+ removed: false,
+ ...logC,
+ },
+ ];
+ const getLogsStub = Sinon.stub(web3Wrapper, 'getLogsAsync');
+ getLogsStub.onCall(0).returns(initialLogs);
+ getLogsStub.onCall(1).returns(changedLogs);
+ stubs.push(getLogsStub);
+ const callback = (event: LogEvent) => {
+ const expectedLogEvent = expectedLogEvents.shift();
+ expect(event).to.be.deep.equal(expectedLogEvent);
+ if (_.isEmpty(expectedLogEvents)) {
+ done();
+ }
+ };
+ eventWatcher.subscribe(callback);
+ });
+});
diff --git a/packages/0x.js/test/exchange_transfer_simulator_test.ts b/packages/0x.js/test/exchange_transfer_simulator_test.ts
new file mode 100644
index 000000000..99cb7fb4f
--- /dev/null
+++ b/packages/0x.js/test/exchange_transfer_simulator_test.ts
@@ -0,0 +1,87 @@
+import * as chai from 'chai';
+import BigNumber from 'bignumber.js';
+import {chaiSetup} from './utils/chai_setup';
+import {web3Factory} from './utils/web3_factory';
+import {ZeroEx, ExchangeContractErrs, Token} from '../src';
+import {TradeSide, TransferType} from '../src/types';
+import {BlockchainLifecycle} from './utils/blockchain_lifecycle';
+import {ExchangeTransferSimulator} from '../src/utils/exchange_transfer_simulator';
+
+chaiSetup.configure();
+const expect = chai.expect;
+const blockchainLifecycle = new BlockchainLifecycle();
+
+describe('ExchangeTransferSimulator', () => {
+ const web3 = web3Factory.create();
+ const zeroEx = new ZeroEx(web3.currentProvider);
+ 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 zeroEx.getAvailableAddressesAsync();
+ [coinbase, sender, recipient] = userAddresses;
+ tokens = await zeroEx.tokenRegistry.getTokensAsync();
+ exampleTokenAddress = tokens[0].address;
+ });
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ describe('#transferFromAsync', () => {
+ beforeEach(() => {
+ exchangeTransferSimulator = new ExchangeTransferSimulator(zeroEx.token);
+ });
+ 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 zeroEx.token.setProxyAllowanceAsync(exampleTokenAddress, sender, transferAmount);
+ await zeroEx.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 zeroEx.token.transferAsync(exampleTokenAddress, coinbase, sender, transferAmount);
+ await zeroEx.awaitTransactionMinedAsync(txHash);
+ txHash = await zeroEx.token.setProxyAllowanceAsync(exampleTokenAddress, sender, transferAmount);
+ await zeroEx.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 zeroEx.token.transferAsync(exampleTokenAddress, coinbase, sender, transferAmount);
+ await zeroEx.awaitTransactionMinedAsync(txHash);
+ txHash = await zeroEx.token.setUnlimitedProxyAllowanceAsync(exampleTokenAddress, sender);
+ await zeroEx.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(zeroEx.token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS);
+ });
+ });
+});
diff --git a/packages/0x.js/test/exchange_wrapper_test.ts b/packages/0x.js/test/exchange_wrapper_test.ts
new file mode 100644
index 000000000..26b8c1e0e
--- /dev/null
+++ b/packages/0x.js/test/exchange_wrapper_test.ts
@@ -0,0 +1,824 @@
+import 'mocha';
+import * as chai from 'chai';
+import * as Web3 from 'web3';
+import BigNumber from 'bignumber.js';
+import {chaiSetup} from './utils/chai_setup';
+import {web3Factory} from './utils/web3_factory';
+import {BlockchainLifecycle} from './utils/blockchain_lifecycle';
+import {
+ ZeroEx,
+ Token,
+ SignedOrder,
+ SubscriptionOpts,
+ ExchangeEvents,
+ ExchangeContractErrs,
+ OrderCancellationRequest,
+ OrderFillRequest,
+ LogFillContractEventArgs,
+ LogCancelContractEventArgs,
+ LogEvent,
+ DecodedLogEvent,
+} from '../src';
+import {DoneCallback, BlockParamLiteral} from '../src/types';
+import {FillScenarios} from './utils/fill_scenarios';
+import {TokenUtils} from './utils/token_utils';
+
+chaiSetup.configure();
+const expect = chai.expect;
+const blockchainLifecycle = new BlockchainLifecycle();
+
+const NON_EXISTENT_ORDER_HASH = '0x79370342234e7acd6bbeac335bd3bb1d368383294b64b8160a00f4060e4d3777';
+
+describe('ExchangeWrapper', () => {
+ let web3: Web3;
+ let zeroEx: ZeroEx;
+ let tokenUtils: TokenUtils;
+ let tokens: Token[];
+ let userAddresses: string[];
+ let zrxTokenAddress: string;
+ let fillScenarios: FillScenarios;
+ let exchangeContractAddress: string;
+ before(async () => {
+ web3 = web3Factory.create();
+ zeroEx = new ZeroEx(web3.currentProvider);
+ exchangeContractAddress = await zeroEx.exchange.getContractAddressAsync();
+ userAddresses = await zeroEx.getAvailableAddressesAsync();
+ tokens = await zeroEx.tokenRegistry.getTokensAsync();
+ tokenUtils = new TokenUtils(tokens);
+ zrxTokenAddress = tokenUtils.getProtocolTokenOrThrow().address;
+ fillScenarios = new FillScenarios(zeroEx, userAddresses, tokens, zrxTokenAddress, exchangeContractAddress);
+ });
+ 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 zeroEx.tokenRegistry.getTokensAsync();
+ const [makerToken, takerToken] = tokenUtils.getNonProtocolTokens();
+ 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 zeroEx.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(zeroEx.exchange.batchFillOrKillAsync(orderFillRequests, takerAddress))
+ .to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
+ });
+ it('should validate when orderTransactionOptions specify to validate', async () => {
+ return expect(zeroEx.exchange.batchFillOrKillAsync(orderFillRequests, takerAddress, {
+ shouldValidate: true,
+ })).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
+ });
+ it('should not validate when orderTransactionOptions specify not to validate', async () => {
+ return expect(zeroEx.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 zeroEx.token.getBalanceAsync(makerTokenAddress, makerAddress))
+ .to.be.bignumber.equal(fillableAmount);
+ expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, makerAddress))
+ .to.be.bignumber.equal(0);
+ expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, takerAddress))
+ .to.be.bignumber.equal(0);
+ expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, takerAddress))
+ .to.be.bignumber.equal(fillableAmount);
+ await zeroEx.exchange.fillOrKillOrderAsync(signedOrder, takerTokenFillAmount, takerAddress);
+ expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, makerAddress))
+ .to.be.bignumber.equal(fillableAmount.minus(takerTokenFillAmount));
+ expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, makerAddress))
+ .to.be.bignumber.equal(takerTokenFillAmount);
+ expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, takerAddress))
+ .to.be.bignumber.equal(takerTokenFillAmount);
+ expect(await zeroEx.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 zeroEx.exchange.fillOrKillOrderAsync(signedOrder, partialFillAmount, takerAddress);
+ expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, makerAddress))
+ .to.be.bignumber.equal(fillableAmount.minus(partialFillAmount));
+ expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, makerAddress))
+ .to.be.bignumber.equal(partialFillAmount);
+ expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, takerAddress))
+ .to.be.bignumber.equal(partialFillAmount);
+ expect(await zeroEx.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(zeroEx.exchange.fillOrKillOrderAsync(signedOrder, emptyFillableAmount, takerAddress))
+ .to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
+ });
+ it('should validate when orderTransactionOptions specify to validate', async () => {
+ return expect(zeroEx.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(zeroEx.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 zeroEx.tokenRegistry.getTokensAsync();
+ const [makerToken, takerToken] = tokenUtils.getNonProtocolTokens();
+ 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 zeroEx.token.getBalanceAsync(makerTokenAddress, makerAddress))
+ .to.be.bignumber.equal(fillableAmount);
+ expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, makerAddress))
+ .to.be.bignumber.equal(0);
+ expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, takerAddress))
+ .to.be.bignumber.equal(0);
+ expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, takerAddress))
+ .to.be.bignumber.equal(fillableAmount);
+ const txHash = await zeroEx.exchange.fillOrderAsync(
+ signedOrder, takerTokenFillAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress);
+ await zeroEx.awaitTransactionMinedAsync(txHash);
+ expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, makerAddress))
+ .to.be.bignumber.equal(fillableAmount.minus(takerTokenFillAmount));
+ expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, makerAddress))
+ .to.be.bignumber.equal(takerTokenFillAmount);
+ expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, takerAddress))
+ .to.be.bignumber.equal(takerTokenFillAmount);
+ expect(await zeroEx.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 zeroEx.exchange.fillOrderAsync(
+ signedOrder, partialFillAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress);
+ await zeroEx.awaitTransactionMinedAsync(txHash);
+ expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, makerAddress))
+ .to.be.bignumber.equal(fillableAmount.minus(partialFillAmount));
+ expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, makerAddress))
+ .to.be.bignumber.equal(partialFillAmount);
+ expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, takerAddress))
+ .to.be.bignumber.equal(partialFillAmount);
+ expect(await zeroEx.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 zeroEx.exchange.fillOrderAsync(
+ signedOrder, takerTokenFillAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress);
+ await zeroEx.awaitTransactionMinedAsync(txHash);
+ expect(await zeroEx.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(zeroEx.exchange.fillOrderAsync(
+ signedOrder, emptyFillTakerAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress,
+ )).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
+ });
+ it('should validate when orderTransactionOptions specify to validate', async () => {
+ return expect(zeroEx.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(zeroEx.exchange.fillOrderAsync(
+ signedOrder, emptyFillTakerAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, {
+ shouldValidate: false,
+ })).to.not.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
+ });
+ });
+ });
+ 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 = ZeroEx.getOrderHashHex(signedOrder);
+ anotherSignedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
+ );
+ anotherOrderHashHex = ZeroEx.getOrderHashHex(anotherSignedOrder);
+ });
+ describe('successful batch fills', () => {
+ beforeEach(() => {
+ orderFillBatch = [
+ {
+ signedOrder,
+ takerTokenFillAmount,
+ },
+ {
+ signedOrder: anotherSignedOrder,
+ takerTokenFillAmount,
+ },
+ ];
+ });
+ it('should throw if a batch is empty', async () => {
+ return expect(zeroEx.exchange.batchFillOrdersAsync(
+ [], shouldThrowOnInsufficientBalanceOrAllowance, takerAddress),
+ ).to.be.rejectedWith(ExchangeContractErrs.BatchOrdersMustHaveAtLeastOneItem);
+ });
+ it('should successfully fill multiple orders', async () => {
+ const txHash = await zeroEx.exchange.batchFillOrdersAsync(
+ orderFillBatch, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress);
+ await zeroEx.awaitTransactionMinedAsync(txHash);
+ const filledAmount = await zeroEx.exchange.getFilledTakerAmountAsync(signedOrderHashHex);
+ const anotherFilledAmount = await zeroEx.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(zeroEx.exchange.batchFillOrdersAsync(
+ orderFillBatch, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress),
+ ).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
+ });
+ it('should validate when orderTransactionOptions specify to validate', async () => {
+ return expect(zeroEx.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(zeroEx.exchange.batchFillOrdersAsync(
+ orderFillBatch, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, {
+ shouldValidate: false,
+ })).to.not.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
+ });
+ });
+ });
+ 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 = ZeroEx.getOrderHashHex(signedOrder);
+ anotherSignedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
+ );
+ anotherOrderHashHex = ZeroEx.getOrderHashHex(anotherSignedOrder);
+ signedOrders = [signedOrder, anotherSignedOrder];
+ });
+ describe('successful batch fills', () => {
+ it('should throw if a batch is empty', async () => {
+ return expect(zeroEx.exchange.fillOrdersUpToAsync(
+ [], fillUpToAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress),
+ ).to.be.rejectedWith(ExchangeContractErrs.BatchOrdersMustHaveAtLeastOneItem);
+ });
+ it('should successfully fill up to specified amount', async () => {
+ const txHash = await zeroEx.exchange.fillOrdersUpToAsync(
+ signedOrders, fillUpToAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress,
+ );
+ await zeroEx.awaitTransactionMinedAsync(txHash);
+ const filledAmount = await zeroEx.exchange.getFilledTakerAmountAsync(signedOrderHashHex);
+ const anotherFilledAmount = await zeroEx.exchange.getFilledTakerAmountAsync(anotherOrderHashHex);
+ expect(filledAmount).to.be.bignumber.equal(fillableAmount);
+ const remainingFillAmount = fillableAmount.minus(1);
+ expect(anotherFilledAmount).to.be.bignumber.equal(remainingFillAmount);
+ });
+ });
+ describe('order transaction options', () => {
+ const emptyFillUpToAmount = new BigNumber(0);
+ it('should validate when orderTransactionOptions are not present', async () => {
+ return expect(zeroEx.exchange.fillOrdersUpToAsync(
+ signedOrders, emptyFillUpToAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress,
+ )).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
+ });
+ it('should validate when orderTransactionOptions specify to validate', async () => {
+ return expect(zeroEx.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(zeroEx.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.getNonProtocolTokens();
+ makerTokenAddress = makerToken.address;
+ takerTokenAddress = takerToken.address;
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
+ );
+ orderHashHex = ZeroEx.getOrderHashHex(signedOrder);
+ });
+ describe('#cancelOrderAsync', () => {
+ describe('successful cancels', () => {
+ it('should cancel an order', async () => {
+ const txHash = await zeroEx.exchange.cancelOrderAsync(signedOrder, cancelAmount);
+ await zeroEx.awaitTransactionMinedAsync(txHash);
+ const cancelledAmount = await zeroEx.exchange.getCanceledTakerAmountAsync(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(zeroEx.exchange.cancelOrderAsync(signedOrder, emptyCancelTakerTokenAmount))
+ .to.be.rejectedWith(ExchangeContractErrs.OrderCancelAmountZero);
+ });
+ it('should validate when orderTransactionOptions specify to validate', async () => {
+ return expect(zeroEx.exchange.cancelOrderAsync(signedOrder, emptyCancelTakerTokenAmount, {
+ shouldValidate: true,
+ })).to.be.rejectedWith(ExchangeContractErrs.OrderCancelAmountZero);
+ });
+ it('should not validate when orderTransactionOptions specify not to validate', async () => {
+ return expect(zeroEx.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 = ZeroEx.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(zeroEx.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 zeroEx.exchange.batchCancelOrdersAsync(cancelBatch);
+ const cancelledAmount = await zeroEx.exchange.getCanceledTakerAmountAsync(orderHashHex);
+ const anotherCancelledAmount = await zeroEx.exchange.getCanceledTakerAmountAsync(
+ 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(zeroEx.exchange.batchCancelOrdersAsync(cancelBatch))
+ .to.be.rejectedWith(ExchangeContractErrs.OrderCancelAmountZero);
+ });
+ it('should validate when orderTransactionOptions specify to validate', async () => {
+ return expect(zeroEx.exchange.batchCancelOrdersAsync(cancelBatch, {
+ shouldValidate: true,
+ })).to.be.rejectedWith(ExchangeContractErrs.OrderCancelAmountZero);
+ });
+ it('should not validate when orderTransactionOptions specify not to validate', async () => {
+ return expect(zeroEx.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];
+ const [makerToken, takerToken] = tokens;
+ 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 = ZeroEx.getOrderHashHex(signedOrder);
+ });
+ describe('#getUnavailableTakerAmountAsync', () => {
+ it('should throw if passed an invalid orderHash', async () => {
+ const invalidOrderHashHex = '0x123';
+ return expect(zeroEx.exchange.getUnavailableTakerAmountAsync(invalidOrderHashHex)).to.be.rejected();
+ });
+ it('should return zero if passed a valid but non-existent orderHash', async () => {
+ const unavailableValueT = await zeroEx.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 zeroEx.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(zeroEx.exchange.getFilledTakerAmountAsync(invalidOrderHashHex)).to.be.rejected();
+ });
+ it('should return zero if passed a valid but non-existent orderHash', async () => {
+ const filledValueT = await zeroEx.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 zeroEx.exchange.getFilledTakerAmountAsync(orderHash);
+ expect(filledValueT).to.be.bignumber.equal(partialFillAmount);
+ });
+ });
+ describe('#getCanceledTakerAmountAsync', () => {
+ it('should throw if passed an invalid orderHash', async () => {
+ const invalidOrderHashHex = '0x123';
+ return expect(zeroEx.exchange.getCanceledTakerAmountAsync(invalidOrderHashHex)).to.be.rejected();
+ });
+ it('should return zero if passed a valid but non-existent orderHash', async () => {
+ const cancelledValueT = await zeroEx.exchange.getCanceledTakerAmountAsync(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 zeroEx.exchange.getCanceledTakerAmountAsync(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 zeroEx.exchange.cancelOrderAsync(signedOrder, cancelAmount);
+ const cancelledValueT = await zeroEx.exchange.getCanceledTakerAmountAsync(orderHash);
+ expect(cancelledValueT).to.be.bignumber.equal(cancelAmount);
+ });
+ });
+ });
+ describe('#subscribeAsync', () => {
+ 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] = tokens;
+ makerTokenAddress = makerToken.address;
+ takerTokenAddress = takerToken.address;
+ });
+ beforeEach(async () => {
+ fillableAmount = new BigNumber(5);
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
+ );
+ });
+ afterEach(async () => {
+ zeroEx.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 = (err: Error, logEvent: DecodedLogEvent<LogFillContractEventArgs>) => {
+ expect(logEvent.event).to.be.equal(ExchangeEvents.LogFill);
+ done();
+ };
+ await zeroEx.exchange.subscribeAsync(
+ ExchangeEvents.LogFill, indexFilterValues, callback,
+ );
+ await zeroEx.exchange.fillOrderAsync(
+ signedOrder, takerTokenFillAmountInBaseUnits, shouldThrowOnInsufficientBalanceOrAllowance,
+ takerAddress,
+ );
+ })().catch(done);
+ });
+ it('Should receive the LogCancel event when an order is cancelled', (done: DoneCallback) => {
+ (async () => {
+
+ const callback = (err: Error, logEvent: DecodedLogEvent<LogCancelContractEventArgs>) => {
+ expect(logEvent.event).to.be.equal(ExchangeEvents.LogCancel);
+ done();
+ };
+ await zeroEx.exchange.subscribeAsync(
+ ExchangeEvents.LogCancel, indexFilterValues, callback,
+ );
+ await zeroEx.exchange.cancelOrderAsync(signedOrder, cancelTakerAmountInBaseUnits);
+ })().catch(done);
+ });
+ it('Outstanding subscriptions are cancelled when zeroEx.setProviderAsync called', (done: DoneCallback) => {
+ (async () => {
+
+ const callbackNeverToBeCalled = (err: Error, logEvent: DecodedLogEvent<LogFillContractEventArgs>) => {
+ done(new Error('Expected this subscription to have been cancelled'));
+ };
+ await zeroEx.exchange.subscribeAsync(
+ ExchangeEvents.LogFill, indexFilterValues, callbackNeverToBeCalled,
+ );
+
+ const newProvider = web3Factory.getRpcProvider();
+ await zeroEx.setProviderAsync(newProvider);
+
+ const callback = (err: Error, logEvent: DecodedLogEvent<LogFillContractEventArgs>) => {
+ expect(logEvent.event).to.be.equal(ExchangeEvents.LogFill);
+ done();
+ };
+ await zeroEx.exchange.subscribeAsync(
+ ExchangeEvents.LogFill, indexFilterValues, callback,
+ );
+ await zeroEx.exchange.fillOrderAsync(
+ signedOrder, takerTokenFillAmountInBaseUnits, shouldThrowOnInsufficientBalanceOrAllowance,
+ takerAddress,
+ );
+ })().catch(done);
+ });
+ it('Should cancel subscription when unsubscribe called', (done: DoneCallback) => {
+ (async () => {
+ const callbackNeverToBeCalled = (err: Error, logEvent: DecodedLogEvent<LogFillContractEventArgs>) => {
+ done(new Error('Expected this subscription to have been cancelled'));
+ };
+ const subscriptionToken = await zeroEx.exchange.subscribeAsync(
+ ExchangeEvents.LogFill, indexFilterValues, callbackNeverToBeCalled,
+ );
+ zeroEx.exchange.unsubscribe(subscriptionToken);
+ await zeroEx.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.getNonProtocolTokens();
+ 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 = ZeroEx.getOrderHashHex(signedOrder);
+ const orderHashFromContract = await (zeroEx.exchange as any)
+ ._getOrderHashHexUsingContractCallAsync(signedOrder);
+ expect(orderHash).to.equal(orderHashFromContract);
+ });
+ });
+ describe('#getZRXTokenAddressAsync', () => {
+ it('gets the same token as is in token registry', async () => {
+ const zrxAddress = await zeroEx.exchange.getZRXTokenAddressAsync();
+ 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 subscriptionOpts: SubscriptionOpts = {
+ fromBlock: BlockParamLiteral.Earliest,
+ toBlock: BlockParamLiteral.Latest,
+ };
+ let txHash: string;
+ before(async () => {
+ [, makerAddress, takerAddress] = userAddresses;
+ const [makerToken, takerToken] = tokenUtils.getNonProtocolTokens();
+ 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 zeroEx.exchange.fillOrderAsync(
+ signedOrder, fillableAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress,
+ );
+ await zeroEx.awaitTransactionMinedAsync(txHash);
+ const eventName = ExchangeEvents.LogFill;
+ const indexFilterValues = {};
+ const logs = await zeroEx.exchange.getLogsAsync(eventName, subscriptionOpts, 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 zeroEx.exchange.fillOrderAsync(
+ signedOrder, fillableAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress,
+ );
+ await zeroEx.awaitTransactionMinedAsync(txHash);
+ const differentEventName = ExchangeEvents.LogCancel;
+ const indexFilterValues = {};
+ const logs = await zeroEx.exchange.getLogsAsync(differentEventName, subscriptionOpts, 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 zeroEx.exchange.fillOrderAsync(
+ signedOrder, fillableAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress,
+ );
+ await zeroEx.awaitTransactionMinedAsync(txHash);
+
+ const differentMakerAddress = userAddresses[2];
+ const anotherSignedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, differentMakerAddress, takerAddress, fillableAmount,
+ );
+ txHash = await zeroEx.exchange.fillOrderAsync(
+ anotherSignedOrder, fillableAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress,
+ );
+ await zeroEx.awaitTransactionMinedAsync(txHash);
+
+ const eventName = ExchangeEvents.LogFill;
+ const indexFilterValues = {
+ maker: differentMakerAddress,
+ };
+ const logs = await zeroEx.exchange.getLogsAsync<LogFillContractEventArgs>(
+ eventName, subscriptionOpts, indexFilterValues,
+ );
+ expect(logs).to.have.length(1);
+ const args = logs[0].args;
+ expect(args.maker).to.be.equal(differentMakerAddress);
+ });
+ });
+});
diff --git a/packages/0x.js/test/order_state_watcher_test.ts b/packages/0x.js/test/order_state_watcher_test.ts
new file mode 100644
index 000000000..c8a4a8064
--- /dev/null
+++ b/packages/0x.js/test/order_state_watcher_test.ts
@@ -0,0 +1,356 @@
+import 'mocha';
+import * as chai from 'chai';
+import * as _ from 'lodash';
+import * as Web3 from 'web3';
+import BigNumber from 'bignumber.js';
+import { chaiSetup } from './utils/chai_setup';
+import { web3Factory } from './utils/web3_factory';
+import { Web3Wrapper } from '../src/web3_wrapper';
+import { OrderStateWatcher } from '../src/order_watcher/order_state_watcher';
+import {
+ Token,
+ ZeroEx,
+ LogEvent,
+ DecodedLogEvent,
+ ZeroExConfig,
+ OrderState,
+ SignedOrder,
+ ZeroExError,
+ OrderStateValid,
+ OrderStateInvalid,
+ ExchangeContractErrs,
+} from '../src';
+import { TokenUtils } from './utils/token_utils';
+import { FillScenarios } from './utils/fill_scenarios';
+import { DoneCallback } from '../src/types';
+import {BlockchainLifecycle} from './utils/blockchain_lifecycle';
+import {reportCallbackErrors} from './utils/report_callback_errors';
+
+const TIMEOUT_MS = 150;
+
+chaiSetup.configure();
+const expect = chai.expect;
+const blockchainLifecycle = new BlockchainLifecycle();
+
+describe('OrderStateWatcher', () => {
+ let web3: Web3;
+ let zeroEx: ZeroEx;
+ let tokens: Token[];
+ let tokenUtils: TokenUtils;
+ let fillScenarios: FillScenarios;
+ let userAddresses: string[];
+ let zrxTokenAddress: string;
+ let exchangeContractAddress: string;
+ let makerToken: Token;
+ let takerToken: Token;
+ let maker: string;
+ let taker: string;
+ let web3Wrapper: Web3Wrapper;
+ let signedOrder: SignedOrder;
+ const fillableAmount = new BigNumber(5);
+ before(async () => {
+ web3 = web3Factory.create();
+ zeroEx = new ZeroEx(web3.currentProvider);
+ exchangeContractAddress = await zeroEx.exchange.getContractAddressAsync();
+ userAddresses = await zeroEx.getAvailableAddressesAsync();
+ [, maker, taker] = userAddresses;
+ tokens = await zeroEx.tokenRegistry.getTokensAsync();
+ tokenUtils = new TokenUtils(tokens);
+ zrxTokenAddress = tokenUtils.getProtocolTokenOrThrow().address;
+ fillScenarios = new FillScenarios(zeroEx, userAddresses, tokens, zrxTokenAddress, exchangeContractAddress);
+ [makerToken, takerToken] = tokenUtils.getNonProtocolTokens();
+ web3Wrapper = (zeroEx as any)._web3Wrapper;
+ });
+ describe('#removeOrder', async () => {
+ it('should successfully remove existing order', async () => {
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerToken.address, takerToken.address, maker, taker, fillableAmount,
+ );
+ const orderHash = ZeroEx.getOrderHashHex(signedOrder);
+ zeroEx.orderStateWatcher.addOrder(signedOrder);
+ expect((zeroEx.orderStateWatcher as any)._orderByOrderHash).to.include({
+ [orderHash]: signedOrder,
+ });
+ let dependentOrderHashes = (zeroEx.orderStateWatcher as any)._dependentOrderHashes;
+ expect(dependentOrderHashes[signedOrder.maker][signedOrder.makerTokenAddress]).to.have.keys(orderHash);
+ zeroEx.orderStateWatcher.removeOrder(orderHash);
+ expect((zeroEx.orderStateWatcher as any)._orderByOrderHash).to.not.include({
+ [orderHash]: signedOrder,
+ });
+ dependentOrderHashes = (zeroEx.orderStateWatcher as any)._dependentOrderHashes;
+ expect(dependentOrderHashes[signedOrder.maker]).to.be.undefined();
+ });
+ it('should no-op when removing a non-existing order', async () => {
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerToken.address, takerToken.address, maker, taker, fillableAmount,
+ );
+ const orderHash = ZeroEx.getOrderHashHex(signedOrder);
+ const nonExistentOrderHash = `0x${orderHash.substr(2).split('').reverse().join('')}`;
+ zeroEx.orderStateWatcher.removeOrder(nonExistentOrderHash);
+ });
+ });
+ describe('#subscribe', async () => {
+ afterEach(async () => {
+ zeroEx.orderStateWatcher.unsubscribe();
+ });
+ it('should fail when trying to subscribe twice', async () => {
+ zeroEx.orderStateWatcher.subscribe(_.noop);
+ expect(() => zeroEx.orderStateWatcher.subscribe(_.noop))
+ .to.throw(ZeroExError.SubscriptionAlreadyPresent);
+ });
+ });
+ describe('tests with cleanup', async () => {
+ afterEach(async () => {
+ zeroEx.orderStateWatcher.unsubscribe();
+ const orderHash = ZeroEx.getOrderHashHex(signedOrder);
+ zeroEx.orderStateWatcher.removeOrder(orderHash);
+ });
+ it('should emit orderStateInvalid when maker allowance set to 0 for watched order', (done: DoneCallback) => {
+ (async () => {
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerToken.address, takerToken.address, maker, taker, fillableAmount,
+ );
+ const orderHash = ZeroEx.getOrderHashHex(signedOrder);
+ zeroEx.orderStateWatcher.addOrder(signedOrder);
+ const callback = reportCallbackErrors(done)((orderState: OrderState) => {
+ expect(orderState.isValid).to.be.false();
+ const invalidOrderState = orderState as OrderStateInvalid;
+ expect(invalidOrderState.orderHash).to.be.equal(orderHash);
+ expect(invalidOrderState.error).to.be.equal(ExchangeContractErrs.InsufficientMakerAllowance);
+ done();
+ });
+ zeroEx.orderStateWatcher.subscribe(callback);
+ await zeroEx.token.setProxyAllowanceAsync(makerToken.address, maker, new BigNumber(0));
+ })().catch(done);
+ });
+ it('should not emit an orderState event when irrelevant Transfer event received', (done: DoneCallback) => {
+ (async () => {
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerToken.address, takerToken.address, maker, taker, fillableAmount,
+ );
+ const orderHash = ZeroEx.getOrderHashHex(signedOrder);
+ zeroEx.orderStateWatcher.addOrder(signedOrder);
+ const callback = reportCallbackErrors(done)((orderState: OrderState) => {
+ throw new Error('OrderState callback fired for irrelevant order');
+ });
+ zeroEx.orderStateWatcher.subscribe(callback);
+ const notTheMaker = userAddresses[0];
+ const anyRecipient = taker;
+ const transferAmount = new BigNumber(2);
+ const notTheMakerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, notTheMaker);
+ await zeroEx.token.transferAsync(makerToken.address, notTheMaker, anyRecipient, transferAmount);
+ setTimeout(() => {
+ done();
+ }, TIMEOUT_MS);
+ })().catch(done);
+ });
+ it('should emit orderStateInvalid when maker moves balance backing watched order', (done: DoneCallback) => {
+ (async () => {
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerToken.address, takerToken.address, maker, taker, fillableAmount,
+ );
+ const orderHash = ZeroEx.getOrderHashHex(signedOrder);
+ zeroEx.orderStateWatcher.addOrder(signedOrder);
+ const callback = reportCallbackErrors(done)((orderState: OrderState) => {
+ expect(orderState.isValid).to.be.false();
+ const invalidOrderState = orderState as OrderStateInvalid;
+ expect(invalidOrderState.orderHash).to.be.equal(orderHash);
+ expect(invalidOrderState.error).to.be.equal(ExchangeContractErrs.InsufficientMakerBalance);
+ done();
+ });
+ zeroEx.orderStateWatcher.subscribe(callback);
+ const anyRecipient = taker;
+ const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker);
+ await zeroEx.token.transferAsync(makerToken.address, maker, anyRecipient, makerBalance);
+ })().catch(done);
+ });
+ it('should emit orderStateInvalid when watched order fully filled', (done: DoneCallback) => {
+ (async () => {
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerToken.address, takerToken.address, maker, taker, fillableAmount,
+ );
+ const orderHash = ZeroEx.getOrderHashHex(signedOrder);
+ zeroEx.orderStateWatcher.addOrder(signedOrder);
+
+ let eventCount = 0;
+ const callback = reportCallbackErrors(done)((orderState: OrderState) => {
+ eventCount++;
+ expect(orderState.isValid).to.be.false();
+ const invalidOrderState = orderState as OrderStateInvalid;
+ expect(invalidOrderState.orderHash).to.be.equal(orderHash);
+ expect(invalidOrderState.error).to.be.equal(ExchangeContractErrs.OrderRemainingFillAmountZero);
+ if (eventCount === 2) {
+ done();
+ }
+ });
+ zeroEx.orderStateWatcher.subscribe(callback);
+
+ const shouldThrowOnInsufficientBalanceOrAllowance = true;
+ await zeroEx.exchange.fillOrderAsync(
+ signedOrder, fillableAmount, shouldThrowOnInsufficientBalanceOrAllowance, taker,
+ );
+ })().catch(done);
+ });
+ it('should emit orderStateValid when watched order partially filled', (done: DoneCallback) => {
+ (async () => {
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerToken.address, takerToken.address, maker, taker, fillableAmount,
+ );
+
+ const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker);
+ const takerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, taker);
+
+ const fillAmountInBaseUnits = new BigNumber(2);
+ const orderHash = ZeroEx.getOrderHashHex(signedOrder);
+ zeroEx.orderStateWatcher.addOrder(signedOrder);
+
+ let eventCount = 0;
+ const callback = reportCallbackErrors(done)((orderState: OrderState) => {
+ eventCount++;
+ expect(orderState.isValid).to.be.true();
+ const validOrderState = orderState as OrderStateValid;
+ expect(validOrderState.orderHash).to.be.equal(orderHash);
+ const orderRelevantState = validOrderState.orderRelevantState;
+ const remainingMakerBalance = makerBalance.sub(fillAmountInBaseUnits);
+ const remainingFillable = fillableAmount.minus(fillAmountInBaseUnits);
+ expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal(
+ remainingFillable);
+ expect(orderRelevantState.makerBalance).to.be.bignumber.equal(remainingMakerBalance);
+ if (eventCount === 2) {
+ done();
+ }
+ });
+ zeroEx.orderStateWatcher.subscribe(callback);
+ const shouldThrowOnInsufficientBalanceOrAllowance = true;
+ await zeroEx.exchange.fillOrderAsync(
+ signedOrder, fillAmountInBaseUnits, shouldThrowOnInsufficientBalanceOrAllowance, taker,
+ );
+ })().catch(done);
+ });
+ describe('remainingFillableMakerTokenAmount', () => {
+ it('should calculate correct remaining fillable', (done: DoneCallback) => {
+ (async () => {
+ const takerFillableAmount = new BigNumber(10);
+ const makerFillableAmount = new BigNumber(20);
+ signedOrder = await fillScenarios.createAsymmetricFillableSignedOrderAsync(
+ makerToken.address, takerToken.address, maker, taker, makerFillableAmount, takerFillableAmount);
+ const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker);
+ const takerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, taker);
+ const fillAmountInBaseUnits = new BigNumber(2);
+ const orderHash = ZeroEx.getOrderHashHex(signedOrder);
+ zeroEx.orderStateWatcher.addOrder(signedOrder);
+ let eventCount = 0;
+ const callback = reportCallbackErrors(done)((orderState: OrderState) => {
+ eventCount++;
+ expect(orderState.isValid).to.be.true();
+ const validOrderState = orderState as OrderStateValid;
+ expect(validOrderState.orderHash).to.be.equal(orderHash);
+ const orderRelevantState = validOrderState.orderRelevantState;
+ expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal(
+ new BigNumber(16));
+ if (eventCount === 2) {
+ done();
+ }
+ });
+ zeroEx.orderStateWatcher.subscribe(callback);
+ const shouldThrowOnInsufficientBalanceOrAllowance = true;
+ await zeroEx.exchange.fillOrderAsync(
+ signedOrder, fillAmountInBaseUnits, shouldThrowOnInsufficientBalanceOrAllowance, taker,
+ );
+ })().catch(done);
+ });
+ it('should equal approved amount when approved amount is lowest', (done: DoneCallback) => {
+ (async () => {
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerToken.address, takerToken.address, maker, taker, fillableAmount,
+ );
+
+ const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker);
+
+ const changedMakerApprovalAmount = new BigNumber(3);
+ zeroEx.orderStateWatcher.addOrder(signedOrder);
+
+ const callback = reportCallbackErrors(done)((orderState: OrderState) => {
+ const validOrderState = orderState as OrderStateValid;
+ const orderRelevantState = validOrderState.orderRelevantState;
+ expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal(
+ changedMakerApprovalAmount);
+ done();
+ });
+ zeroEx.orderStateWatcher.subscribe(callback);
+ await zeroEx.token.setProxyAllowanceAsync(makerToken.address, maker, changedMakerApprovalAmount);
+ })().catch(done);
+ });
+ it('should equal balance amount when balance amount is lowest', (done: DoneCallback) => {
+ (async () => {
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerToken.address, takerToken.address, maker, taker, fillableAmount,
+ );
+
+ const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker);
+
+ const remainingAmount = new BigNumber(1);
+ const transferAmount = makerBalance.sub(remainingAmount);
+ zeroEx.orderStateWatcher.addOrder(signedOrder);
+
+ const callback = reportCallbackErrors(done)((orderState: OrderState) => {
+ const validOrderState = orderState as OrderStateValid;
+ const orderRelevantState = validOrderState.orderRelevantState;
+ expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal(
+ remainingAmount);
+ done();
+ });
+ zeroEx.orderStateWatcher.subscribe(callback);
+ await zeroEx.token.transferAsync(
+ makerToken.address, maker, ZeroEx.NULL_ADDRESS, transferAmount);
+ })().catch(done);
+ });
+ });
+ it('should emit orderStateInvalid when watched order cancelled', (done: DoneCallback) => {
+ (async () => {
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerToken.address, takerToken.address, maker, taker, fillableAmount,
+ );
+ const orderHash = ZeroEx.getOrderHashHex(signedOrder);
+ zeroEx.orderStateWatcher.addOrder(signedOrder);
+
+ const callback = reportCallbackErrors(done)((orderState: OrderState) => {
+ expect(orderState.isValid).to.be.false();
+ const invalidOrderState = orderState as OrderStateInvalid;
+ expect(invalidOrderState.orderHash).to.be.equal(orderHash);
+ expect(invalidOrderState.error).to.be.equal(ExchangeContractErrs.OrderRemainingFillAmountZero);
+ done();
+ });
+ zeroEx.orderStateWatcher.subscribe(callback);
+
+ const shouldThrowOnInsufficientBalanceOrAllowance = true;
+ await zeroEx.exchange.cancelOrderAsync(signedOrder, fillableAmount);
+ })().catch(done);
+ });
+ it('should emit orderStateValid when watched order partially cancelled', (done: DoneCallback) => {
+ (async () => {
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerToken.address, takerToken.address, maker, taker, fillableAmount,
+ );
+
+ const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker);
+ const takerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, taker);
+
+ const cancelAmountInBaseUnits = new BigNumber(2);
+ const orderHash = ZeroEx.getOrderHashHex(signedOrder);
+ zeroEx.orderStateWatcher.addOrder(signedOrder);
+
+ const callback = reportCallbackErrors(done)((orderState: OrderState) => {
+ expect(orderState.isValid).to.be.true();
+ const validOrderState = orderState as OrderStateValid;
+ expect(validOrderState.orderHash).to.be.equal(orderHash);
+ const orderRelevantState = validOrderState.orderRelevantState;
+ expect(orderRelevantState.canceledTakerTokenAmount).to.be.bignumber.equal(cancelAmountInBaseUnits);
+ done();
+ });
+ zeroEx.orderStateWatcher.subscribe(callback);
+ await zeroEx.exchange.cancelOrderAsync(signedOrder, cancelAmountInBaseUnits);
+ })().catch(done);
+ });
+ });
+});
diff --git a/packages/0x.js/test/order_validation_test.ts b/packages/0x.js/test/order_validation_test.ts
new file mode 100644
index 000000000..4f18742d3
--- /dev/null
+++ b/packages/0x.js/test/order_validation_test.ts
@@ -0,0 +1,327 @@
+import * as chai from 'chai';
+import * as Web3 from 'web3';
+import BigNumber from 'bignumber.js';
+import * as Sinon from 'sinon';
+import {chaiSetup} from './utils/chai_setup';
+import {web3Factory} from './utils/web3_factory';
+import {ZeroEx, SignedOrder, Token, ExchangeContractErrs, ZeroExError} from '../src';
+import {TradeSide, TransferType} from '../src/types';
+import {TokenUtils} from './utils/token_utils';
+import {BlockchainLifecycle} from './utils/blockchain_lifecycle';
+import {FillScenarios} from './utils/fill_scenarios';
+import {OrderValidationUtils} from '../src/utils/order_validation_utils';
+import {ExchangeTransferSimulator} from '../src/utils/exchange_transfer_simulator';
+
+chaiSetup.configure();
+const expect = chai.expect;
+const blockchainLifecycle = new BlockchainLifecycle();
+
+describe('OrderValidation', () => {
+ let web3: Web3;
+ let zeroEx: ZeroEx;
+ 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;
+ let orderValidationUtils: OrderValidationUtils;
+ const fillableAmount = new BigNumber(5);
+ const fillTakerAmount = new BigNumber(5);
+ before(async () => {
+ web3 = web3Factory.create();
+ zeroEx = new ZeroEx(web3.currentProvider);
+ exchangeContractAddress = await zeroEx.exchange.getContractAddressAsync();
+ userAddresses = await zeroEx.getAvailableAddressesAsync();
+ [coinbase, makerAddress, takerAddress, feeRecipient] = userAddresses;
+ tokens = await zeroEx.tokenRegistry.getTokensAsync();
+ tokenUtils = new TokenUtils(tokens);
+ zrxTokenAddress = tokenUtils.getProtocolTokenOrThrow().address;
+ fillScenarios = new FillScenarios(zeroEx, userAddresses, tokens, zrxTokenAddress, exchangeContractAddress);
+ const [makerToken, takerToken] = tokenUtils.getNonProtocolTokens();
+ makerTokenAddress = makerToken.address;
+ takerTokenAddress = takerToken.address;
+ orderValidationUtils = new OrderValidationUtils(zeroEx.token, zeroEx.exchange);
+ });
+ 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 zeroEx.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 zeroEx.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 zeroEx.exchange.cancelOrderAsync(signedOrder, fillableAmount);
+ return expect(zeroEx.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(zeroEx.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(zeroEx.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 = 27 + (28 - signedOrder.ecSignature.v);
+ return expect(zeroEx.exchange.validateFillOrderThrowIfInvalidAsync(
+ signedOrder, fillableAmount, takerAddress,
+ )).to.be.rejectedWith(ZeroExError.InvalidSignature);
+ });
+ it('should throw when the order is fully filled or cancelled', async () => {
+ const signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
+ );
+ await zeroEx.exchange.cancelOrderAsync(signedOrder, fillableAmount);
+ return expect(zeroEx.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(zeroEx.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(zeroEx.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(zeroEx.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 zeroEx.token.transferAsync(takerTokenAddress, coinbase, takerAddress, fillAmountDifference);
+ await zeroEx.token.setProxyAllowanceAsync(takerTokenAddress, takerAddress, tooLargeFillAmount);
+ await zeroEx.token.transferAsync(makerTokenAddress, coinbase, makerAddress, fillAmountDifference);
+ await zeroEx.token.setProxyAllowanceAsync(makerTokenAddress, makerAddress, tooLargeFillAmount);
+
+ return expect(zeroEx.exchange.validateFillOrKillOrderThrowIfInvalidAsync(
+ signedOrder, tooLargeFillAmount, takerAddress,
+ )).to.be.rejectedWith(ExchangeContractErrs.InsufficientRemainingFillAmount);
+ });
+ });
+ describe('validateCancelOrderAndThrowIfInvalidAsync', () => {
+ let signedOrder: SignedOrder;
+ let orderHashHex: string;
+ const cancelAmount = new BigNumber(3);
+ beforeEach(async () => {
+ [coinbase, makerAddress, takerAddress] = userAddresses;
+ const [makerToken, takerToken] = tokenUtils.getNonProtocolTokens();
+ makerTokenAddress = makerToken.address;
+ takerTokenAddress = takerToken.address;
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
+ );
+ orderHashHex = ZeroEx.getOrderHashHex(signedOrder);
+ });
+ it('should throw when cancel amount is zero', async () => {
+ const zeroCancelAmount = new BigNumber(0);
+ return expect(zeroEx.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,
+ );
+ orderHashHex = ZeroEx.getOrderHashHex(expiredSignedOrder);
+ return expect(zeroEx.exchange.validateCancelOrderThrowIfInvalidAsync(expiredSignedOrder, cancelAmount))
+ .to.be.rejectedWith(ExchangeContractErrs.OrderCancelExpired);
+ });
+ it('should throw when order is already cancelled or filled', async () => {
+ await zeroEx.exchange.cancelOrderAsync(signedOrder, fillableAmount);
+ return expect(zeroEx.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(zeroEx.token);
+ 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, ZeroEx.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, ZeroEx.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);
+ });
+ });
+});
diff --git a/packages/0x.js/test/subscription_test.ts b/packages/0x.js/test/subscription_test.ts
new file mode 100644
index 000000000..985fdc1d6
--- /dev/null
+++ b/packages/0x.js/test/subscription_test.ts
@@ -0,0 +1,95 @@
+import 'mocha';
+import * as _ from 'lodash';
+import * as chai from 'chai';
+import * as Sinon from 'sinon';
+import {chaiSetup} from './utils/chai_setup';
+import * as Web3 from 'web3';
+import BigNumber from 'bignumber.js';
+import promisify = require('es6-promisify');
+import {web3Factory} from './utils/web3_factory';
+import {
+ ZeroEx,
+ ZeroExError,
+ Token,
+ ApprovalContractEventArgs,
+ TokenEvents,
+ LogEvent,
+} from '../src';
+import {BlockchainLifecycle} from './utils/blockchain_lifecycle';
+import {TokenUtils} from './utils/token_utils';
+import {DoneCallback, BlockParamLiteral} from '../src/types';
+
+chaiSetup.configure();
+const expect = chai.expect;
+const blockchainLifecycle = new BlockchainLifecycle();
+
+describe('SubscriptionTest', () => {
+ let web3: Web3;
+ let zeroEx: ZeroEx;
+ let userAddresses: string[];
+ let tokens: Token[];
+ let tokenUtils: TokenUtils;
+ let coinbase: string;
+ let addressWithoutFunds: string;
+ before(async () => {
+ web3 = web3Factory.create();
+ zeroEx = new ZeroEx(web3.currentProvider);
+ userAddresses = await zeroEx.getAvailableAddressesAsync();
+ tokens = await zeroEx.tokenRegistry.getTokensAsync();
+ tokenUtils = new TokenUtils(tokens);
+ coinbase = userAddresses[0];
+ addressWithoutFunds = userAddresses[1];
+ });
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ describe('#subscribe', () => {
+ const indexFilterValues = {};
+ const shouldThrowOnInsufficientBalanceOrAllowance = true;
+ let tokenAddress: string;
+ const transferAmount = new BigNumber(42);
+ const allowanceAmount = new BigNumber(42);
+ let stubs: Sinon.SinonStub[] = [];
+ before(() => {
+ const token = tokens[0];
+ tokenAddress = token.address;
+ });
+ afterEach(() => {
+ zeroEx.token.unsubscribeAll();
+ _.each(stubs, s => s.restore());
+ stubs = [];
+ });
+ it('Should receive the Error when an error occurs', (done: DoneCallback) => {
+ (async () => {
+ const callback = (err: Error, logEvent: LogEvent<ApprovalContractEventArgs>) => {
+ expect(err).to.not.be.null();
+ expect(logEvent).to.be.undefined();
+ done();
+ };
+ stubs = [
+ Sinon.stub((zeroEx as any)._web3Wrapper, 'getBlockAsync')
+ .throws("JSON RPC error")
+ ]
+ zeroEx.token.subscribe(
+ tokenAddress, TokenEvents.Approval, indexFilterValues, callback);
+ await zeroEx.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, logEvent: LogEvent<ApprovalContractEventArgs>) => { };
+ zeroEx.token.subscribe(
+ tokenAddress, TokenEvents.Approval, indexFilterValues, callback);
+ stubs = [
+ Sinon.stub((zeroEx as any)._web3Wrapper, 'getBlockAsync')
+ .throws("JSON RPC error")
+ ]
+ zeroEx.token.unsubscribeAll();
+ done();
+ })().catch(done);
+ });
+ })
+ }) \ No newline at end of file
diff --git a/packages/0x.js/test/token_registry_wrapper_test.ts b/packages/0x.js/test/token_registry_wrapper_test.ts
new file mode 100644
index 000000000..6b5dd517e
--- /dev/null
+++ b/packages/0x.js/test/token_registry_wrapper_test.ts
@@ -0,0 +1,123 @@
+import * as _ from 'lodash';
+import 'mocha';
+import * as chai from 'chai';
+import {SchemaValidator, schemas} from '0x-json-schemas';
+import {chaiSetup} from './utils/chai_setup';
+import {web3Factory} from './utils/web3_factory';
+import {ZeroEx, Token} from '../src';
+import {BlockchainLifecycle} from './utils/blockchain_lifecycle';
+
+chaiSetup.configure();
+const expect = chai.expect;
+const blockchainLifecycle = new BlockchainLifecycle();
+
+const TOKEN_REGISTRY_SIZE_AFTER_MIGRATION = 7;
+
+describe('TokenRegistryWrapper', () => {
+ let zeroEx: ZeroEx;
+ 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';
+ before(async () => {
+ const web3 = web3Factory.create();
+ zeroEx = new ZeroEx(web3.currentProvider);
+ tokens = await zeroEx.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 zeroEx.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(ZeroEx.NULL_ADDRESS);
+ });
+ });
+ });
+ describe('#getTokenAddressBySymbol', () => {
+ it('should return correct address for a token in the registry', async () => {
+ const tokenAddress = await zeroEx.tokenRegistry.getTokenAddressBySymbolIfExistsAsync(registeredSymbol);
+ expect(tokenAddress).to.be.equal(tokenAddressBySymbol[registeredSymbol]);
+ });
+ it('should return undefined for a token out of registry', async () => {
+ const tokenAddress = await zeroEx.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 zeroEx.tokenRegistry.getTokenAddressByNameIfExistsAsync(registeredName);
+ expect(tokenAddress).to.be.equal(tokenAddressByName[registeredName]);
+ });
+ it('should return undefined for a token out of registry', async () => {
+ const tokenAddress = await zeroEx.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 zeroEx.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 zeroEx.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 zeroEx.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 zeroEx.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 zeroEx.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 zeroEx.tokenRegistry.getTokenIfExistsAsync(unregisteredTokenAddress);
+ expect(tokenIfExists).to.be.undefined();
+ });
+ });
+});
diff --git a/packages/0x.js/test/token_transfer_proxy_wrapper_test.ts b/packages/0x.js/test/token_transfer_proxy_wrapper_test.ts
new file mode 100644
index 000000000..8faef0b30
--- /dev/null
+++ b/packages/0x.js/test/token_transfer_proxy_wrapper_test.ts
@@ -0,0 +1,31 @@
+import * as chai from 'chai';
+import {chaiSetup} from './utils/chai_setup';
+import {web3Factory} from './utils/web3_factory';
+import {ZeroEx} from '../src';
+import {TokenTransferProxyWrapper} from '../src/contract_wrappers/token_transfer_proxy_wrapper';
+
+chaiSetup.configure();
+const expect = chai.expect;
+
+describe('TokenTransferProxyWrapper', () => {
+ let zeroEx: ZeroEx;
+ before(async () => {
+ const web3 = web3Factory.create();
+ zeroEx = new ZeroEx(web3.currentProvider);
+ });
+ describe('#isAuthorizedAsync', () => {
+ it('should return false if the address is not authorized', async () => {
+ const isAuthorized = await zeroEx.proxy.isAuthorizedAsync(ZeroEx.NULL_ADDRESS);
+ expect(isAuthorized).to.be.false();
+ });
+ });
+ describe('#getAuthorizedAddressesAsync', () => {
+ it('should return the list of authorized addresses', async () => {
+ const authorizedAddresses = await zeroEx.proxy.getAuthorizedAddressesAsync();
+ for (const authorizedAddress of authorizedAddresses) {
+ const isAuthorized = await zeroEx.proxy.isAuthorizedAsync(authorizedAddress);
+ expect(isAuthorized).to.be.true();
+ }
+ });
+ });
+});
diff --git a/packages/0x.js/test/token_wrapper_test.ts b/packages/0x.js/test/token_wrapper_test.ts
new file mode 100644
index 000000000..b30762e8c
--- /dev/null
+++ b/packages/0x.js/test/token_wrapper_test.ts
@@ -0,0 +1,477 @@
+import 'mocha';
+import * as chai from 'chai';
+import {chaiSetup} from './utils/chai_setup';
+import * as Web3 from 'web3';
+import BigNumber from 'bignumber.js';
+import promisify = require('es6-promisify');
+import {web3Factory} from './utils/web3_factory';
+import {
+ ZeroEx,
+ ZeroExError,
+ Token,
+ SubscriptionOpts,
+ TokenEvents,
+ ContractEvent,
+ TransferContractEventArgs,
+ ApprovalContractEventArgs,
+ TokenContractEventArgs,
+ LogWithDecodedArgs,
+ LogEvent,
+ DecodedLogEvent,
+} from '../src';
+import {BlockchainLifecycle} from './utils/blockchain_lifecycle';
+import {TokenUtils} from './utils/token_utils';
+import {DoneCallback, BlockParamLiteral} from '../src/types';
+
+chaiSetup.configure();
+const expect = chai.expect;
+const blockchainLifecycle = new BlockchainLifecycle();
+
+describe('TokenWrapper', () => {
+ let web3: Web3;
+ let zeroEx: ZeroEx;
+ let userAddresses: string[];
+ let tokens: Token[];
+ let tokenUtils: TokenUtils;
+ let coinbase: string;
+ let addressWithoutFunds: string;
+ before(async () => {
+ web3 = web3Factory.create();
+ zeroEx = new ZeroEx(web3.currentProvider);
+ userAddresses = await zeroEx.getAvailableAddressesAsync();
+ tokens = await zeroEx.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 zeroEx.token.getBalanceAsync(token.address, toAddress);
+ expect(preBalance).to.be.bignumber.equal(0);
+ const txHash = await zeroEx.token.transferAsync(token.address, fromAddress, toAddress, transferAmount);
+ const receipt = await zeroEx.awaitTransactionMinedAsync(txHash);
+ const postBalance = await zeroEx.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(zeroEx.token.transferAsync(
+ token.address, fromAddress, toAddress, transferAmount,
+ )).to.be.rejectedWith(ZeroExError.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(zeroEx.token.transferAsync(
+ nonExistentTokenAddress, fromAddress, toAddress, transferAmount,
+ )).to.be.rejectedWith(ZeroExError.ContractDoesNotExist);
+ });
+ });
+ 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 zeroEx.token.getBalanceAsync(token.address, fromAddress);
+ expect(fromAddressBalance).to.be.bignumber.greaterThan(transferAmount);
+
+ const fromAddressAllowance = await zeroEx.token.getAllowanceAsync(token.address, fromAddress,
+ toAddress);
+ expect(fromAddressAllowance).to.be.bignumber.equal(0);
+
+ return expect(zeroEx.token.transferFromAsync(
+ token.address, fromAddress, toAddress, senderAddress, transferAmount,
+ )).to.be.rejectedWith(ZeroExError.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 zeroEx.token.setAllowanceAsync(token.address, fromAddress, toAddress, transferAmount);
+
+ return expect(zeroEx.token.transferFromAsync(
+ token.address, fromAddress, toAddress, senderAddress, transferAmount,
+ )).to.be.rejectedWith(ZeroExError.InsufficientAllowanceForTransfer);
+ });
+ it('should fail to transfer tokens if fromAddress has insufficient balance', async () => {
+ const fromAddress = addressWithoutFunds;
+ const transferAmount = new BigNumber(42);
+
+ const fromAddressBalance = await zeroEx.token.getBalanceAsync(token.address, fromAddress);
+ expect(fromAddressBalance).to.be.bignumber.equal(0);
+
+ await zeroEx.token.setAllowanceAsync(token.address, fromAddress, senderAddress, transferAmount);
+ const fromAddressAllowance = await zeroEx.token.getAllowanceAsync(token.address, fromAddress,
+ senderAddress);
+ expect(fromAddressAllowance).to.be.bignumber.equal(transferAmount);
+
+ return expect(zeroEx.token.transferFromAsync(
+ token.address, fromAddress, toAddress, senderAddress, transferAmount,
+ )).to.be.rejectedWith(ZeroExError.InsufficientBalanceForTransfer);
+ });
+ it('should successfully transfer tokens', async () => {
+ const fromAddress = coinbase;
+
+ const preBalance = await zeroEx.token.getBalanceAsync(token.address, toAddress);
+ expect(preBalance).to.be.bignumber.equal(0);
+
+ const transferAmount = new BigNumber(42);
+ await zeroEx.token.setAllowanceAsync(token.address, fromAddress, senderAddress, transferAmount);
+
+ await zeroEx.token.transferFromAsync(token.address, fromAddress, toAddress, senderAddress,
+ transferAmount);
+ const postBalance = await zeroEx.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(zeroEx.token.transferFromAsync(
+ nonExistentTokenAddress, fromAddress, toAddress, senderAddress, new BigNumber(42),
+ )).to.be.rejectedWith(ZeroExError.ContractDoesNotExist);
+ });
+ });
+ describe('#getBalanceAsync', () => {
+ describe('With web3 provider with accounts', () => {
+ it('should return the balance for an existing ERC20 token', async () => {
+ const token = tokens[0];
+ const ownerAddress = coinbase;
+ const balance = await zeroEx.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(zeroEx.token.getBalanceAsync(nonExistentTokenAddress, ownerAddress))
+ .to.be.rejectedWith(ZeroExError.ContractDoesNotExist);
+ });
+ it('should return a balance of 0 for a non-existent owner address', async () => {
+ const token = tokens[0];
+ const nonExistentOwner = '0x198c6ad858f213fb31b6fe809e25040e6b964593';
+ const balance = await zeroEx.token.getBalanceAsync(token.address, nonExistentOwner);
+ const expectedBalance = new BigNumber(0);
+ return expect(balance).to.be.bignumber.equal(expectedBalance);
+ });
+ });
+ describe('With web3 provider without accounts', () => {
+ let zeroExWithoutAccounts: ZeroEx;
+ before(async () => {
+ const hasAddresses = false;
+ const web3WithoutAccounts = web3Factory.create(hasAddresses);
+ zeroExWithoutAccounts = new ZeroEx(web3WithoutAccounts.currentProvider);
+ });
+ it('should return balance even when called with Web3 provider instance without addresses', async () => {
+ const token = tokens[0];
+ const ownerAddress = coinbase;
+ const balance = await zeroExWithoutAccounts.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 zeroEx.token.getAllowanceAsync(token.address, ownerAddress,
+ spenderAddress);
+ const expectedAllowanceBeforeAllowanceSet = new BigNumber(0);
+ expect(allowanceBeforeSet).to.be.bignumber.equal(expectedAllowanceBeforeAllowanceSet);
+
+ const amountInBaseUnits = new BigNumber(50);
+ await zeroEx.token.setAllowanceAsync(token.address, ownerAddress, spenderAddress, amountInBaseUnits);
+
+ const allowanceAfterSet = await zeroEx.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 zeroEx.token.setUnlimitedAllowanceAsync(token.address, ownerAddress, spenderAddress);
+ const allowance = await zeroEx.token.getAllowanceAsync(token.address, ownerAddress, spenderAddress);
+ return expect(allowance).to.be.bignumber.equal(zeroEx.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 zeroEx.token.setAllowanceAsync(zrx.address, coinbase, userWithNormalAllowance, transferAmount);
+ await zeroEx.token.setUnlimitedAllowanceAsync(zrx.address, coinbase, userWithUnlimitedAllowance);
+
+ const initBalanceWithNormalAllowance = await promisify(web3.eth.getBalance)(userWithNormalAllowance);
+ const initBalanceWithUnlimitedAllowance = await promisify(web3.eth.getBalance)(userWithUnlimitedAllowance);
+
+ await zeroEx.token.transferFromAsync(
+ zrx.address, coinbase, userWithNormalAllowance, userWithNormalAllowance, transferAmount,
+ );
+ await zeroEx.token.transferFromAsync(
+ zrx.address, coinbase, userWithUnlimitedAllowance, userWithUnlimitedAllowance, transferAmount,
+ );
+
+ const finalBalanceWithNormalAllowance = await promisify(web3.eth.getBalance)(userWithNormalAllowance);
+ const finalBalanceWithUnlimitedAllowance = await promisify(web3.eth.getBalance)(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 web3 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 zeroEx.token.setAllowanceAsync(token.address, ownerAddress, spenderAddress, amountInBaseUnits);
+
+ const allowance = await zeroEx.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 zeroEx.token.getAllowanceAsync(token.address, ownerAddress, spenderAddress);
+ const expectedAllowance = new BigNumber(0);
+ return expect(allowance).to.be.bignumber.equal(expectedAllowance);
+ });
+ });
+ describe('With web3 provider without accounts', () => {
+ let zeroExWithoutAccounts: ZeroEx;
+ before(async () => {
+ const hasAddresses = false;
+ const web3WithoutAccounts = web3Factory.create(hasAddresses);
+ zeroExWithoutAccounts = new ZeroEx(web3WithoutAccounts.currentProvider);
+ });
+ it('should get the proxy allowance', async () => {
+ const token = tokens[0];
+ const ownerAddress = coinbase;
+ const spenderAddress = addressWithoutFunds;
+
+ const amountInBaseUnits = new BigNumber(50);
+ await zeroEx.token.setAllowanceAsync(token.address, ownerAddress, spenderAddress, amountInBaseUnits);
+
+ const allowance = await zeroExWithoutAccounts.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 zeroEx.token.setProxyAllowanceAsync(token.address, ownerAddress, amountInBaseUnits);
+
+ const allowance = await zeroEx.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 zeroEx.token.getProxyAllowanceAsync(token.address, ownerAddress);
+ const expectedAllowanceBeforeAllowanceSet = new BigNumber(0);
+ expect(allowanceBeforeSet).to.be.bignumber.equal(expectedAllowanceBeforeAllowanceSet);
+
+ const amountInBaseUnits = new BigNumber(50);
+ await zeroEx.token.setProxyAllowanceAsync(token.address, ownerAddress, amountInBaseUnits);
+
+ const allowanceAfterSet = await zeroEx.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 zeroEx.token.setUnlimitedProxyAllowanceAsync(token.address, ownerAddress);
+ const allowance = await zeroEx.token.getProxyAllowanceAsync(token.address, ownerAddress);
+ return expect(allowance).to.be.bignumber.equal(zeroEx.token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS);
+ });
+ });
+ describe('#subscribe', () => {
+ const indexFilterValues = {};
+ const shouldThrowOnInsufficientBalanceOrAllowance = true;
+ let tokenAddress: string;
+ const transferAmount = new BigNumber(42);
+ const allowanceAmount = new BigNumber(42);
+ before(() => {
+ const token = tokens[0];
+ tokenAddress = token.address;
+ });
+ afterEach(() => {
+ zeroEx.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 = (err: Error, logEvent: DecodedLogEvent<TransferContractEventArgs>) => {
+ expect(logEvent).to.not.be.undefined();
+ const args = logEvent.args;
+ expect(args._from).to.be.equal(coinbase);
+ expect(args._to).to.be.equal(addressWithoutFunds);
+ expect(args._value).to.be.bignumber.equal(transferAmount);
+ done();
+ };
+ zeroEx.token.subscribe(
+ tokenAddress, TokenEvents.Transfer, indexFilterValues, callback);
+ await zeroEx.token.transferAsync(tokenAddress, coinbase, addressWithoutFunds, transferAmount);
+ })().catch(done);
+ });
+ it('Should receive the Approval event when allowance is being set', (done: DoneCallback) => {
+ (async () => {
+ const callback = (err: Error, logEvent: DecodedLogEvent<ApprovalContractEventArgs>) => {
+ expect(logEvent).to.not.be.undefined();
+ const args = logEvent.args;
+ expect(args._owner).to.be.equal(coinbase);
+ expect(args._spender).to.be.equal(addressWithoutFunds);
+ expect(args._value).to.be.bignumber.equal(allowanceAmount);
+ done();
+ };
+ zeroEx.token.subscribe(
+ tokenAddress, TokenEvents.Approval, indexFilterValues, callback);
+ await zeroEx.token.setAllowanceAsync(tokenAddress, coinbase, addressWithoutFunds, allowanceAmount);
+ })().catch(done);
+ });
+ it('Outstanding subscriptions are cancelled when zeroEx.setProviderAsync called', (done: DoneCallback) => {
+ (async () => {
+ const callbackNeverToBeCalled = (err: Error, logEvent: DecodedLogEvent<ApprovalContractEventArgs>) => {
+ done(new Error('Expected this subscription to have been cancelled'));
+ };
+ zeroEx.token.subscribe(
+ tokenAddress, TokenEvents.Transfer, indexFilterValues, callbackNeverToBeCalled,
+ );
+ const callbackToBeCalled = (err: Error, logEvent: DecodedLogEvent<ApprovalContractEventArgs>) => {
+ done();
+ };
+ const newProvider = web3Factory.getRpcProvider();
+ await zeroEx.setProviderAsync(newProvider);
+ zeroEx.token.subscribe(
+ tokenAddress, TokenEvents.Transfer, indexFilterValues, callbackToBeCalled,
+ );
+ await zeroEx.token.transferAsync(tokenAddress, coinbase, addressWithoutFunds, transferAmount);
+ })().catch(done);
+ });
+ it('Should cancel subscription when unsubscribe called', (done: DoneCallback) => {
+ (async () => {
+ const callbackNeverToBeCalled = (err: Error, logEvent: DecodedLogEvent<ApprovalContractEventArgs>) => {
+ done(new Error('Expected this subscription to have been cancelled'));
+ };
+ const subscriptionToken = zeroEx.token.subscribe(
+ tokenAddress, TokenEvents.Transfer, indexFilterValues, callbackNeverToBeCalled);
+ zeroEx.token.unsubscribe(subscriptionToken);
+ await zeroEx.token.transferAsync(tokenAddress, coinbase, addressWithoutFunds, transferAmount);
+ done();
+ })().catch(done);
+ });
+ });
+ describe('#getLogsAsync', () => {
+ let tokenAddress: string;
+ let tokenTransferProxyAddress: string;
+ const subscriptionOpts: SubscriptionOpts = {
+ fromBlock: BlockParamLiteral.Earliest,
+ toBlock: BlockParamLiteral.Latest,
+ };
+ let txHash: string;
+ before(async () => {
+ const token = tokens[0];
+ tokenAddress = token.address;
+ tokenTransferProxyAddress = await zeroEx.proxy.getContractAddressAsync();
+ });
+ it('should get logs with decoded args emitted by Approval', async () => {
+ txHash = await zeroEx.token.setUnlimitedProxyAllowanceAsync(tokenAddress, coinbase);
+ await zeroEx.awaitTransactionMinedAsync(txHash);
+ const eventName = TokenEvents.Approval;
+ const indexFilterValues = {};
+ const logs = await zeroEx.token.getLogsAsync<ApprovalContractEventArgs>(
+ tokenAddress, eventName, subscriptionOpts, 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(zeroEx.token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS);
+ });
+ it('should only get the logs with the correct event name', async () => {
+ txHash = await zeroEx.token.setUnlimitedProxyAllowanceAsync(tokenAddress, coinbase);
+ await zeroEx.awaitTransactionMinedAsync(txHash);
+ const differentEventName = TokenEvents.Transfer;
+ const indexFilterValues = {};
+ const logs = await zeroEx.token.getLogsAsync(
+ tokenAddress, differentEventName, subscriptionOpts, indexFilterValues,
+ );
+ expect(logs).to.have.length(0);
+ });
+ it('should only get the logs with the correct indexed fields', async () => {
+ txHash = await zeroEx.token.setUnlimitedProxyAllowanceAsync(tokenAddress, coinbase);
+ await zeroEx.awaitTransactionMinedAsync(txHash);
+ txHash = await zeroEx.token.setUnlimitedProxyAllowanceAsync(tokenAddress, addressWithoutFunds);
+ await zeroEx.awaitTransactionMinedAsync(txHash);
+ const eventName = TokenEvents.Approval;
+ const indexFilterValues = {
+ _owner: coinbase,
+ };
+ const logs = await zeroEx.token.getLogsAsync<ApprovalContractEventArgs>(
+ tokenAddress, eventName, subscriptionOpts, indexFilterValues,
+ );
+ expect(logs).to.have.length(1);
+ const args = logs[0].args;
+ expect(args._owner).to.be.equal(coinbase);
+ });
+ });
+});
diff --git a/packages/0x.js/test/utils/blockchain_lifecycle.ts b/packages/0x.js/test/utils/blockchain_lifecycle.ts
new file mode 100644
index 000000000..9a44ccd6f
--- /dev/null
+++ b/packages/0x.js/test/utils/blockchain_lifecycle.ts
@@ -0,0 +1,26 @@
+import {RPC} from './rpc';
+
+export class BlockchainLifecycle {
+ private rpc: RPC;
+ private snapshotIdsStack: number[];
+ constructor() {
+ this.rpc = new RPC();
+ this.snapshotIdsStack = [];
+ }
+ // TODO: In order to run these tests on an actual node, we should check if we are running against
+ // TestRPC, if so, use snapshots, otherwise re-deploy contracts before every test
+ public async startAsync(): Promise<void> {
+ const snapshotId = await this.rpc.takeSnapshotAsync();
+ this.snapshotIdsStack.push(snapshotId);
+ }
+ public async revertAsync(): Promise<void> {
+ const snapshotId = this.snapshotIdsStack.pop() as number;
+ const didRevert = await this.rpc.revertSnapshotAsync(snapshotId);
+ if (!didRevert) {
+ throw new Error(`Snapshot with id #${snapshotId} failed to revert`);
+ }
+ }
+ public async mineABlock(): Promise<void> {
+ await this.rpc.mineBlockAsync();
+ }
+}
diff --git a/packages/0x.js/test/utils/chai_setup.ts b/packages/0x.js/test/utils/chai_setup.ts
new file mode 100644
index 000000000..c18988106
--- /dev/null
+++ b/packages/0x.js/test/utils/chai_setup.ts
@@ -0,0 +1,13 @@
+import * as chai from 'chai';
+import * as dirtyChai from 'dirty-chai';
+import ChaiBigNumber = require('chai-bignumber');
+import chaiAsPromised = require('chai-as-promised');
+
+export const chaiSetup = {
+ configure() {
+ chai.config.includeStack = true;
+ chai.use(ChaiBigNumber());
+ chai.use(dirtyChai);
+ chai.use(chaiAsPromised);
+ },
+};
diff --git a/packages/0x.js/test/utils/constants.ts b/packages/0x.js/test/utils/constants.ts
new file mode 100644
index 000000000..c7d3aebca
--- /dev/null
+++ b/packages/0x.js/test/utils/constants.ts
@@ -0,0 +1,8 @@
+export const constants = {
+ NULL_ADDRESS: '0x0000000000000000000000000000000000000000',
+ RPC_HOST: 'localhost',
+ RPC_PORT: 8545,
+ TESTRPC_NETWORK_ID: 50,
+ KOVAN_RPC_URL: 'https://kovan.infura.io',
+ ROPSTEN_RPC_URL: 'https://ropsten.infura.io',
+};
diff --git a/packages/0x.js/test/utils/fill_scenarios.ts b/packages/0x.js/test/utils/fill_scenarios.ts
new file mode 100644
index 000000000..a0632b12c
--- /dev/null
+++ b/packages/0x.js/test/utils/fill_scenarios.ts
@@ -0,0 +1,114 @@
+import BigNumber from 'bignumber.js';
+import {ZeroEx, Token, SignedOrder} from '../../src';
+import {orderFactory} from '../utils/order_factory';
+import {constants} from './constants';
+
+export class FillScenarios {
+ private zeroEx: ZeroEx;
+ private userAddresses: string[];
+ private tokens: Token[];
+ private coinbase: string;
+ private zrxTokenAddress: string;
+ private exchangeContractAddress: string;
+ constructor(zeroEx: ZeroEx, userAddresses: string[],
+ tokens: Token[], zrxTokenAddress: string, exchangeContractAddress: string) {
+ this.zeroEx = zeroEx;
+ this.userAddresses = userAddresses;
+ this.tokens = tokens;
+ this.coinbase = userAddresses[0];
+ this.zrxTokenAddress = zrxTokenAddress;
+ this.exchangeContractAddress = exchangeContractAddress;
+ }
+ public async createFillableSignedOrderAsync(makerTokenAddress: string, takerTokenAddress: string,
+ makerAddress: string, takerAddress: string,
+ fillableAmount: BigNumber,
+ expirationUnixTimestampSec?: BigNumber):
+ Promise<SignedOrder> {
+ return this.createAsymmetricFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress,
+ fillableAmount, fillableAmount, expirationUnixTimestampSec,
+ );
+ }
+ public async createFillableSignedOrderWithFeesAsync(
+ makerTokenAddress: string, takerTokenAddress: string,
+ makerFee: BigNumber, takerFee: BigNumber,
+ makerAddress: string, takerAddress: string,
+ fillableAmount: BigNumber,
+ feeRecepient: string, expirationUnixTimestampSec?: BigNumber,
+ ): Promise<SignedOrder> {
+ return this.createAsymmetricFillableSignedOrderWithFeesAsync(
+ makerTokenAddress, takerTokenAddress, makerFee, takerFee, makerAddress, takerAddress,
+ fillableAmount, fillableAmount, feeRecepient, expirationUnixTimestampSec,
+ );
+ }
+ public async createAsymmetricFillableSignedOrderAsync(
+ makerTokenAddress: string, takerTokenAddress: string, makerAddress: string, takerAddress: string,
+ makerFillableAmount: BigNumber, takerFillableAmount: BigNumber,
+ expirationUnixTimestampSec?: BigNumber): Promise<SignedOrder> {
+ const makerFee = new BigNumber(0);
+ const takerFee = new BigNumber(0);
+ const feeRecepient = constants.NULL_ADDRESS;
+ return this.createAsymmetricFillableSignedOrderWithFeesAsync(
+ makerTokenAddress, takerTokenAddress, makerFee, takerFee, makerAddress, takerAddress,
+ makerFillableAmount, takerFillableAmount, feeRecepient, expirationUnixTimestampSec,
+ );
+ }
+ public async createPartiallyFilledSignedOrderAsync(makerTokenAddress: string, takerTokenAddress: string,
+ takerAddress: string, fillableAmount: BigNumber,
+ partialFillAmount: BigNumber) {
+ const [makerAddress] = this.userAddresses;
+ const signedOrder = await this.createAsymmetricFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress,
+ fillableAmount, fillableAmount,
+ );
+ const shouldThrowOnInsufficientBalanceOrAllowance = false;
+ await this.zeroEx.exchange.fillOrderAsync(
+ signedOrder, partialFillAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress,
+ );
+ return signedOrder;
+ }
+ private async createAsymmetricFillableSignedOrderWithFeesAsync(
+ makerTokenAddress: string, takerTokenAddress: string,
+ makerFee: BigNumber, takerFee: BigNumber,
+ makerAddress: string, takerAddress: string,
+ makerFillableAmount: BigNumber, takerFillableAmount: BigNumber,
+ feeRecepient: string, expirationUnixTimestampSec?: BigNumber): Promise<SignedOrder> {
+
+ await Promise.all([
+ this.increaseBalanceAndAllowanceAsync(makerTokenAddress, makerAddress, makerFillableAmount),
+ this.increaseBalanceAndAllowanceAsync(takerTokenAddress, takerAddress, takerFillableAmount),
+ ]);
+ await Promise.all([
+ this.increaseBalanceAndAllowanceAsync(this.zrxTokenAddress, makerAddress, makerFee),
+ this.increaseBalanceAndAllowanceAsync(this.zrxTokenAddress, takerAddress, takerFee),
+ ]);
+
+ const signedOrder = await orderFactory.createSignedOrderAsync(this.zeroEx,
+ makerAddress, takerAddress, makerFee, takerFee,
+ makerFillableAmount, makerTokenAddress, takerFillableAmount, takerTokenAddress,
+ this.exchangeContractAddress, feeRecepient, expirationUnixTimestampSec);
+ return signedOrder;
+ }
+ private async increaseBalanceAndAllowanceAsync(
+ tokenAddress: string, address: string, amount: BigNumber): Promise<void> {
+ if (amount.isZero() || address === ZeroEx.NULL_ADDRESS) {
+ return; // noop
+ }
+ await Promise.all([
+ this.increaseBalanceAsync(tokenAddress, address, amount),
+ this.increaseAllowanceAsync(tokenAddress, address, amount),
+ ]);
+ }
+ private async increaseBalanceAsync(
+ tokenAddress: string, address: string, amount: BigNumber): Promise<void> {
+ await this.zeroEx.token.transferAsync(tokenAddress, this.coinbase, address, amount);
+ }
+ private async increaseAllowanceAsync(
+ tokenAddress: string, address: string, amount: BigNumber): Promise<void> {
+ const oldMakerAllowance = await this.zeroEx.token.getProxyAllowanceAsync(tokenAddress, address);
+ const newMakerAllowance = oldMakerAllowance.plus(amount);
+ await this.zeroEx.token.setProxyAllowanceAsync(
+ tokenAddress, address, newMakerAllowance,
+ );
+ }
+}
diff --git a/packages/0x.js/test/utils/order_factory.ts b/packages/0x.js/test/utils/order_factory.ts
new file mode 100644
index 000000000..6086e09f7
--- /dev/null
+++ b/packages/0x.js/test/utils/order_factory.ts
@@ -0,0 +1,42 @@
+import * as _ from 'lodash';
+import BigNumber from 'bignumber.js';
+import {ZeroEx, SignedOrder} from '../../src';
+
+export const orderFactory = {
+ async createSignedOrderAsync(
+ zeroEx: ZeroEx,
+ maker: string,
+ taker: string,
+ makerFee: BigNumber,
+ takerFee: BigNumber,
+ makerTokenAmount: BigNumber,
+ makerTokenAddress: string,
+ takerTokenAmount: BigNumber,
+ takerTokenAddress: string,
+ exchangeContractAddress: string,
+ feeRecipient: string,
+ expirationUnixTimestampSec?: BigNumber): Promise<SignedOrder> {
+ const defaultExpirationUnixTimestampSec = new BigNumber(2524604400); // Close to infinite
+ expirationUnixTimestampSec = _.isUndefined(expirationUnixTimestampSec) ?
+ defaultExpirationUnixTimestampSec :
+ expirationUnixTimestampSec;
+ const order = {
+ maker,
+ taker,
+ makerFee,
+ takerFee,
+ makerTokenAmount,
+ takerTokenAmount,
+ makerTokenAddress,
+ takerTokenAddress,
+ salt: ZeroEx.generatePseudoRandomSalt(),
+ exchangeContractAddress,
+ feeRecipient,
+ expirationUnixTimestampSec,
+ };
+ const orderHash = ZeroEx.getOrderHashHex(order);
+ const ecSignature = await zeroEx.signOrderHashAsync(orderHash, maker);
+ const signedOrder: SignedOrder = _.assign(order, {ecSignature});
+ return signedOrder;
+ },
+};
diff --git a/packages/0x.js/test/utils/report_callback_errors.ts b/packages/0x.js/test/utils/report_callback_errors.ts
new file mode 100644
index 000000000..d471b2af2
--- /dev/null
+++ b/packages/0x.js/test/utils/report_callback_errors.ts
@@ -0,0 +1,14 @@
+import { DoneCallback } from '../../src/types';
+
+export const reportCallbackErrors = (done: DoneCallback) => {
+ return (f: (...args: any[]) => void) => {
+ const wrapped = (...args: any[]) => {
+ try {
+ f(...args);
+ } catch (err) {
+ done(err);
+ }
+ };
+ return wrapped;
+ };
+};
diff --git a/packages/0x.js/test/utils/rpc.ts b/packages/0x.js/test/utils/rpc.ts
new file mode 100644
index 000000000..299e72e79
--- /dev/null
+++ b/packages/0x.js/test/utils/rpc.ts
@@ -0,0 +1,57 @@
+import * as ethUtil from 'ethereumjs-util';
+import * as request from 'request-promise-native';
+import {constants} from './constants';
+
+export class RPC {
+ private host: string;
+ private port: number;
+ private id: number;
+ constructor() {
+ this.host = constants.RPC_HOST;
+ this.port = constants.RPC_PORT;
+ this.id = 0;
+ }
+ public async takeSnapshotAsync(): Promise<number> {
+ const method = 'evm_snapshot';
+ const params: any[] = [];
+ const payload = this.toPayload(method, params);
+ const snapshotIdHex = await this.sendAsync(payload);
+ const snapshotId = ethUtil.bufferToInt(ethUtil.toBuffer(snapshotIdHex));
+ return snapshotId;
+ }
+ public async revertSnapshotAsync(snapshotId: number): Promise<boolean> {
+ const method = 'evm_revert';
+ const params = [snapshotId];
+ const payload = this.toPayload(method, params);
+ const didRevert = await this.sendAsync(payload);
+ return didRevert;
+ }
+ public async mineBlockAsync(): Promise<void> {
+ const method = 'evm_mine';
+ const params: any[] = [];
+ const payload = this.toPayload(method, params);
+ await this.sendAsync(payload);
+ }
+ private toPayload(method: string, params: any[] = []): string {
+ const payload = JSON.stringify({
+ id: this.id,
+ method,
+ params,
+ });
+ this.id += 1;
+ return payload;
+ }
+ private async sendAsync(payload: string): Promise<any> {
+ const opts = {
+ method: 'POST',
+ uri: `http://${this.host}:${this.port}`,
+ body: payload,
+ headers: {
+ 'content-type': 'application/json',
+ },
+ };
+ const bodyString = await request(opts);
+ const body = JSON.parse(bodyString);
+ return body.result;
+ }
+}
diff --git a/packages/0x.js/test/utils/token_utils.ts b/packages/0x.js/test/utils/token_utils.ts
new file mode 100644
index 000000000..51cb9411c
--- /dev/null
+++ b/packages/0x.js/test/utils/token_utils.ts
@@ -0,0 +1,24 @@
+import * as _ from 'lodash';
+import {Token, InternalZeroExError} from '../../src/types';
+
+const PROTOCOL_TOKEN_SYMBOL = 'ZRX';
+
+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(InternalZeroExError.ZrxNotInTokenRegistry);
+ }
+ return zrxToken;
+ }
+ public getNonProtocolTokens(): Token[] {
+ const nonProtocolTokens = _.filter(this.tokens, token => {
+ return token.symbol !== PROTOCOL_TOKEN_SYMBOL;
+ });
+ return nonProtocolTokens;
+ }
+}
diff --git a/packages/0x.js/test/utils/web3_factory.ts b/packages/0x.js/test/utils/web3_factory.ts
new file mode 100644
index 000000000..b20070c74
--- /dev/null
+++ b/packages/0x.js/test/utils/web3_factory.ts
@@ -0,0 +1,31 @@
+// HACK: web3 injects XMLHttpRequest into the global scope and ProviderEngine checks XMLHttpRequest
+// to know whether it is running in a browser or node environment. We need it to be undefined since
+// we are not running in a browser env.
+// Filed issue: https://github.com/ethereum/web3.js/issues/844
+(global as any).XMLHttpRequest = undefined;
+import ProviderEngine = require('web3-provider-engine');
+import RpcSubprovider = require('web3-provider-engine/subproviders/rpc');
+import * as Web3 from 'web3';
+import {constants} from './constants';
+import {EmptyWalletSubProvider} from '../../src/subproviders/empty_wallet_subprovider';
+
+export const web3Factory = {
+ create(hasAddresses: boolean = true): Web3 {
+ const provider = this.getRpcProvider(hasAddresses);
+ const web3 = new Web3();
+ web3.setProvider(provider);
+ return web3;
+ },
+ getRpcProvider(hasAddresses: boolean = true): Web3.Provider {
+ const provider = new ProviderEngine();
+ const rpcUrl = `http://${constants.RPC_HOST}:${constants.RPC_PORT}`;
+ if (!hasAddresses) {
+ provider.addProvider(new EmptyWalletSubProvider());
+ }
+ provider.addProvider(new RpcSubprovider({
+ rpcUrl,
+ }));
+ provider.start();
+ return provider;
+ },
+};
diff --git a/packages/0x.js/test/web3_wrapper_test.ts b/packages/0x.js/test/web3_wrapper_test.ts
new file mode 100644
index 000000000..d1c2e8e89
--- /dev/null
+++ b/packages/0x.js/test/web3_wrapper_test.ts
@@ -0,0 +1,29 @@
+import * as chai from 'chai';
+import {web3Factory} from './utils/web3_factory';
+import {ZeroEx} from '../src/';
+import {Web3Wrapper} from '../src/web3_wrapper';
+import {constants} from './utils/constants';
+
+chai.config.includeStack = true;
+const expect = chai.expect;
+
+describe('Web3Wrapper', () => {
+ const web3Provider = web3Factory.create().currentProvider;
+ describe('#getNetworkIdIfExistsAsync', () => {
+ it('caches network id requests', async () => {
+ const web3Wrapper = (new ZeroEx(web3Provider) as any)._web3Wrapper as Web3Wrapper;
+ expect((web3Wrapper as any).networkIdIfExists).to.be.undefined();
+ const networkIdIfExists = await web3Wrapper.getNetworkIdIfExistsAsync();
+ expect((web3Wrapper as any).networkIdIfExists).to.be.equal(constants.TESTRPC_NETWORK_ID);
+ });
+ it('invalidates network id cache on setProvider call', async () => {
+ const web3Wrapper = (new ZeroEx(web3Provider) as any)._web3Wrapper as Web3Wrapper;
+ expect((web3Wrapper as any).networkIdIfExists).to.be.undefined();
+ const networkIdIfExists = await web3Wrapper.getNetworkIdIfExistsAsync();
+ expect((web3Wrapper as any).networkIdIfExists).to.be.equal(constants.TESTRPC_NETWORK_ID);
+ const newProvider = web3Factory.create().currentProvider;
+ web3Wrapper.setProvider(newProvider);
+ expect((web3Wrapper as any).networkIdIfExists).to.be.undefined();
+ });
+ });
+});