import { BlockchainLifecycle, callbackErrorReporter } from '@0x/dev-utils'; import { FillScenarios } from '@0x/fill-scenarios'; import { assetDataUtils, orderHashUtils } from '@0x/order-utils'; import { DoneCallback, RevertReason, SignedOrder } from '@0x/types'; import { BigNumber } from '@0x/utils'; import * as chai from 'chai'; import { BlockParamLiteral } from 'ethereum-types'; import 'mocha'; import { ContractWrappers, ExchangeCancelEventArgs, ExchangeEvents, ExchangeFillEventArgs, OrderStatus } from '../src'; import { DecodedLogEvent } from '../src/types'; import { chaiSetup } from './utils/chai_setup'; import { constants } from './utils/constants'; import { migrateOnceAsync } from './utils/migrate'; import { tokenUtils } from './utils/token_utils'; import { provider, web3Wrapper } from './utils/web3_wrapper'; chaiSetup.configure(); const expect = chai.expect; const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); describe('ExchangeWrapper', () => { let contractWrappers: ContractWrappers; let userAddresses: string[]; let zrxTokenAddress: string; let fillScenarios: FillScenarios; let exchangeContractAddress: string; let makerTokenAddress: string; let takerTokenAddress: string; let makerAddress: string; let anotherMakerAddress: string; let takerAddress: string; let makerAssetData: string; let takerAssetData: string; let txHash: string; const fillableAmount = new BigNumber(5); const takerTokenFillAmount = new BigNumber(5); let signedOrder: SignedOrder; let anotherSignedOrder: SignedOrder; before(async () => { const contractAddresses = await migrateOnceAsync(); await blockchainLifecycle.startAsync(); const config = { networkId: constants.TESTRPC_NETWORK_ID, contractAddresses, blockPollingIntervalMs: 10, }; contractWrappers = new ContractWrappers(provider, config); exchangeContractAddress = contractWrappers.exchange.address; userAddresses = await web3Wrapper.getAvailableAddressesAsync(); zrxTokenAddress = contractWrappers.exchange.zrxTokenAddress; fillScenarios = new FillScenarios( provider, userAddresses, zrxTokenAddress, exchangeContractAddress, contractWrappers.erc20Proxy.address, contractWrappers.erc721Proxy.address, ); [, makerAddress, takerAddress, , anotherMakerAddress] = userAddresses; [makerTokenAddress, takerTokenAddress] = tokenUtils.getDummyERC20TokenAddresses(); [makerAssetData, takerAssetData] = [ assetDataUtils.encodeERC20AssetData(makerTokenAddress), assetDataUtils.encodeERC20AssetData(takerTokenAddress), ]; signedOrder = await fillScenarios.createFillableSignedOrderAsync( makerAssetData, takerAssetData, makerAddress, takerAddress, fillableAmount, ); anotherSignedOrder = await fillScenarios.createFillableSignedOrderAsync( makerAssetData, takerAssetData, makerAddress, takerAddress, fillableAmount, ); }); after(async () => { await blockchainLifecycle.revertAsync(); }); beforeEach(async () => { await blockchainLifecycle.startAsync(); }); afterEach(async () => { await blockchainLifecycle.revertAsync(); }); describe('fill order(s)', () => { describe('#fillOrderAsync', () => { it('should fill a valid order', async () => { txHash = await contractWrappers.exchange.fillOrderAsync( signedOrder, takerTokenFillAmount, takerAddress, ); await web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS); }); }); describe('#fillOrderNoThrowAsync', () => { it('should fill a valid order', async () => { txHash = await contractWrappers.exchange.fillOrderNoThrowAsync( signedOrder, takerTokenFillAmount, takerAddress, ); await web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS); const orderInfo = await contractWrappers.exchange.getOrderInfoAsync(signedOrder); expect(orderInfo.orderStatus).to.be.equal(OrderStatus.FULLY_FILLED); }); }); describe('#fillOrKillOrderAsync', () => { it('should fill or kill a valid order', async () => { txHash = await contractWrappers.exchange.fillOrKillOrderAsync( signedOrder, takerTokenFillAmount, takerAddress, ); await web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS); }); }); describe('#batchFillOrdersAsync', () => { it('should fill a batch of valid orders', async () => { const signedOrders = [signedOrder, anotherSignedOrder]; const takerAssetFillAmounts = [takerTokenFillAmount, takerTokenFillAmount]; txHash = await contractWrappers.exchange.batchFillOrdersAsync( signedOrders, takerAssetFillAmounts, takerAddress, ); await web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS); }); }); describe('#marketBuyOrdersAsync', () => { it('should maker buy', async () => { const signedOrders = [signedOrder, anotherSignedOrder]; const makerAssetFillAmount = takerTokenFillAmount; txHash = await contractWrappers.exchange.marketBuyOrdersAsync( signedOrders, makerAssetFillAmount, takerAddress, ); await web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS); }); }); describe('#marketBuyOrdersNoThrowAsync', () => { it('should no throw maker buy', async () => { const signedOrders = [signedOrder, anotherSignedOrder]; const makerAssetFillAmount = takerTokenFillAmount; txHash = await contractWrappers.exchange.marketBuyOrdersNoThrowAsync( signedOrders, makerAssetFillAmount, takerAddress, ); await web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS); const orderInfo = await contractWrappers.exchange.getOrderInfoAsync(signedOrder); expect(orderInfo.orderStatus).to.be.equal(OrderStatus.FULLY_FILLED); }); }); describe('#marketSellOrdersAsync', () => { it('should maker sell', async () => { const signedOrders = [signedOrder, anotherSignedOrder]; const takerAssetFillAmount = takerTokenFillAmount; txHash = await contractWrappers.exchange.marketSellOrdersAsync( signedOrders, takerAssetFillAmount, takerAddress, ); await web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS); }); }); describe('#marketSellOrdersNoThrowAsync', () => { it('should no throw maker sell', async () => { const signedOrders = [signedOrder, anotherSignedOrder]; const takerAssetFillAmount = takerTokenFillAmount; txHash = await contractWrappers.exchange.marketSellOrdersNoThrowAsync( signedOrders, takerAssetFillAmount, takerAddress, ); await web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS); const orderInfo = await contractWrappers.exchange.getOrderInfoAsync(signedOrder); expect(orderInfo.orderStatus).to.be.equal(OrderStatus.FULLY_FILLED); }); }); describe('#batchFillOrdersNoThrowAsync', () => { it('should fill a batch of valid orders', async () => { const signedOrders = [signedOrder, anotherSignedOrder]; const takerAssetFillAmounts = [takerTokenFillAmount, takerTokenFillAmount]; txHash = await contractWrappers.exchange.batchFillOrdersNoThrowAsync( signedOrders, takerAssetFillAmounts, takerAddress, ); await web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS); let orderInfo = await contractWrappers.exchange.getOrderInfoAsync(signedOrder); expect(orderInfo.orderStatus).to.be.equal(OrderStatus.FULLY_FILLED); orderInfo = await contractWrappers.exchange.getOrderInfoAsync(anotherSignedOrder); expect(orderInfo.orderStatus).to.be.equal(OrderStatus.FULLY_FILLED); }); }); describe('#batchFillOrKillOrdersAsync', () => { it('should fill or kill a batch of valid orders', async () => { const signedOrders = [signedOrder, anotherSignedOrder]; const takerAssetFillAmounts = [takerTokenFillAmount, takerTokenFillAmount]; txHash = await contractWrappers.exchange.batchFillOrKillOrdersAsync( signedOrders, takerAssetFillAmounts, takerAddress, ); await web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS); }); }); describe('#matchOrdersAsync', () => { it('should match two valid ordersr', async () => { const matchingSignedOrder = await fillScenarios.createFillableSignedOrderAsync( takerAssetData, makerAssetData, makerAddress, takerAddress, fillableAmount, ); txHash = await contractWrappers.exchange.matchOrdersAsync( signedOrder, matchingSignedOrder, takerAddress, ); await web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS); }); }); }); describe('cancel order(s)', () => { describe('#cancelOrderAsync', () => { it('should cancel a valid order', async () => { txHash = await contractWrappers.exchange.cancelOrderAsync(signedOrder); await web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS); }); }); describe('#batchCancelOrdersAsync', () => { it('should cancel a batch of valid orders', async () => { const orders = [signedOrder, anotherSignedOrder]; txHash = await contractWrappers.exchange.batchCancelOrdersAsync(orders); await web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS); }); }); describe('#cancelOrdersUpTo/getOrderEpochAsync', () => { it('should cancel orders up to target order epoch', async () => { const targetOrderEpoch = new BigNumber(42); txHash = await contractWrappers.exchange.cancelOrdersUpToAsync(targetOrderEpoch, makerAddress); await web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS); const orderEpoch = await contractWrappers.exchange.getOrderEpochAsync( makerAddress, constants.NULL_ADDRESS, ); expect(orderEpoch).to.be.bignumber.equal(targetOrderEpoch.plus(1)); }); }); }); describe('#getZRXAssetData', () => { it('should get the asset data', () => { const ZRX_ASSET_DATA = contractWrappers.exchange.getZRXAssetData(); const ASSET_DATA_HEX_LENGTH = 74; expect(ZRX_ASSET_DATA).to.have.length(ASSET_DATA_HEX_LENGTH); }); }); describe('#getOrderInfoAsync', () => { it('should get the order info', async () => { const orderInfo = await contractWrappers.exchange.getOrderInfoAsync(signedOrder); const orderHash = orderHashUtils.getOrderHashHex(signedOrder); expect(orderInfo.orderHash).to.be.equal(orderHash); }); }); describe('#getOrdersInfoAsync', () => { it('should get the orders info', async () => { const ordersInfo = await contractWrappers.exchange.getOrdersInfoAsync([signedOrder, anotherSignedOrder]); const orderHash = orderHashUtils.getOrderHashHex(signedOrder); expect(ordersInfo[0].orderHash).to.be.equal(orderHash); const anotherOrderHash = orderHashUtils.getOrderHashHex(anotherSignedOrder); expect(ordersInfo[1].orderHash).to.be.equal(anotherOrderHash); }); }); describe('#validateOrderFillableOrThrowAsync', () => { it('should throw if signature is invalid', async () => { const signedOrderWithInvalidSignature = { ...signedOrder, signature: '0x1b61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc3340349190569279751135161d22529dc25add4f6069af05be04cacbda2ace225403', }; expect( contractWrappers.exchange.validateOrderFillableOrThrowAsync(signedOrderWithInvalidSignature), ).to.eventually.to.be.rejectedWith(RevertReason.InvalidOrderSignature); }); }); describe('#isValidSignature', () => { it('should check if the signature is valid', async () => { const orderHash = orderHashUtils.getOrderHashHex(signedOrder); let isValid = await contractWrappers.exchange.isValidSignatureAsync( orderHash, signedOrder.makerAddress, signedOrder.signature, ); expect(isValid).to.be.true(); isValid = await contractWrappers.exchange.isValidSignatureAsync( orderHash, signedOrder.takerAddress, signedOrder.signature, ); expect(isValid).to.be.false(); }); }); describe('#isAllowedValidatorAsync', () => { it('should check if the validator is allowed', async () => { const signerAddress = makerAddress; const validatorAddress = constants.NULL_ADDRESS; const isAllowed = await contractWrappers.exchange.isAllowedValidatorAsync(signerAddress, validatorAddress); expect(isAllowed).to.be.false(); }); }); describe('#setSignatureValidatorApproval', () => { it('should set signature validator approval', async () => { const validatorAddress = constants.NULL_ADDRESS; const isApproved = true; const senderAddress = makerAddress; txHash = await contractWrappers.exchange.setSignatureValidatorApprovalAsync( validatorAddress, isApproved, senderAddress, ); await web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS); }); }); describe('#isTransactionExecutedAsync', () => { it('should check if the transaction is executed', async () => { const transactionHash = '0x0000000000000000000000000000000000000000000000000000000000000000'; const isExecuted = await contractWrappers.exchange.isTransactionExecutedAsync(transactionHash); expect(isExecuted).to.be.false(); }); }); describe('#getAssetProxyBySignatureAsync', () => { it('should fill or kill a valid order', async () => { const erc20ProxyId = await contractWrappers.erc20Proxy.getProxyIdAsync(); const erc20ProxyAddressById = await contractWrappers.exchange.getAssetProxyBySignatureAsync(erc20ProxyId); const erc20ProxyAddress = contractWrappers.erc20Proxy.address; expect(erc20ProxyAddressById).to.be.equal(erc20ProxyAddress); const erc721ProxyId = await contractWrappers.erc721Proxy.getProxyIdAsync(); const erc721ProxyAddressById = await contractWrappers.exchange.getAssetProxyBySignatureAsync(erc721ProxyId); const erc721ProxyAddress = contractWrappers.erc721Proxy.address; expect(erc721ProxyAddressById).to.be.equal(erc721ProxyAddress); }); }); describe('#preSignAsync/isPreSignedAsync', () => { it('should preSign the hash', async () => { const senderAddress = takerAddress; const hash = orderHashUtils.getOrderHashHex(signedOrder); const signerAddress = signedOrder.makerAddress; let isPreSigned = await contractWrappers.exchange.isPreSignedAsync(hash, signerAddress); expect(isPreSigned).to.be.false(); txHash = await contractWrappers.exchange.preSignAsync( hash, signerAddress, signedOrder.signature, senderAddress, ); await web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS); isPreSigned = await contractWrappers.exchange.isPreSignedAsync(hash, signerAddress); expect(isPreSigned).to.be.true(); }); }); describe('#getVersionAsync', () => { it('should return version the hash', async () => { const version = await contractWrappers.exchange.getVersionAsync(); const VERSION = '2.0.0'; expect(version).to.be.equal(VERSION); }); }); describe('#subscribe', () => { const indexFilterValues = {}; const takerTokenFillAmountInBaseUnits = new BigNumber(1); beforeEach(async () => { signedOrder = await fillScenarios.createFillableSignedOrderAsync( makerAssetData, takerAssetData, makerAddress, takerAddress, fillableAmount, ); }); afterEach(async () => { contractWrappers.exchange.unsubscribeAll(); }); // Hack: Mocha does not allow a test to be both async and have a `done` callback // Since we need to await the receipt of the event in the `subscribe` callback, // we do need both. A hack is to make the top-level a sync fn w/ a done callback and then // wrap the rest of the test in an async block // Source: https://github.com/mochajs/mocha/issues/2407 it('Should receive the Fill event when an order is filled', (done: DoneCallback) => { (async () => { const callback = callbackErrorReporter.reportNodeCallbackErrors(done)( (logEvent: DecodedLogEvent) => { expect(logEvent.log.event).to.be.equal(ExchangeEvents.Fill); }, ); contractWrappers.exchange.subscribe(ExchangeEvents.Fill, indexFilterValues, callback); await contractWrappers.exchange.fillOrderAsync( signedOrder, takerTokenFillAmountInBaseUnits, takerAddress, ); })().catch(done); }); it('Should receive the LogCancel event when an order is cancelled', (done: DoneCallback) => { (async () => { const callback = callbackErrorReporter.reportNodeCallbackErrors(done)( (logEvent: DecodedLogEvent) => { expect(logEvent.log.event).to.be.equal(ExchangeEvents.Cancel); }, ); contractWrappers.exchange.subscribe(ExchangeEvents.Cancel, indexFilterValues, callback); await contractWrappers.exchange.cancelOrderAsync(signedOrder); })().catch(done); }); it('Outstanding subscriptions are cancelled when contractWrappers.unsubscribeAll called', (done: DoneCallback) => { (async () => { const callbackNeverToBeCalled = callbackErrorReporter.reportNodeCallbackErrors(done)( (logEvent: DecodedLogEvent) => { done(new Error('Expected this subscription to have been cancelled')); }, ); contractWrappers.exchange.subscribe(ExchangeEvents.Fill, indexFilterValues, callbackNeverToBeCalled); contractWrappers.unsubscribeAll(); const callback = callbackErrorReporter.reportNodeCallbackErrors(done)( (logEvent: DecodedLogEvent) => { expect(logEvent.log.event).to.be.equal(ExchangeEvents.Fill); }, ); contractWrappers.exchange.subscribe(ExchangeEvents.Fill, indexFilterValues, callback); await contractWrappers.exchange.fillOrderAsync( signedOrder, takerTokenFillAmountInBaseUnits, takerAddress, ); })().catch(done); }); it('Should cancel subscription when unsubscribe called', (done: DoneCallback) => { (async () => { const callbackNeverToBeCalled = callbackErrorReporter.reportNodeCallbackErrors(done)( (_logEvent: DecodedLogEvent) => { done(new Error('Expected this subscription to have been cancelled')); }, ); const subscriptionToken = contractWrappers.exchange.subscribe( ExchangeEvents.Fill, indexFilterValues, callbackNeverToBeCalled, ); contractWrappers.exchange.unsubscribe(subscriptionToken); await contractWrappers.exchange.fillOrderAsync( signedOrder, takerTokenFillAmountInBaseUnits, takerAddress, ); done(); })().catch(done); }); }); describe('#getLogsAsync', () => { const blockRange = { fromBlock: 0, toBlock: BlockParamLiteral.Latest, }; it('should get logs with decoded args emitted by Fill', async () => { txHash = await contractWrappers.exchange.fillOrderAsync(signedOrder, takerTokenFillAmount, takerAddress); const eventName = ExchangeEvents.Fill; const indexFilterValues = {}; const logs = await contractWrappers.exchange.getLogsAsync(eventName, blockRange, indexFilterValues); expect(logs).to.have.length(1); expect(logs[0].event).to.be.equal(eventName); }); it('should only get the logs with the correct event name', async () => { txHash = await contractWrappers.exchange.fillOrderAsync(signedOrder, takerTokenFillAmount, takerAddress); await web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS); const differentEventName = ExchangeEvents.Cancel; const indexFilterValues = {}; const logs = await contractWrappers.exchange.getLogsAsync( differentEventName, blockRange, indexFilterValues, ); expect(logs).to.have.length(0); }); it('should only get the logs with the correct indexed fields', async () => { txHash = await contractWrappers.exchange.fillOrderAsync(signedOrder, takerTokenFillAmount, takerAddress); await web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS); const signedOrderWithAnotherMakerAddress = await fillScenarios.createFillableSignedOrderAsync( makerAssetData, takerAssetData, anotherMakerAddress, takerAddress, fillableAmount, ); txHash = await contractWrappers.exchange.fillOrderAsync( signedOrderWithAnotherMakerAddress, takerTokenFillAmount, takerAddress, ); await web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS); const eventName = ExchangeEvents.Fill; const indexFilterValues = { makerAddress: anotherMakerAddress, }; const logs = await contractWrappers.exchange.getLogsAsync( eventName, blockRange, indexFilterValues, ); expect(logs).to.have.length(1); const args = logs[0].args; expect(args.makerAddress).to.be.equal(anotherMakerAddress); }); }); }); // tslint:disable:max-file-line-count