hardhat
Version:
Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.
299 lines (257 loc) • 8.03 kB
text/typescript
import type { JsonRpcRequestWrapperFunction } from "./network-manager.js";
import type {
JsonRpcRequest,
JsonRpcResponse,
RequestArguments,
SuccessfulJsonRpcResponse,
} from "../../../types/providers.js";
import type {
Dispatcher,
HttpResponse,
RequestOptions,
} from "@nomicfoundation/hardhat-utils/request";
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;
interface HttpProviderConfig {
url: string;
networkName: string;
extraHeaders?: Record<string, string>;
timeout: number;
jsonRpcRequestWrapper?: JsonRpcRequestWrapperFunction;
testDispatcher?: Dispatcher;
}
export class HttpProvider extends BaseProvider {
readonly #url: string;
readonly #networkName: string;
readonly #extraHeaders: Readonly<Record<string, string>>;
readonly #jsonRpcRequestWrapper?: JsonRpcRequestWrapperFunction;
#dispatcher: Dispatcher | undefined;
#nextRequestId = 1;
/**
* Creates a new instance of `HttpProvider`.
*/
public static async create({
url,
networkName,
extraHeaders = {},
timeout,
jsonRpcRequestWrapper,
testDispatcher,
}: HttpProviderConfig): Promise<HttpProvider> {
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`.
*/
private constructor(
url: string,
networkName: string,
extraHeaders: Record<string, string>,
dispatcher: Dispatcher,
jsonRpcRequestWrapper?: JsonRpcRequestWrapperFunction,
) {
super();
this.#url = url;
this.#networkName = networkName;
this.#extraHeaders = extraHeaders;
this.#dispatcher = dispatcher;
this.#jsonRpcRequestWrapper = jsonRpcRequestWrapper;
}
public async request(
requestArguments: RequestArguments,
): Promise<SuccessfulJsonRpcResponse["result"]> {
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: 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;
}
public async close(): Promise<void> {
if (this.#dispatcher !== undefined) {
// See https://github.com/nodejs/undici/discussions/3522#discussioncomment-10498734
await this.#dispatcher.close();
this.#dispatcher = undefined;
}
}
async #fetchJsonRpcResponse(
jsonRpcRequest: JsonRpcRequest,
retryCount = 0,
): Promise<JsonRpcResponse> {
const requestOptions: RequestOptions = {
extraHeaders: {
"User-Agent": `Hardhat ${await getHardhatVersion()}`,
...this.#extraHeaders,
},
};
let response: HttpResponse;
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: string | undefined,
retryCount: number,
) {
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: number, retryCount: number) {
if (retryCount > MAX_RETRIES) {
return false;
}
if (retryAfterSeconds > MAX_RETRY_WAIT_TIME_SECONDS) {
return false;
}
return true;
}
async #retry(
request: JsonRpcRequest,
retryAfterSeconds: number,
retryCount: number,
) {
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: string,
timeout?: number,
): Promise<Dispatcher> {
let dispatcher: 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;
}