@nomiclabs/buidler
Version:
Buidler is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.
242 lines (197 loc) • 6.38 kB
text/typescript
import debug from "debug";
import { IncomingMessage, ServerResponse } from "http";
import getRawBody from "raw-body";
import WebSocket from "ws";
import { EthereumProvider } from "../../../types";
import {
isSuccessfulJsonResponse,
isValidJsonRequest,
isValidJsonResponse,
JsonRpcRequest,
JsonRpcResponse,
} from "../../util/jsonrpc";
import {
BuidlerEVMProviderError,
InternalError,
InvalidJsonInputError,
InvalidRequestError,
} from "../provider/errors";
// tslint:disable only-buidler-error
const log = debug("buidler:core:buidler-evm:jsonrpc");
export default class JsonRpcHandler {
private _provider: EthereumProvider;
constructor(provider: EthereumProvider) {
this._provider = provider;
}
public handleHttp = async (req: IncomingMessage, res: ServerResponse) => {
this._setCorsHeaders(res);
if (req.method === "OPTIONS") {
this._sendEmptyResponse(res);
return;
}
let jsonHttpRequest: any;
try {
jsonHttpRequest = await _readJsonHttpRequest(req);
} catch (error) {
this._sendResponse(res, _handleError(error));
return;
}
if (Array.isArray(jsonHttpRequest)) {
const responses = await Promise.all(
jsonHttpRequest.map((singleReq: any) =>
this._handleSingleRequest(singleReq)
)
);
this._sendResponse(res, responses);
return;
}
const rpcResp = await this._handleSingleRequest(jsonHttpRequest);
this._sendResponse(res, rpcResp);
};
public handleWs = async (ws: WebSocket) => {
const subscriptions: string[] = [];
let isClosed = false;
const listener = (payload: { subscription: string; result: any }) => {
// Don't attempt to send a message to the websocket if we already know it is closed,
// or the current websocket connection isn't interested in the particular subscription.
if (isClosed || subscriptions.includes(payload.subscription)) {
return;
}
try {
ws.send(
JSON.stringify({
jsonrpc: "2.0",
method: "eth_subscribe",
params: payload,
})
);
} catch (error) {
_handleError(error);
}
};
// Handle eth_subscribe notifications.
this._provider.addListener("notification", listener);
ws.on("message", async (msg) => {
let rpcReq: JsonRpcRequest | undefined;
let rpcResp: JsonRpcResponse | undefined;
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.id);
}
} 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));
});
ws.on("close", () => {
// Remove eth_subscribe listener.
this._provider.removeListener("notification", listener);
// Clear any active subscriptions for the closed websocket connection.
isClosed = true;
subscriptions.forEach(async (subscriptionId) => {
await this._provider.send("eth_unsubscribe", [subscriptionId]);
});
});
};
private _sendEmptyResponse(res: ServerResponse) {
res.writeHead(200);
res.end();
}
private _setCorsHeaders(res: ServerResponse) {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Request-Method", "*");
res.setHeader("Access-Control-Allow-Methods", "OPTIONS, GET");
res.setHeader("Access-Control-Allow-Headers", "*");
}
private _sendResponse(
res: ServerResponse,
rpcResp: JsonRpcResponse | JsonRpcResponse[]
) {
res.statusCode = 200;
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify(rpcResp));
}
private async _handleSingleRequest(req: any): Promise<JsonRpcResponse> {
if (!isValidJsonRequest(req)) {
return _handleError(new InvalidRequestError("Invalid request"));
}
const rpcReq: JsonRpcRequest = req;
let rpcResp: JsonRpcResponse | undefined;
try {
rpcResp = await this._handleRequest(rpcReq);
} 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 !== undefined ? rpcReq.id : null;
}
return rpcResp;
}
private _handleRequest = async (
req: JsonRpcRequest
): Promise<JsonRpcResponse> => {
const result = await this._provider.send(req.method, req.params);
return {
jsonrpc: "2.0",
id: req.id,
result,
};
};
}
const _readJsonHttpRequest = async (req: IncomingMessage): Promise<any> => {
let json;
try {
const buf = await getRawBody(req);
const text = buf.toString();
json = JSON.parse(text);
} catch (error) {
throw new InvalidJsonInputError(`Parse error: ${error.message}`);
}
return json;
};
const _readWsRequest = (msg: string): JsonRpcRequest => {
let json: any;
try {
json = JSON.parse(msg);
} catch (error) {
throw new InvalidJsonInputError(`Parse error: ${error.message}`);
}
return json;
};
const _handleError = (error: any): JsonRpcResponse => {
// In case of non-buidler error, treat it as internal and associate the appropriate error code.
if (!BuidlerEVMProviderError.isBuidlerEVMProviderError(error)) {
error = new InternalError(error.message);
}
return {
jsonrpc: "2.0",
id: null,
error: {
code: error.code,
message: error.message,
},
};
};