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
JavaScript
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