UNPKG

hardhat

Version:

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

161 lines 7.09 kB
import { HardhatError } from "@nomicfoundation/hardhat-errors"; import { ensureError } from "@nomicfoundation/hardhat-utils/error"; import { sleep, isObject } from "@nomicfoundation/hardhat-utils/lang"; import { getDispatcher, getProxyUrl, isValidUrl, postJsonRequest, shouldUseProxy, ConnectionRefusedError, RequestTimeoutError, ResponseStatusCodeError, } from "@nomicfoundation/hardhat-utils/request"; import { EDR_NETWORK_REVERT_SNAPSHOT_EVENT } from "../../constants.js"; import { getHardhatVersion } from "../../utils/package.js"; import { BaseProvider } from "./base-provider.js"; import { getJsonRpcRequest, isFailedJsonRpcResponse, parseJsonRpcResponse, } from "./json-rpc.js"; import { ProviderError, LimitExceededError, UnknownError, } from "./provider-errors.js"; const TOO_MANY_REQUEST_STATUS = 429; const MAX_RETRIES = 6; const MAX_RETRY_WAIT_TIME_SECONDS = 5; export class HttpProvider extends BaseProvider { #url; #networkName; #extraHeaders; #jsonRpcRequestWrapper; #dispatcher; #nextRequestId = 1; /** * Creates a new instance of `HttpProvider`. */ static async create({ url, networkName, extraHeaders = {}, timeout, jsonRpcRequestWrapper, testDispatcher, }) { if (!isValidUrl(url)) { throw new HardhatError(HardhatError.ERRORS.CORE.NETWORK.INVALID_URL, { value: url, }); } const dispatcher = testDispatcher ?? (await getHttpDispatcher(url, timeout)); const httpProvider = new HttpProvider(url, networkName, extraHeaders, dispatcher, jsonRpcRequestWrapper); return httpProvider; } /** * @private * * This constructor is intended for internal use only. * Use the static method {@link HttpProvider.create} to create an instance of * `HttpProvider`. */ constructor(url, networkName, extraHeaders, dispatcher, jsonRpcRequestWrapper) { super(); this.#url = url; this.#networkName = networkName; this.#extraHeaders = extraHeaders; this.#dispatcher = dispatcher; this.#jsonRpcRequestWrapper = jsonRpcRequestWrapper; } async request(requestArguments) { if (this.#dispatcher === undefined) { throw new HardhatError(HardhatError.ERRORS.CORE.NETWORK.PROVIDER_CLOSED); } const { method, params } = requestArguments; const jsonRpcRequest = getJsonRpcRequest(this.#nextRequestId++, method, params); let jsonRpcResponse; if (this.#jsonRpcRequestWrapper !== undefined) { jsonRpcResponse = await this.#jsonRpcRequestWrapper(jsonRpcRequest, (request) => this.#fetchJsonRpcResponse(request)); } else { jsonRpcResponse = await this.#fetchJsonRpcResponse(jsonRpcRequest); } if (isFailedJsonRpcResponse(jsonRpcResponse)) { const error = new ProviderError(jsonRpcResponse.error.message, jsonRpcResponse.error.code); error.data = jsonRpcResponse.error.data; // eslint-disable-next-line no-restricted-syntax -- allow throwing ProviderError throw error; } if (jsonRpcRequest.method === "evm_revert") { this.emit(EDR_NETWORK_REVERT_SNAPSHOT_EVENT); } return jsonRpcResponse.result; } async close() { if (this.#dispatcher !== undefined) { // See https://github.com/nodejs/undici/discussions/3522#discussioncomment-10498734 await this.#dispatcher.close(); this.#dispatcher = undefined; } } async #fetchJsonRpcResponse(jsonRpcRequest, retryCount = 0) { const requestOptions = { extraHeaders: { "User-Agent": `Hardhat ${await getHardhatVersion()}`, ...this.#extraHeaders, }, }; let response; try { response = await postJsonRequest(this.#url, jsonRpcRequest, requestOptions, this.#dispatcher); } catch (e) { ensureError(e); if (e instanceof ConnectionRefusedError) { throw new HardhatError(HardhatError.ERRORS.CORE.NETWORK.CONNECTION_REFUSED, { network: this.#networkName }, e); } if (e instanceof RequestTimeoutError) { throw new HardhatError(HardhatError.ERRORS.CORE.NETWORK.NETWORK_TIMEOUT, e); } /** * Nodes can have a rate limit mechanism to avoid abuse. This logic checks * if the response indicates a rate limit has been reached and retries the * request after the specified time. */ if (e instanceof ResponseStatusCodeError && e.statusCode === TOO_MANY_REQUEST_STATUS) { const retryAfterHeader = isObject(e.headers) && typeof e.headers["retry-after"] === "string" ? e.headers["retry-after"] : undefined; const retryAfterSeconds = this.#getRetryAfterSeconds(retryAfterHeader, retryCount); if (this.#shouldRetryRequest(retryAfterSeconds, retryCount)) { return this.#retry(jsonRpcRequest, retryAfterSeconds, retryCount); } // eslint-disable-next-line no-restricted-syntax -- allow throwing ProviderError throw new LimitExceededError(undefined, e); } // eslint-disable-next-line no-restricted-syntax -- allow throwing ProviderError throw new UnknownError(e.message, e); } return parseJsonRpcResponse(await response.body.text()); } #getRetryAfterSeconds(retryAfterHeader, retryCount) { const parsedRetryAfter = parseInt(`${retryAfterHeader}`, 10); if (isNaN(parsedRetryAfter)) { // use an exponential backoff if the retry-after header can't be parsed return Math.min(2 ** retryCount, MAX_RETRY_WAIT_TIME_SECONDS); } return parsedRetryAfter; } #shouldRetryRequest(retryAfterSeconds, retryCount) { if (retryCount > MAX_RETRIES) { return false; } if (retryAfterSeconds > MAX_RETRY_WAIT_TIME_SECONDS) { return false; } return true; } async #retry(request, retryAfterSeconds, retryCount) { await sleep(retryAfterSeconds); return this.#fetchJsonRpcResponse(request, retryCount + 1); } } /** * Gets either a pool or proxy dispatcher depending on the URL and the * proxy configuration. This function is used internally by * `HttpProvider.create` and should not be used directly. */ export async function getHttpDispatcher(url, timeout) { let dispatcher; const proxyUrl = shouldUseProxy(url) ? getProxyUrl(url) : undefined; if (proxyUrl !== undefined) { dispatcher = await getDispatcher(url, { proxy: proxyUrl, timeout, }); } else { dispatcher = await getDispatcher(url, { pool: true, timeout }); } return dispatcher; } //# sourceMappingURL=http-provider.js.map