Merge pull request #1427 from 0xProject/features/orderwatcher_ws
OrderWatcher WebSocket Server
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"id": "/orderWatcherWebSocketRequestSchema",
|
||||
"type": "object",
|
||||
"definitions": {
|
||||
"signedOrderParam": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"signedOrder": { "$ref": "/signedOrderSchema" }
|
||||
},
|
||||
"required": ["signedOrder"]
|
||||
},
|
||||
"orderHashParam": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"orderHash": { "$ref": "/hexSchema" }
|
||||
},
|
||||
"required": ["orderHash"]
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "number" },
|
||||
"jsonrpc": { "type": "string" },
|
||||
"method": { "enum": ["ADD_ORDER"] },
|
||||
"params": { "$ref": "#/definitions/signedOrderParam" }
|
||||
},
|
||||
"required": ["id", "jsonrpc", "method", "params"]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "number" },
|
||||
"jsonrpc": { "type": "string" },
|
||||
"method": { "enum": ["REMOVE_ORDER"] },
|
||||
"params": { "$ref": "#/definitions/orderHashParam" }
|
||||
},
|
||||
"required": ["id", "jsonrpc", "method", "params"]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "number" },
|
||||
"jsonrpc": { "type": "string" },
|
||||
"method": { "enum": ["GET_STATS"] },
|
||||
"params": {}
|
||||
},
|
||||
"required": ["id", "jsonrpc", "method"]
|
||||
}
|
||||
]
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"id": "/orderWatcherWebSocketUtf8MessageSchema",
|
||||
"properties": {
|
||||
"utf8Data": { "type": "string" }
|
||||
},
|
||||
"required": [
|
||||
"utf8Data"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
@@ -16,6 +16,8 @@ import * as orderFillOrKillRequestsSchema from '../schemas/order_fill_or_kill_re
|
||||
import * as orderFillRequestsSchema from '../schemas/order_fill_requests_schema.json';
|
||||
import * as orderHashSchema from '../schemas/order_hash_schema.json';
|
||||
import * as orderSchema from '../schemas/order_schema.json';
|
||||
import * as orderWatcherWebSocketRequestSchema from '../schemas/order_watcher_web_socket_request_schema.json';
|
||||
import * as orderWatcherWebSocketUtf8MessageSchema from '../schemas/order_watcher_web_socket_utf8_message_schema.json';
|
||||
import * as orderBookRequestSchema from '../schemas/orderbook_request_schema.json';
|
||||
import * as ordersRequestOptsSchema from '../schemas/orders_request_opts_schema.json';
|
||||
import * as ordersSchema from '../schemas/orders_schema.json';
|
||||
@@ -66,6 +68,8 @@ export const schemas = {
|
||||
jsNumber,
|
||||
requestOptsSchema,
|
||||
pagedRequestOptsSchema,
|
||||
orderWatcherWebSocketRequestSchema,
|
||||
orderWatcherWebSocketUtf8MessageSchema,
|
||||
ordersRequestOptsSchema,
|
||||
orderBookRequestSchema,
|
||||
orderConfigRequestSchema,
|
||||
|
@@ -23,6 +23,8 @@
|
||||
"./schemas/order_schema.json",
|
||||
"./schemas/signed_order_schema.json",
|
||||
"./schemas/orders_schema.json",
|
||||
"./schemas/order_watcher_web_socket_request_schema.json",
|
||||
"./schemas/order_watcher_web_socket_utf8_message_schema.json",
|
||||
"./schemas/paginated_collection_schema.json",
|
||||
"./schemas/relayer_api_asset_data_pairs_response_schema.json",
|
||||
"./schemas/relayer_api_asset_data_pairs_schema.json",
|
||||
|
@@ -4,6 +4,9 @@ An order watcher daemon that watches for order validity.
|
||||
|
||||
#### Read the wiki [article](https://0xproject.com/wiki#0x-OrderWatcher).
|
||||
|
||||
OrderWatcher also comes with a WebSocket server to provide language-agnostic access
|
||||
to order watching functionality. We used the [WebSocket Client and Server Implementation for Node](https://www.npmjs.com/package/websocket). The server sends and receives messages that conform to the [JSON RPC specifications](https://www.jsonrpc.org/specification).
|
||||
|
||||
## Installation
|
||||
|
||||
**Install**
|
||||
@@ -26,6 +29,91 @@ If your project is in [TypeScript](https://www.typescriptlang.org/), add the fol
|
||||
}
|
||||
```
|
||||
|
||||
## Using the WebSocket Server
|
||||
|
||||
**Setup**
|
||||
|
||||
**Environmental Variables**
|
||||
Several environmental variables can be set to configure the server:
|
||||
|
||||
* `ORDER_WATCHER_HTTP_PORT` specifies the port that the http server will listen on
|
||||
and accept connections from. When this is not set, we default to 8080.
|
||||
|
||||
**Requests**
|
||||
The server accepts three types of requests: `ADD_ORDER`, `REMOVE_ORDER` and `GET_STATS`. These mirror what the underlying OrderWatcher does. You can read more in the [wiki](https://0xproject.com/wiki#0x-OrderWatcher). Unlike the OrderWatcher, it does not expose any `subscribe` or `unsubscribe` functionality because the WebSocket server keeps a single subscription open for all clients.
|
||||
|
||||
The first step for making a request is establishing a connection with the server. In Javascript:
|
||||
|
||||
```
|
||||
var W3CWebSocket = require('websocket').w3cwebsocket;
|
||||
wsClient = new W3CWebSocket('ws://127.0.0.1:8080');
|
||||
```
|
||||
|
||||
In Python, you could use the [websocket-client library](http://pypi.python.org/pypi/websocket-client/) and run:
|
||||
|
||||
```
|
||||
from websocket import create_connection
|
||||
wsClient = create_connection("ws://127.0.0.1:8080")
|
||||
```
|
||||
|
||||
With the connection established, you prepare the payload for your request. The payload is a json object with a format established by the [JSON RPC specification](https://www.jsonrpc.org/specification):
|
||||
|
||||
* `id`: All requests require you to specify a numerical `id`. When the server responds to the request, the response will have the same `id` as the one supplied with your request.
|
||||
* `jsonrpc`: This is always the string `'2.0'`.
|
||||
* `method`: This specifies the OrderWatcher method you want to call. I.e., `'ADD_ORDER'`, `'REMOVE_ORDER'` or `'GET_STATS'`.
|
||||
* `params`: These contain the parameters needed by OrderWatcher to execute the method you called. For `ADD_ORDER`, provide `{ signedOrder: <your signedOrder> }`. For `REMOVE_ORDER`, provide `{ orderHash: <your orderHash> }`. For `GET_STATS`, no parameters are needed, so you may leave this empty.
|
||||
|
||||
Next, convert the payload to a string and send it through the connection.
|
||||
In Javascript:
|
||||
|
||||
```
|
||||
const addOrderPayload = {
|
||||
id: 1,
|
||||
jsonrpc: '2.0',
|
||||
method: 'ADD_ORDER',
|
||||
params: { signedOrder: <your signedOrder> },
|
||||
};
|
||||
wsClient.send(JSON.stringify(addOrderPayload));
|
||||
```
|
||||
|
||||
In Python:
|
||||
|
||||
```
|
||||
import json
|
||||
remove_order_payload = {
|
||||
'id': 1,
|
||||
'jsonrpc': '2.0',
|
||||
'method': 'REMOVE_ORDER',
|
||||
'params': {'orderHash': '0x6edc16bf37fde79f5012088c33784c730e2f103d9ab1caf73060c386ad107b7e'},
|
||||
}
|
||||
wsClient.send(json.dumps(remove_order_payload));
|
||||
```
|
||||
|
||||
**Response**
|
||||
The server responds to all requests in a similar format. In the data field, you'll find another object containing the following fields:
|
||||
|
||||
* `id`: The id corresponding to the request that the server is responding to. `UPDATE` responses are not based on any requests so the `id` field is omitted`.
|
||||
* `jsonrpc`: Always `'2.0'`.
|
||||
* `method`: The method the server is responding to. Eg. `ADD_ORDER`. When order states change the server may also initiate a response. In this case, method will be listed as `UPDATE`.
|
||||
* `result`: This field varies based on the method. `UPDATE` responses contain the new order state. `GET_STATS` responses contain the current order count. When there are errors, this field is omitted.
|
||||
* `error`: When there is an error executing a request, the [JSON RPC](https://www.jsonrpc.org/specification) error object is listed here. When the server responds successfully, this field is omitted.
|
||||
|
||||
In Javascript, the responses can be parsed using the `onmessage` callback:
|
||||
|
||||
```
|
||||
wsClient.onmessage = (msg) => {
|
||||
const responseData = JSON.parse(msg.data);
|
||||
const method = responseData.method
|
||||
};
|
||||
```
|
||||
|
||||
In Python, `recv` is a lightweight way to receive a response:
|
||||
|
||||
```
|
||||
result = wsClient.recv()
|
||||
method = result.method
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
We strongly recommend that the community help us make improvements and determine the future direction of the protocol. To report bugs within this package, please create an issue in this repository.
|
||||
|
@@ -74,7 +74,8 @@
|
||||
"ethereum-types": "^1.1.4",
|
||||
"ethereumjs-blockstream": "6.0.0",
|
||||
"ethers": "~4.0.4",
|
||||
"lodash": "^4.17.5"
|
||||
"lodash": "^4.17.5",
|
||||
"websocket": "^1.0.25"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
@@ -1,4 +1,5 @@
|
||||
export { OrderWatcher } from './order_watcher/order_watcher';
|
||||
export { OrderWatcherWebSocketServer } from './order_watcher/order_watcher_web_socket_server';
|
||||
export { ExpirationWatcher } from './order_watcher/expiration_watcher';
|
||||
|
||||
export {
|
||||
|
@@ -0,0 +1,200 @@
|
||||
import { ContractAddresses } from '@0x/contract-addresses';
|
||||
import { schemas } from '@0x/json-schemas';
|
||||
import { OrderStateInvalid, OrderStateValid, SignedOrder } from '@0x/types';
|
||||
import { BigNumber, logUtils } from '@0x/utils';
|
||||
import { Provider } from 'ethereum-types';
|
||||
import * as http from 'http';
|
||||
import * as WebSocket from 'websocket';
|
||||
|
||||
import { GetStatsResult, OrderWatcherConfig, OrderWatcherMethod, WebSocketRequest, WebSocketResponse } from '../types';
|
||||
import { assert } from '../utils/assert';
|
||||
|
||||
import { OrderWatcher } from './order_watcher';
|
||||
|
||||
const DEFAULT_HTTP_PORT = 8080;
|
||||
const JSON_RPC_VERSION = '2.0';
|
||||
|
||||
// Wraps the OrderWatcher functionality in a WebSocket server. Motivations:
|
||||
// 1) Users can watch orders via non-typescript programs.
|
||||
// 2) Better encapsulation so that users can work
|
||||
export class OrderWatcherWebSocketServer {
|
||||
private readonly _orderWatcher: OrderWatcher;
|
||||
private readonly _httpServer: http.Server;
|
||||
private readonly _connectionStore: Set<WebSocket.connection>;
|
||||
private readonly _wsServer: WebSocket.server;
|
||||
private readonly _isVerbose: boolean;
|
||||
/**
|
||||
* Recover types lost when the payload is stringified.
|
||||
*/
|
||||
private static _parseSignedOrder(rawRequest: any): SignedOrder {
|
||||
const bigNumberFields = [
|
||||
'salt',
|
||||
'makerFee',
|
||||
'takerFee',
|
||||
'makerAssetAmount',
|
||||
'takerAssetAmount',
|
||||
'expirationTimeSeconds',
|
||||
];
|
||||
for (const field of bigNumberFields) {
|
||||
rawRequest[field] = new BigNumber(rawRequest[field]);
|
||||
}
|
||||
return rawRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate a new WebSocket server which provides OrderWatcher functionality
|
||||
* @param provider Web3 provider to use for JSON RPC calls.
|
||||
* @param networkId NetworkId to watch orders on.
|
||||
* @param contractAddresses Optional contract addresses. Defaults to known
|
||||
* addresses based on networkId.
|
||||
* @param orderWatcherConfig OrderWatcher configurations. isVerbose sets the verbosity for the WebSocket server aswell.
|
||||
* @param isVerbose Whether to enable verbose logging. Defaults to true.
|
||||
*/
|
||||
constructor(
|
||||
provider: Provider,
|
||||
networkId: number,
|
||||
contractAddresses?: ContractAddresses,
|
||||
orderWatcherConfig?: Partial<OrderWatcherConfig>,
|
||||
) {
|
||||
this._isVerbose =
|
||||
orderWatcherConfig !== undefined && orderWatcherConfig.isVerbose !== undefined
|
||||
? orderWatcherConfig.isVerbose
|
||||
: true;
|
||||
this._orderWatcher = new OrderWatcher(provider, networkId, contractAddresses, orderWatcherConfig);
|
||||
this._connectionStore = new Set();
|
||||
this._httpServer = http.createServer();
|
||||
this._wsServer = new WebSocket.server({
|
||||
httpServer: this._httpServer,
|
||||
// Avoid setting autoAcceptConnections to true as it defeats all
|
||||
// standard cross-origin protection facilities built into the protocol
|
||||
// and the browser.
|
||||
// Source: https://www.npmjs.com/package/websocket#server-example
|
||||
// Also ensures that a request event is emitted by
|
||||
// the server whenever a new WebSocket request is made.
|
||||
autoAcceptConnections: false,
|
||||
});
|
||||
|
||||
this._wsServer.on('request', async (request: any) => {
|
||||
// Designed for usage pattern where client and server are run on the same
|
||||
// machine by the same user. As such, no security checks are in place.
|
||||
const connection: WebSocket.connection = request.accept(null, request.origin);
|
||||
this._log(`${new Date()} [Server] Accepted connection from origin ${request.origin}.`);
|
||||
connection.on('message', this._onMessageCallbackAsync.bind(this, connection));
|
||||
connection.on('close', this._onCloseCallback.bind(this, connection));
|
||||
this._connectionStore.add(connection);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates the WebSocket server by subscribing to the OrderWatcher and
|
||||
* starting the WebSocket's HTTP server
|
||||
*/
|
||||
public start(): void {
|
||||
// Have the WebSocket server subscribe to the OrderWatcher to receive updates.
|
||||
// These updates are then broadcast to clients in the _connectionStore.
|
||||
this._orderWatcher.subscribe(this._broadcastCallback.bind(this));
|
||||
|
||||
const port = process.env.ORDER_WATCHER_HTTP_PORT || DEFAULT_HTTP_PORT;
|
||||
this._httpServer.listen(port, () => {
|
||||
this._log(`${new Date()} [Server] Listening on port ${port}`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivates the WebSocket server by stopping the HTTP server from accepting
|
||||
* new connections and unsubscribing from the OrderWatcher
|
||||
*/
|
||||
public stop(): void {
|
||||
this._httpServer.close();
|
||||
this._orderWatcher.unsubscribe();
|
||||
}
|
||||
|
||||
private _log(...args: any[]): void {
|
||||
if (this._isVerbose) {
|
||||
logUtils.log(...args);
|
||||
}
|
||||
}
|
||||
|
||||
private async _onMessageCallbackAsync(connection: WebSocket.connection, message: any): Promise<void> {
|
||||
let response: WebSocketResponse;
|
||||
let id: number | null = null;
|
||||
try {
|
||||
assert.doesConformToSchema('message', message, schemas.orderWatcherWebSocketUtf8MessageSchema);
|
||||
const request: WebSocketRequest = JSON.parse(message.utf8Data);
|
||||
id = request.id;
|
||||
assert.doesConformToSchema('request', request, schemas.orderWatcherWebSocketRequestSchema);
|
||||
assert.isString(request.jsonrpc, JSON_RPC_VERSION);
|
||||
response = {
|
||||
id,
|
||||
jsonrpc: JSON_RPC_VERSION,
|
||||
method: request.method,
|
||||
result: await this._routeRequestAsync(request),
|
||||
};
|
||||
} catch (err) {
|
||||
response = {
|
||||
id,
|
||||
jsonrpc: JSON_RPC_VERSION,
|
||||
method: null,
|
||||
error: err.toString(),
|
||||
};
|
||||
}
|
||||
this._log(`${new Date()} [Server] OrderWatcher output: ${JSON.stringify(response)}`);
|
||||
connection.sendUTF(JSON.stringify(response));
|
||||
}
|
||||
|
||||
private _onCloseCallback(connection: WebSocket.connection): void {
|
||||
this._connectionStore.delete(connection);
|
||||
this._log(`${new Date()} [Server] Client ${connection.remoteAddress} disconnected.`);
|
||||
}
|
||||
|
||||
private async _routeRequestAsync(request: WebSocketRequest): Promise<GetStatsResult | undefined> {
|
||||
this._log(`${new Date()} [Server] Request received: ${request.method}`);
|
||||
switch (request.method) {
|
||||
case OrderWatcherMethod.AddOrder: {
|
||||
const signedOrder: SignedOrder = OrderWatcherWebSocketServer._parseSignedOrder(
|
||||
request.params.signedOrder,
|
||||
);
|
||||
await this._orderWatcher.addOrderAsync(signedOrder);
|
||||
break;
|
||||
}
|
||||
case OrderWatcherMethod.RemoveOrder: {
|
||||
this._orderWatcher.removeOrder(request.params.orderHash || 'undefined');
|
||||
break;
|
||||
}
|
||||
case OrderWatcherMethod.GetStats: {
|
||||
return this._orderWatcher.getStats();
|
||||
}
|
||||
default:
|
||||
// Should never reach here. Should be caught by JSON schema check.
|
||||
throw new Error(`Unexpected default case hit for request.method`);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts OrderState changes to ALL connected clients. At the moment,
|
||||
* we do not support clients subscribing to only a subset of orders. As such,
|
||||
* Client B will be notified of changes to an order that Client A added.
|
||||
*/
|
||||
private _broadcastCallback(err: Error | null, orderState?: OrderStateValid | OrderStateInvalid | undefined): void {
|
||||
const method = OrderWatcherMethod.Update;
|
||||
const response =
|
||||
err === null
|
||||
? {
|
||||
jsonrpc: JSON_RPC_VERSION,
|
||||
method,
|
||||
result: orderState,
|
||||
}
|
||||
: {
|
||||
jsonrpc: JSON_RPC_VERSION,
|
||||
method,
|
||||
error: {
|
||||
code: -32000,
|
||||
message: err.message,
|
||||
},
|
||||
};
|
||||
this._connectionStore.forEach((connection: WebSocket.connection) => {
|
||||
connection.sendUTF(JSON.stringify(response));
|
||||
});
|
||||
}
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
import { OrderState } from '@0x/types';
|
||||
import { OrderState, SignedOrder } from '@0x/types';
|
||||
import { LogEntryEvent } from 'ethereum-types';
|
||||
|
||||
export enum OrderWatcherError {
|
||||
@@ -31,3 +31,67 @@ export enum InternalOrderWatcherError {
|
||||
ZrxNotInTokenRegistry = 'ZRX_NOT_IN_TOKEN_REGISTRY',
|
||||
WethNotInTokenRegistry = 'WETH_NOT_IN_TOKEN_REGISTRY',
|
||||
}
|
||||
|
||||
export enum OrderWatcherMethod {
|
||||
// Methods initiated by the user.
|
||||
GetStats = 'GET_STATS',
|
||||
AddOrder = 'ADD_ORDER',
|
||||
RemoveOrder = 'REMOVE_ORDER',
|
||||
// These are spontaneous; they are primarily orderstate changes.
|
||||
Update = 'UPDATE',
|
||||
// `subscribe` and `unsubscribe` are methods of OrderWatcher, but we don't
|
||||
// need to expose them to the WebSocket server user because the user implicitly
|
||||
// subscribes and unsubscribes by connecting and disconnecting from the server.
|
||||
}
|
||||
|
||||
// Users have to create a json object of this format and attach it to
|
||||
// the data field of their WebSocket message to interact with the server.
|
||||
export type WebSocketRequest = AddOrderRequest | RemoveOrderRequest | GetStatsRequest;
|
||||
|
||||
export interface AddOrderRequest {
|
||||
id: number;
|
||||
jsonrpc: string;
|
||||
method: OrderWatcherMethod.AddOrder;
|
||||
params: { signedOrder: SignedOrder };
|
||||
}
|
||||
|
||||
export interface RemoveOrderRequest {
|
||||
id: number;
|
||||
jsonrpc: string;
|
||||
method: OrderWatcherMethod.RemoveOrder;
|
||||
params: { orderHash: string };
|
||||
}
|
||||
|
||||
export interface GetStatsRequest {
|
||||
id: number;
|
||||
jsonrpc: string;
|
||||
method: OrderWatcherMethod.GetStats;
|
||||
}
|
||||
|
||||
// Users should expect a json object of this format in the data field
|
||||
// of the WebSocket messages that the server sends out.
|
||||
export type WebSocketResponse = SuccessfulWebSocketResponse | ErrorWebSocketResponse;
|
||||
|
||||
export interface SuccessfulWebSocketResponse {
|
||||
id: number;
|
||||
jsonrpc: string;
|
||||
method: OrderWatcherMethod;
|
||||
result: OrderState | GetStatsResult | undefined; // result is undefined for ADD_ORDER and REMOVE_ORDER
|
||||
}
|
||||
|
||||
export interface ErrorWebSocketResponse {
|
||||
id: number | null;
|
||||
jsonrpc: string;
|
||||
method: null;
|
||||
error: JSONRPCError;
|
||||
}
|
||||
|
||||
export interface JSONRPCError {
|
||||
code: number;
|
||||
message: string;
|
||||
data?: string | object;
|
||||
}
|
||||
|
||||
export interface GetStatsResult {
|
||||
orderCount: number;
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user