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 orderFillRequestsSchema from '../schemas/order_fill_requests_schema.json';
|
||||||
import * as orderHashSchema from '../schemas/order_hash_schema.json';
|
import * as orderHashSchema from '../schemas/order_hash_schema.json';
|
||||||
import * as orderSchema from '../schemas/order_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 orderBookRequestSchema from '../schemas/orderbook_request_schema.json';
|
||||||
import * as ordersRequestOptsSchema from '../schemas/orders_request_opts_schema.json';
|
import * as ordersRequestOptsSchema from '../schemas/orders_request_opts_schema.json';
|
||||||
import * as ordersSchema from '../schemas/orders_schema.json';
|
import * as ordersSchema from '../schemas/orders_schema.json';
|
||||||
@@ -66,6 +68,8 @@ export const schemas = {
|
|||||||
jsNumber,
|
jsNumber,
|
||||||
requestOptsSchema,
|
requestOptsSchema,
|
||||||
pagedRequestOptsSchema,
|
pagedRequestOptsSchema,
|
||||||
|
orderWatcherWebSocketRequestSchema,
|
||||||
|
orderWatcherWebSocketUtf8MessageSchema,
|
||||||
ordersRequestOptsSchema,
|
ordersRequestOptsSchema,
|
||||||
orderBookRequestSchema,
|
orderBookRequestSchema,
|
||||||
orderConfigRequestSchema,
|
orderConfigRequestSchema,
|
||||||
|
|||||||
@@ -23,6 +23,8 @@
|
|||||||
"./schemas/order_schema.json",
|
"./schemas/order_schema.json",
|
||||||
"./schemas/signed_order_schema.json",
|
"./schemas/signed_order_schema.json",
|
||||||
"./schemas/orders_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/paginated_collection_schema.json",
|
||||||
"./schemas/relayer_api_asset_data_pairs_response_schema.json",
|
"./schemas/relayer_api_asset_data_pairs_response_schema.json",
|
||||||
"./schemas/relayer_api_asset_data_pairs_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).
|
#### 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
|
## Installation
|
||||||
|
|
||||||
**Install**
|
**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
|
## 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.
|
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",
|
"ethereum-types": "^1.1.4",
|
||||||
"ethereumjs-blockstream": "6.0.0",
|
"ethereumjs-blockstream": "6.0.0",
|
||||||
"ethers": "~4.0.4",
|
"ethers": "~4.0.4",
|
||||||
"lodash": "^4.17.5"
|
"lodash": "^4.17.5",
|
||||||
|
"websocket": "^1.0.25"
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export { OrderWatcher } from './order_watcher/order_watcher';
|
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 { ExpirationWatcher } from './order_watcher/expiration_watcher';
|
||||||
|
|
||||||
export {
|
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';
|
import { LogEntryEvent } from 'ethereum-types';
|
||||||
|
|
||||||
export enum OrderWatcherError {
|
export enum OrderWatcherError {
|
||||||
@@ -31,3 +31,67 @@ export enum InternalOrderWatcherError {
|
|||||||
ZrxNotInTokenRegistry = 'ZRX_NOT_IN_TOKEN_REGISTRY',
|
ZrxNotInTokenRegistry = 'ZRX_NOT_IN_TOKEN_REGISTRY',
|
||||||
WethNotInTokenRegistry = 'WETH_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