diff options
Diffstat (limited to 'packages/0x.js/test')
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..f69ae0b13 --- /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, + 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('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: DecodedLogEvent<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: DecodedLogEvent<ApprovalContractEventArgs>) => _.noop; + 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); + }); + }); +}); 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(); + }); + }); +}); |