From 69546655e2f821865cb626dea17be1e6e13aa243 Mon Sep 17 00:00:00 2001 From: Alex Kit Date: Fri, 13 Jan 2023 14:25:29 +0100 Subject: [PATCH] fix #1324 (WebSocket BatchRequest) supports Batch requests via WebSockets --- .changeset/plenty-comics-bow.md | 5 ++ .../hardhat-network/jsonrpc/handler.ts | 56 ++++++++-------- .../provider/modules/eth/websocket.ts | 65 +++++++++++++++++++ 3 files changed, 98 insertions(+), 28 deletions(-) create mode 100644 .changeset/plenty-comics-bow.md diff --git a/.changeset/plenty-comics-bow.md b/.changeset/plenty-comics-bow.md new file mode 100644 index 0000000000..f424dca148 --- /dev/null +++ b/.changeset/plenty-comics-bow.md @@ -0,0 +1,5 @@ +--- +"hardhat": patch +--- + +Add BatchRequest support for WebSocket server diff --git a/packages/hardhat-core/src/internal/hardhat-network/jsonrpc/handler.ts b/packages/hardhat-core/src/internal/hardhat-network/jsonrpc/handler.ts index 2d38345f54..681c43591a 100644 --- a/packages/hardhat-core/src/internal/hardhat-network/jsonrpc/handler.ts +++ b/packages/hardhat-core/src/internal/hardhat-network/jsonrpc/handler.ts @@ -82,40 +82,23 @@ export class JsonRpcHandler { this._provider.addListener("notification", listener); ws.on("message", async (msg) => { - let rpcReq: JsonRpcRequest | undefined; - let rpcResp: JsonRpcResponse | undefined; + let rpcReq: JsonRpcRequest | JsonRpcRequest[]; + let rpcResp: JsonRpcResponse | JsonRpcResponse[]; try { rpcReq = _readWsRequest(msg as string); - if (!isValidJsonRequest(rpcReq)) { - throw new InvalidRequestError("Invalid request"); - } - - rpcResp = await this._handleRequest(rpcReq); - - // If eth_subscribe was successful, keep track of the subscription id, - // so we can cleanup on websocket close. - if ( - rpcReq.method === "eth_subscribe" && - isSuccessfulJsonResponse(rpcResp) - ) { - subscriptions.push(rpcResp.result); - } + rpcResp = Array.isArray(rpcReq) + ? await Promise.all( + rpcReq.map((req) => + this._handleSingleWsRequest(req, subscriptions) + ) + ) + : await this._handleSingleWsRequest(rpcReq, subscriptions); } catch (error) { rpcResp = _handleError(error); } - // Validate the RPC response. - if (!isValidJsonResponse(rpcResp)) { - // Malformed response coming from the provider, report to user as an internal error. - rpcResp = _handleError(new InternalError("Internal error")); - } - - if (rpcReq !== undefined) { - rpcResp.id = rpcReq.id; - } - ws.send(JSON.stringify(rpcResp)); }); @@ -155,7 +138,9 @@ export class JsonRpcHandler { res.end(JSON.stringify(rpcResp)); } - private async _handleSingleRequest(req: any): Promise { + private async _handleSingleRequest( + req: JsonRpcRequest + ): Promise { if (!isValidJsonRequest(req)) { return _handleError(new InvalidRequestError("Invalid request")); } @@ -181,6 +166,21 @@ export class JsonRpcHandler { return rpcResp; } + private async _handleSingleWsRequest( + rpcReq: JsonRpcRequest, + subscriptions: string[] + ) { + const rpcResp = await this._handleSingleRequest(rpcReq); + // If eth_subscribe was successful, keep track of the subscription id, + // so we can cleanup on websocket close. + if ( + rpcReq.method === "eth_subscribe" && + isSuccessfulJsonResponse(rpcResp) + ) { + subscriptions.push(rpcResp.result); + } + return rpcResp; + } private _handleRequest = async ( req: JsonRpcRequest @@ -218,7 +218,7 @@ const _readJsonHttpRequest = async (req: IncomingMessage): Promise => { return json; }; -const _readWsRequest = (msg: string): JsonRpcRequest => { +const _readWsRequest = (msg: string): JsonRpcRequest | JsonRpcRequest[] => { let json: any; try { json = JSON.parse(msg); diff --git a/packages/hardhat-core/test/internal/hardhat-network/provider/modules/eth/websocket.ts b/packages/hardhat-core/test/internal/hardhat-network/provider/modules/eth/websocket.ts index 3276de92f3..f8caefac42 100644 --- a/packages/hardhat-core/test/internal/hardhat-network/provider/modules/eth/websocket.ts +++ b/packages/hardhat-core/test/internal/hardhat-network/provider/modules/eth/websocket.ts @@ -118,6 +118,34 @@ describe("Eth module", function () { assert.equal(newLogEvent.params.subscription, subscription); }); + it("Supports single and batched requests", async function () { + const { result: accounts } = await sendMethod("eth_accounts"); + const [acc1, acc2] = accounts; + const balancesAcc1Resp = await sendJson({ + jsonrpc: "2.0", + id: Math.random(), + method: "eth_getBalance", + params: [acc1], + }); + const balancesResp = await sendJson([ + { + jsonrpc: "2.0", + id: Math.random(), + method: "eth_getBalance", + params: [acc1], + }, + { + jsonrpc: "2.0", + id: Math.random(), + method: "eth_getBalance", + params: [acc2], + }, + ]); + + assert.match(balancesAcc1Resp.result, /^0x[\dA-F]+$/i); + assert.equal(balancesAcc1Resp.result, balancesResp[0].result); + }); + async function subscribeTo(event: string, ...extraParams: any[]) { const subscriptionPromise = new Promise((resolve) => { const listener: any = (message: any) => { @@ -177,6 +205,43 @@ describe("Eth module", function () { return result; } + async function sendJson< + TBody extends TReq | TReq[], + TReq extends { + jsonrpc: "2.0"; + id: number; + method: string; + params: any[]; + }, + TResp extends { + jsonrpc: "2.0"; + id: number; + result: any; + } + >(body: TBody): Promise { + const resultPromise = new Promise((resolve) => { + const listener: any = (message: any) => { + const parsedMessage = JSON.parse(message.toString()); + const receivedId = Array.isArray(parsedMessage) + ? parsedMessage[0]?.id + : parsedMessage.id; + const sentId = Array.isArray(body) ? body[0]?.id : body.id; + + if (receivedId === sentId) { + ws.removeListener("message", listener); + resolve(parsedMessage); + } + }; + + ws.on("message", listener); + }); + + ws.send(JSON.stringify(body)); + const result = await resultPromise; + + return result; + } + /** * Send `method` with `params` and get the first message that corresponds to * the given subscription.