aboutsummaryrefslogtreecommitdiffstats
path: root/packages/order-watcher/test/order_watcher_web_socket_server_test.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/order-watcher/test/order_watcher_web_socket_server_test.ts')
-rw-r--r--packages/order-watcher/test/order_watcher_web_socket_server_test.ts308
1 files changed, 308 insertions, 0 deletions
diff --git a/packages/order-watcher/test/order_watcher_web_socket_server_test.ts b/packages/order-watcher/test/order_watcher_web_socket_server_test.ts
new file mode 100644
index 000000000..578e0de61
--- /dev/null
+++ b/packages/order-watcher/test/order_watcher_web_socket_server_test.ts
@@ -0,0 +1,308 @@
+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.only('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<WsMessage> {
+ return new Promise<WsMessage>(resolve => {
+ client.onmessage = (msg: WsMessage) => {
+ const data = JSON.parse(msg.data);
+ if (data.method === method) {
+ resolve(msg);
+ }
+ };
+ });
+}