UNPKG

hardhat

Version:

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

302 lines (256 loc) 9.18 kB
import type * as Undici from "undici"; import { EventEmitter } from "events"; import { EIP1193Provider, RequestArguments } from "../../../types"; import { HARDHAT_NETWORK_RESET_EVENT, HARDHAT_NETWORK_REVERT_SNAPSHOT_EVENT, } from "../../constants"; import { FailedJsonRpcResponse, JsonRpcRequest, JsonRpcResponse, parseJsonResponse, SuccessfulJsonRpcResponse, } from "../../util/jsonrpc"; import { getHardhatVersion } from "../../util/packageInfo"; import { HardhatError } from "../errors"; import { ERRORS } from "../errors-list"; import { shouldUseProxy } from "../../util/proxy"; import { ProviderError } from "./errors"; export function isErrorResponse( response: any ): response is FailedJsonRpcResponse { return typeof response.error !== "undefined"; } const MAX_RETRIES = 6; const MAX_RETRY_AWAIT_SECONDS = 5; const TOO_MANY_REQUEST_STATUS = 429; const hardhatVersion = getHardhatVersion(); export class HttpProvider extends EventEmitter implements EIP1193Provider { private _nextRequestId = 1; private _dispatcher: Undici.Dispatcher; private _path: string; private _authHeader: string | undefined; constructor( private readonly _url: string, private readonly _networkName: string, private readonly _extraHeaders: { [name: string]: string } = {}, private readonly _timeout = 20000, client: Undici.Dispatcher | undefined = undefined ) { super(); const { Pool, ProxyAgent } = require("undici") as typeof Undici; if (this._url.trim().length === 0) { throw new HardhatError(ERRORS.NETWORK.EMPTY_URL, { value: this._url, }); } const url = new URL(this._url); this._path = url.pathname; this._authHeader = url.username === "" ? undefined : `Basic ${Buffer.from( `${url.username}:${url.password}`, "utf-8" ).toString("base64")}`; try { this._dispatcher = client ?? new Pool(url.origin); if (process.env.http_proxy !== undefined && shouldUseProxy(url.origin)) { this._dispatcher = new ProxyAgent(process.env.http_proxy); } } catch (e) { if (e instanceof TypeError && e.message === "Invalid URL") { e.message += ` ${url.origin}`; } // eslint-disable-next-line @nomicfoundation/hardhat-internal-rules/only-hardhat-error throw e; } } public get url(): string { return this._url; } public async request(args: RequestArguments): Promise<unknown> { const jsonRpcRequest = this._getJsonRpcRequest( args.method, args.params as any[] ); const jsonRpcResponse = await this._fetchJsonRpcResponse(jsonRpcRequest); if (isErrorResponse(jsonRpcResponse)) { const error = new ProviderError( jsonRpcResponse.error.message, jsonRpcResponse.error.code ); error.data = jsonRpcResponse.error.data; // eslint-disable-next-line @nomicfoundation/hardhat-internal-rules/only-hardhat-error throw error; } if (args.method === "hardhat_reset") { this.emit(HARDHAT_NETWORK_RESET_EVENT); } if (args.method === "evm_revert") { this.emit(HARDHAT_NETWORK_REVERT_SNAPSHOT_EVENT); } return jsonRpcResponse.result; } /** * Sends a batch of requests. Fails if any of them fails. */ public async sendBatch( batch: Array<{ method: string; params: any[] }> ): Promise<any[]> { // We create the errors here to capture the stack traces at this point, // the async call that follows would probably loose of the stack trace const stackSavingError = new ProviderError("HttpProviderError", -1); // we need this to sort the responses const idToIndexMap: Record<string, number> = {}; const requests = batch.map((r, i) => { const jsonRpcRequest = this._getJsonRpcRequest(r.method, r.params); idToIndexMap[jsonRpcRequest.id] = i; return jsonRpcRequest; }); const jsonRpcResponses = await this._fetchJsonRpcResponse(requests); for (const response of jsonRpcResponses) { if (isErrorResponse(response)) { const error = new ProviderError( response.error.message, response.error.code, stackSavingError ); error.data = response.error.data; // eslint-disable-next-line @nomicfoundation/hardhat-internal-rules/only-hardhat-error throw error; } } // We already know that it has this type, but TS can't infer it. const responses = jsonRpcResponses as SuccessfulJsonRpcResponse[]; // we use the id to sort the responses so that they match the order of the requests const sortedResponses = responses .map( (response) => [idToIndexMap[response.id], response.result] as [number, any] ) .sort(([indexA], [indexB]) => indexA - indexB) .map(([, result]) => result); return sortedResponses; } private async _fetchJsonRpcResponse( request: JsonRpcRequest, retryNumber?: number ): Promise<JsonRpcResponse>; private async _fetchJsonRpcResponse( request: JsonRpcRequest[], retryNumber?: number ): Promise<JsonRpcResponse[]>; private async _fetchJsonRpcResponse( request: JsonRpcRequest | JsonRpcRequest[], retryNumber?: number ): Promise<JsonRpcResponse | JsonRpcResponse[]>; private async _fetchJsonRpcResponse( request: JsonRpcRequest | JsonRpcRequest[], retryNumber = 0 ): Promise<JsonRpcResponse | JsonRpcResponse[]> { const { request: sendRequest } = await import("undici"); const url = new URL(this._url); const headers: { [name: string]: string } = { "Content-Type": "application/json", "User-Agent": `hardhat ${hardhatVersion}`, ...this._extraHeaders, }; if (this._authHeader !== undefined) { headers.Authorization = this._authHeader; } try { const response = await sendRequest(url, { dispatcher: this._dispatcher, method: "POST", body: JSON.stringify(request), maxRedirections: 10, headersTimeout: process.env.DO_NOT_SET_THIS_ENV_VAR____IS_HARDHAT_CI !== undefined ? 0 : this._timeout, headers, }); if (this._isRateLimitResponse(response)) { // "The Fetch Standard allows users to skip consuming the response body // by relying on garbage collection to release connection resources. // Undici does not do the same. Therefore, it is important to always // either consume or cancel the response body." // https://undici.nodejs.org/#/?id=garbage-collection // It's not clear how to "cancel", so we'll just consume: await response.body.text(); const seconds = this._getRetryAfterSeconds(response, retryNumber); if (seconds !== undefined && this._shouldRetry(retryNumber, seconds)) { return await this._retry(request, seconds, retryNumber); } // eslint-disable-next-line @nomicfoundation/hardhat-internal-rules/only-hardhat-error throw new ProviderError( `Too Many Requests error received from ${url.hostname}`, -32005 // Limit exceeded according to EIP1474 ); } return parseJsonResponse(await response.body.text()); } catch (error: any) { if (error.code === "ECONNREFUSED") { throw new HardhatError( ERRORS.NETWORK.NODE_IS_NOT_RUNNING, { network: this._networkName }, error ); } if (error.type === "request-timeout") { throw new HardhatError(ERRORS.NETWORK.NETWORK_TIMEOUT, {}, error); } // eslint-disable-next-line @nomicfoundation/hardhat-internal-rules/only-hardhat-error throw error; } } private async _retry( request: JsonRpcRequest | JsonRpcRequest[], seconds: number, retryNumber: number ) { await new Promise((resolve) => setTimeout(resolve, 1000 * seconds)); return this._fetchJsonRpcResponse(request, retryNumber + 1); } private _getJsonRpcRequest( method: string, params: any[] = [] ): JsonRpcRequest { return { jsonrpc: "2.0", method, params, id: this._nextRequestId++, }; } private _shouldRetry(retryNumber: number, retryAfterSeconds: number) { if (retryNumber > MAX_RETRIES) { return false; } if (retryAfterSeconds > MAX_RETRY_AWAIT_SECONDS) { return false; } return true; } private _isRateLimitResponse(response: Undici.Dispatcher.ResponseData) { return response.statusCode === TOO_MANY_REQUEST_STATUS; } private _getRetryAfterSeconds( response: Undici.Dispatcher.ResponseData, retryNumber: number ): number | undefined { const header = response.headers["retry-after"]; if (header === undefined || header === null || Array.isArray(header)) { // if the response doesn't have a retry-after header, we do // an exponential backoff return Math.min(2 ** retryNumber, MAX_RETRY_AWAIT_SECONDS); } const parsed = parseInt(header, 10); if (isNaN(parsed)) { return undefined; } return parsed; } }