UNPKG

hardhat

Version:

Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.

225 lines 8.03 kB
import { ensureError } from "@nomicfoundation/hardhat-utils/error"; import { isObject } from "@nomicfoundation/hardhat-utils/lang"; import { isJsonRpcRequest, isJsonRpcResponse, isSuccessfulJsonRpcResponse, } from "../../network-manager/json-rpc.js"; import { InternalError, InvalidJsonInputError, InvalidRequestError, ProviderError, } from "../../network-manager/provider-errors.js"; export class JsonRpcHandler { #provider; constructor(provider) { this.#provider = provider; } handleHttp = async (req, res) => { this.#setCorsHeaders(res); if (req.method === "OPTIONS") { this.#sendEmptyResponse(res); return; } let jsonHttpRequest; try { jsonHttpRequest = await _readJsonHttpRequest(req); } catch (error) { ensureError(error); this.#sendResponse(res, _handleError(error)); return; } // NOTE: EthereumProvider currently doesn't support batch requests. Thus, // the following code block could be safely removed. if (Array.isArray(jsonHttpRequest)) { const responses = await Promise.all(jsonHttpRequest.map((singleReq) => this.#handleRequest(singleReq))); this.#sendResponse(res, responses); return; } const rpcResp = await this.#handleRequest(jsonHttpRequest); this.#sendResponse(res, rpcResp); }; handleWs = async (ws) => { const subscriptions = []; let isClosed = false; const listener = (payload) => { // 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_subscription", params: payload, })); } catch (error) { ensureError(error); _handleError(error); } }; // Handle eth_subscribe notifications. this.#provider.addListener("notification", listener); ws.on("message", async (msg) => { let rpcReq; let rpcResp; try { rpcReq = _readWsRequest(msg); rpcResp = Array.isArray(rpcReq) ? await Promise.all(rpcReq.map((req) => this.#handleWsRequest(req, subscriptions))) : await this.#handleWsRequest(rpcReq, subscriptions); } catch (error) { ensureError(error); rpcResp = _handleError(error); } 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.request({ method: "eth_unsubscribe", params: [subscriptionId], }); }); }); }; #sendEmptyResponse(res) { res.writeHead(200); res.end(); } #setCorsHeaders(res) { 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", "*"); } #sendResponse(res, rpcResp) { res.statusCode = 200; res.setHeader("Content-Type", "application/json"); res.end(JSON.stringify(rpcResp)); } async #handleRequest(payload) { if (!isObject(payload)) { return _handleError(new InvalidRequestError()); } const maybeReq = { ...payload, params: payload.params ?? [], }; if (!isJsonRpcRequest(maybeReq)) { return _handleError(new InvalidRequestError()); } const rpcReq = maybeReq; let rpcResp; try { const result = await this.#provider.request({ method: rpcReq.method, params: rpcReq.params, }); rpcResp = { jsonrpc: "2.0", id: rpcReq.id, result, }; } catch (error) { ensureError(error); rpcResp = _handleError(error); } // Validate the RPC response. if (!isJsonRpcResponse(rpcResp)) { // Malformed response coming from the provider, report to user as an internal error. rpcResp = _handleError(new InternalError()); } rpcResp.id = rpcReq.id !== undefined ? rpcReq.id : null; return rpcResp; } async #handleWsRequest(rpcReq, subscriptions) { const 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" && isSuccessfulJsonRpcResponse(rpcResp) && typeof rpcResp.result === "string") { subscriptions.push(rpcResp.result); } return rpcResp; } } const _readJsonHttpRequest = async (req) => { let json; try { const bytes = []; for await (const chunk of req) { bytes.push(...chunk); } const text = new TextDecoder("utf-8").decode(new Uint8Array(bytes)); json = JSON.parse(text); } catch (error) { if (error instanceof Error) { // eslint-disable-next-line no-restricted-syntax -- Malformed JSON-RPC request received, report to user as a json input error. throw new InvalidJsonInputError(`Parse error: ${error.message}`); } throw error; } return json; }; const _readWsRequest = (msg) => { let json; try { json = JSON.parse(msg); } catch (error) { if (error instanceof Error) { // eslint-disable-next-line no-restricted-syntax -- Malformed JSON-RPC request received, report to user as a json input error. throw new InvalidJsonInputError(`Parse error: ${error.message}`); } throw error; } return json; }; const _handleError = (error) => { // In case of non-hardhat error, treat it as internal and associate the appropriate error code. if (!ProviderError.isProviderError(error)) { error = new InternalError(undefined, error); } const response = { jsonrpc: "2.0", id: null, error: { code: "code" in error && typeof error.code === "number" ? error.code : InternalError.CODE, message: error.message, data: { message: error.message, txHash: extractTxHash(error), data: extractReturnData(error), }, }, }; return response; }; function extractTxHash(error) { if ("transactionHash" in error && typeof error.transactionHash === "string") { return error.transactionHash; } if ("data" in error && isObject(error.data) && typeof error.data.transactionHash === "string") { return error.data.transactionHash; } } function extractReturnData(error) { if (!("data" in error)) { return undefined; } if (typeof error.data === "string") { return error.data; } if (isObject(error.data) && typeof error.data.data === "string") { return error.data.data; } } //# sourceMappingURL=handler.js.map