import { ContractWrappers } from '@0x/contract-wrappers'; import { tokenUtils } from '@0x/contract-wrappers/lib/test/utils/token_utils'; import { BlockchainLifecycle } from '@0x/dev-utils'; import { FillScenarios } from '@0x/fill-scenarios'; import { assetDataUtils, orderHashUtils } from '@0x/order-utils'; import { ExchangeContractErrs, OrderStateInvalid, OrderStateValid, SignedOrder } from '@0x/types'; import { BigNumber, logUtils } from '@0x/utils'; import { Web3Wrapper } from '@0x/web3-wrapper'; import * as chai from 'chai'; import 'mocha'; import * as WebSocket from 'websocket'; import { OrderWatcherWebSocketServer } from '../src/order_watcher/order_watcher_web_socket_server'; import { AddOrderRequest, OrderWatcherMethod, RemoveOrderRequest } from '../src/types'; import { chaiSetup } from './utils/chai_setup'; import { constants } from './utils/constants'; import { migrateOnceAsync } from './utils/migrate'; import { provider, web3Wrapper } from './utils/web3_wrapper'; chaiSetup.configure(); const expect = chai.expect; const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); interface WsMessage { data: string; } describe('OrderWatcherWebSocketServer', async () => { let contractWrappers: ContractWrappers; let wsServer: OrderWatcherWebSocketServer; let wsClient: WebSocket.w3cwebsocket; let wsClientTwo: WebSocket.w3cwebsocket; let fillScenarios: FillScenarios; let userAddresses: string[]; let makerAssetData: string; let takerAssetData: string; let makerTokenAddress: string; let takerTokenAddress: string; let makerAddress: string; let takerAddress: string; let zrxTokenAddress: string; let signedOrder: SignedOrder; let orderHash: string; let addOrderPayload: AddOrderRequest; let removeOrderPayload: RemoveOrderRequest; const decimals = constants.ZRX_DECIMALS; const fillableAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(5), decimals); before(async () => { // Set up constants const contractAddresses = await migrateOnceAsync(); await blockchainLifecycle.startAsync(); const networkId = constants.TESTRPC_NETWORK_ID; const config = { networkId, contractAddresses, }; contractWrappers = new ContractWrappers(provider, config); userAddresses = await web3Wrapper.getAvailableAddressesAsync(); zrxTokenAddress = contractAddresses.zrxToken; [makerAddress, takerAddress] = userAddresses; [makerTokenAddress, takerTokenAddress] = tokenUtils.getDummyERC20TokenAddresses(); [makerAssetData, takerAssetData] = [ assetDataUtils.encodeERC20AssetData(makerTokenAddress), assetDataUtils.encodeERC20AssetData(takerTokenAddress), ]; fillScenarios = new FillScenarios( provider, userAddresses, zrxTokenAddress, contractAddresses.exchange, contractAddresses.erc20Proxy, contractAddresses.erc721Proxy, ); signedOrder = await fillScenarios.createFillableSignedOrderAsync( makerAssetData, takerAssetData, makerAddress, takerAddress, fillableAmount, ); orderHash = orderHashUtils.getOrderHashHex(signedOrder); addOrderPayload = { id: 1, jsonrpc: '2.0', method: OrderWatcherMethod.AddOrder, params: { signedOrder }, }; removeOrderPayload = { id: 1, jsonrpc: '2.0', method: OrderWatcherMethod.RemoveOrder, params: { orderHash }, }; // Prepare OrderWatcher WebSocket server const orderWatcherConfig = { isVerbose: true, }; wsServer = new OrderWatcherWebSocketServer(provider, networkId, contractAddresses, orderWatcherConfig); }); after(async () => { await blockchainLifecycle.revertAsync(); }); beforeEach(async () => { wsServer.start(); await blockchainLifecycle.startAsync(); wsClient = new WebSocket.w3cwebsocket('ws://127.0.0.1:8080/'); logUtils.log(`${new Date()} [Client] Connected.`); }); afterEach(async () => { wsClient.close(); await blockchainLifecycle.revertAsync(); wsServer.stop(); logUtils.log(`${new Date()} [Client] Closed.`); }); it('responds to getStats requests correctly', (done: any) => { const payload = { id: 1, jsonrpc: '2.0', method: 'GET_STATS', }; wsClient.onopen = () => wsClient.send(JSON.stringify(payload)); wsClient.onmessage = (msg: any) => { const responseData = JSON.parse(msg.data); expect(responseData.id).to.be.eq(1); expect(responseData.jsonrpc).to.be.eq('2.0'); expect(responseData.method).to.be.eq('GET_STATS'); expect(responseData.result.orderCount).to.be.eq(0); done(); }; }); it('throws an error when an invalid method is attempted', async () => { const invalidMethodPayload = { id: 1, jsonrpc: '2.0', method: 'BAD_METHOD', }; wsClient.onopen = () => wsClient.send(JSON.stringify(invalidMethodPayload)); const errorMsg = await onMessageAsync(wsClient, null); const errorData = JSON.parse(errorMsg.data); // tslint:disable-next-line:no-unused-expression expect(errorData.id).to.be.null; // tslint:disable-next-line:no-unused-expression expect(errorData.method).to.be.null; expect(errorData.jsonrpc).to.be.eq('2.0'); expect(errorData.error).to.match(/^Error: Expected request to conform to schema/); }); it('throws an error when jsonrpc field missing from request', async () => { const noJsonRpcPayload = { id: 1, method: 'GET_STATS', }; wsClient.onopen = () => wsClient.send(JSON.stringify(noJsonRpcPayload)); const errorMsg = await onMessageAsync(wsClient, null); const errorData = JSON.parse(errorMsg.data); // tslint:disable-next-line:no-unused-expression expect(errorData.method).to.be.null; expect(errorData.jsonrpc).to.be.eq('2.0'); expect(errorData.error).to.match(/^Error: Expected request to conform to schema/); }); it('throws an error when we try to add an order without a signedOrder', async () => { const noSignedOrderAddOrderPayload = { id: 1, jsonrpc: '2.0', method: 'ADD_ORDER', orderHash: '0x7337e2f2a9aa2ed6afe26edc2df7ad79c3ffa9cf9b81a964f707ea63f5272355', }; wsClient.onopen = () => wsClient.send(JSON.stringify(noSignedOrderAddOrderPayload)); const errorMsg = await onMessageAsync(wsClient, null); const errorData = JSON.parse(errorMsg.data); // tslint:disable-next-line:no-unused-expression expect(errorData.id).to.be.null; // tslint:disable-next-line:no-unused-expression expect(errorData.method).to.be.null; expect(errorData.jsonrpc).to.be.eq('2.0'); expect(errorData.error).to.match(/^Error: Expected request to conform to schema/); }); it('throws an error when we try to add a bad signedOrder', async () => { const invalidAddOrderPayload = { id: 1, jsonrpc: '2.0', method: 'ADD_ORDER', signedOrder: { makerAddress: '0x0', }, }; wsClient.onopen = () => wsClient.send(JSON.stringify(invalidAddOrderPayload)); const errorMsg = await onMessageAsync(wsClient, null); const errorData = JSON.parse(errorMsg.data); // tslint:disable-next-line:no-unused-expression expect(errorData.id).to.be.null; // tslint:disable-next-line:no-unused-expression expect(errorData.method).to.be.null; expect(errorData.error).to.match(/^Error: Expected request to conform to schema/); }); it('executes addOrder and removeOrder requests correctly', async () => { wsClient.onopen = () => wsClient.send(JSON.stringify(addOrderPayload)); const addOrderMsg = await onMessageAsync(wsClient, OrderWatcherMethod.AddOrder); const addOrderData = JSON.parse(addOrderMsg.data); expect(addOrderData.method).to.be.eq('ADD_ORDER'); expect((wsServer as any)._orderWatcher._orderByOrderHash).to.deep.include({ [orderHash]: signedOrder, }); const clientOnMessagePromise = onMessageAsync(wsClient, OrderWatcherMethod.RemoveOrder); wsClient.send(JSON.stringify(removeOrderPayload)); const removeOrderMsg = await clientOnMessagePromise; const removeOrderData = JSON.parse(removeOrderMsg.data); expect(removeOrderData.method).to.be.eq('REMOVE_ORDER'); expect((wsServer as any)._orderWatcher._orderByOrderHash).to.not.deep.include({ [orderHash]: signedOrder, }); }); it('broadcasts orderStateInvalid message when makerAddress allowance set to 0 for watched order', async () => { // Add the regular order wsClient.onopen = () => wsClient.send(JSON.stringify(addOrderPayload)); // We register the onMessage callback before calling `setProxyAllowanceAsync` which we // expect will cause a message to be emitted. We do now "await" here, since we want to // check for messages _after_ calling `setProxyAllowanceAsync` const clientOnMessagePromise = onMessageAsync(wsClient, OrderWatcherMethod.Update); // Set the allowance to 0 await contractWrappers.erc20Token.setProxyAllowanceAsync(makerTokenAddress, makerAddress, new BigNumber(0)); // We now await the `onMessage` promise to check for the message const orderWatcherUpdateMsg = await clientOnMessagePromise; const orderWatcherUpdateData = JSON.parse(orderWatcherUpdateMsg.data); expect(orderWatcherUpdateData.method).to.be.eq('UPDATE'); const invalidOrderState = orderWatcherUpdateData.result as OrderStateInvalid; expect(invalidOrderState.isValid).to.be.false(); expect(invalidOrderState.orderHash).to.be.eq(orderHash); expect(invalidOrderState.error).to.be.eq(ExchangeContractErrs.InsufficientMakerAllowance); }); it('broadcasts to multiple clients when an order backing ZRX allowance changes', async () => { // Prepare order const makerFee = Web3Wrapper.toBaseUnitAmount(new BigNumber(2), decimals); const takerFee = Web3Wrapper.toBaseUnitAmount(new BigNumber(0), decimals); const nonZeroMakerFeeSignedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync( makerAssetData, takerAssetData, makerFee, takerFee, makerAddress, takerAddress, fillableAmount, takerAddress, ); const nonZeroMakerFeeOrderPayload = { id: 1, jsonrpc: '2.0', method: 'ADD_ORDER', signedOrder: nonZeroMakerFeeSignedOrder, }; // Set up a second client and have it add the order wsClientTwo = new WebSocket.w3cwebsocket('ws://127.0.0.1:8080/'); logUtils.log(`${new Date()} [Client] Connected.`); wsClientTwo.onopen = () => wsClientTwo.send(JSON.stringify(nonZeroMakerFeeOrderPayload)); // Setup the onMessage callbacks, but don't await them yet const clientOneOnMessagePromise = onMessageAsync(wsClient, OrderWatcherMethod.Update); const clientTwoOnMessagePromise = onMessageAsync(wsClientTwo, OrderWatcherMethod.Update); // Change the allowance await contractWrappers.erc20Token.setProxyAllowanceAsync(zrxTokenAddress, makerAddress, new BigNumber(0)); // Check that both clients receive the emitted event by awaiting the onMessageAsync promises let updateMsg = await clientOneOnMessagePromise; let updateData = JSON.parse(updateMsg.data); let orderState = updateData.result as OrderStateValid; expect(orderState.isValid).to.be.true(); expect(orderState.orderRelevantState.makerFeeProxyAllowance).to.be.eq('0'); updateMsg = await clientTwoOnMessagePromise; updateData = JSON.parse(updateMsg.data); orderState = updateData.result as OrderStateValid; expect(orderState.isValid).to.be.true(); expect(orderState.orderRelevantState.makerFeeProxyAllowance).to.be.eq('0'); wsClientTwo.close(); logUtils.log(`${new Date()} [Client] Closed.`); }); }); // HACK: createFillableSignedOrderAsync is Promise-based, which forces us // to use Promises instead of the done() callbacks for tests. // onmessage callback must thus be wrapped as a Promise. async function onMessageAsync(client: WebSocket.w3cwebsocket, method: string | null): Promise { return new Promise(resolve => { client.onmessage = (msg: WsMessage) => { const data = JSON.parse(msg.data); if (data.method === method) { resolve(msg); } }; }); }