hardhat
Version:
Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.
521 lines (461 loc) • 17.5 kB
text/typescript
import type { SolidityStackTrace } from "./stack-traces/solidity-stack-trace.js";
import type { CoverageConfig } from "./types/coverage.js";
import type { LoggerConfig } from "./types/logger.js";
import type {
ChainDescriptorsConfig,
EdrNetworkConfig,
EdrNetworkHDAccountsConfig,
} from "../../../../types/config.js";
import type {
EthSubscription,
JsonRpcRequest,
JsonRpcResponse,
RequestArguments,
SuccessfulJsonRpcResponse,
} from "../../../../types/providers.js";
import type { RequireField } from "../../../../types/utils.js";
import type { DefaultHDAccountsConfigParams } from "../accounts/constants.js";
import type { JsonRpcRequestWrapperFunction } from "../network-manager.js";
import type {
SubscriptionEvent,
Response,
Provider,
ProviderConfig,
TracingConfigWithBuffers,
AccountOverride,
GasReportConfig,
} from "@nomicfoundation/edr";
import {
opGenesisState,
opHardforkFromString,
l1GenesisState,
l1HardforkFromString,
ContractDecoder,
} from "@nomicfoundation/edr";
import {
assertHardhatInvariant,
HardhatError,
} from "@nomicfoundation/hardhat-errors";
import { toSeconds } from "@nomicfoundation/hardhat-utils/date";
import { ensureError } from "@nomicfoundation/hardhat-utils/error";
import { numberToHexString } from "@nomicfoundation/hardhat-utils/hex";
import { deepEqual } from "@nomicfoundation/hardhat-utils/lang";
import debug from "debug";
import { hexToBytes } from "ethereum-cryptography/utils";
import { addr } from "micro-eth-signer";
import { sendErrorTelemetry } from "../../../cli/telemetry/sentry/reporter.js";
import {
EDR_NETWORK_REVERT_SNAPSHOT_EVENT,
OPTIMISM_CHAIN_TYPE,
} from "../../../constants.js";
import { hardhatChainTypeToEdrChainType } from "../../../edr/chain-type.js";
import { getGlobalEdrContext } from "../../../edr/context.js";
import { DEFAULT_HD_ACCOUNTS_CONFIG_PARAMS } from "../accounts/constants.js";
import { BaseProvider } from "../base-provider.js";
import { getJsonRpcRequest, isFailedJsonRpcResponse } from "../json-rpc.js";
import {
InvalidArgumentsError,
ProviderError,
UnknownError,
} from "../provider-errors.js";
import { EdrProviderStackTraceGenerationError } from "./stack-traces/stack-trace-generation-errors.js";
import { createSolidityErrorWithStackTrace } from "./stack-traces/stack-trace-solidity-errors.js";
import {
isDebugTraceResult,
isEdrProviderErrorData,
} from "./type-validation.js";
import { clientVersion } from "./utils/client-version.js";
import { ConsoleLogger } from "./utils/console-logger.js";
import {
edrRpcDebugTraceToHardhat,
hardhatMiningIntervalToEdrMiningInterval,
hardhatMempoolOrderToEdrMineOrdering,
hardhatHardforkToEdrSpecId,
hardhatAccountsToEdrOwnedAccounts,
hardhatForkingConfigToEdrForkConfig,
} from "./utils/convert-to-edr.js";
import { printLine, replaceLastLine } from "./utils/logger.js";
const log = debug("hardhat:core:hardhat-network:provider");
export const EDR_NETWORK_DEFAULT_COINBASE =
"0xc014ba5ec014ba5ec014ba5ec014ba5ec014ba5e";
interface EdrNetworkDefaultHDAccountsConfigParams
extends DefaultHDAccountsConfigParams {
mnemonic: string;
accountsBalance: bigint;
}
export const EDR_NETWORK_MNEMONIC =
"test test test test test test test test test test test junk";
export const DEFAULT_EDR_NETWORK_BALANCE = 10000000000000000000000n;
export const DEFAULT_EDR_NETWORK_HD_ACCOUNTS_CONFIG_PARAMS: EdrNetworkDefaultHDAccountsConfigParams =
{
...DEFAULT_HD_ACCOUNTS_CONFIG_PARAMS,
mnemonic: EDR_NETWORK_MNEMONIC,
accountsBalance: DEFAULT_EDR_NETWORK_BALANCE,
};
export async function isDefaultEdrNetworkHDAccountsConfig(
accounts: EdrNetworkHDAccountsConfig,
): Promise<boolean> {
return deepEqual(
{
...accounts,
mnemonic: await accounts.mnemonic.get(),
passphrase: await accounts.passphrase.get(),
},
DEFAULT_EDR_NETWORK_HD_ACCOUNTS_CONFIG_PARAMS,
);
}
export const EDR_NETWORK_DEFAULT_PRIVATE_KEYS: string[] = [
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
"0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d",
"0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a",
"0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6",
"0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a",
"0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba",
"0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e",
"0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356",
"0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97",
"0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6",
"0xf214f2b2cd398c806f84e317254e0f0b801d0643303237d97a22a48e01628897",
"0x701b615bbdfb9de65240bc28bd21bbc0d996645a3dd57e7b12bc2bdf6f192c82",
"0xa267530f49f8280200edf313ee7af6b827f2a8bce2897751d06a843f644967b1",
"0x47c99abed3324a2707c28affff1267e45918ec8c3f20b8aa892e8b065d2942dd",
"0xc526ee95bf44d8fc405a158bb884d9d1238d99f0612e9f33d006bb0789009aaa",
"0x8166f546bab6da521a8369cab06c5d2b9e46670292d85c875ee9ec20e84ffb61",
"0xea6c44ac03bff858b476bba40716402b03e41b8e97e276d1baec7c37d42484a0",
"0x689af8efa8c651a91ad287602527f3af2fe9f6501a7ac4b061667b5a93e037fd",
"0xde9be858da4a475276426320d5e9262ecfc3ba460bfac56360bfa6c4c28b4ee0",
"0xdf57089febbacf7ba0bc227dafbffa9fc08a93fdc68e1e42411a14efcf23656e",
];
interface EdrProviderConfig {
chainDescriptors: ChainDescriptorsConfig;
networkConfig: RequireField<EdrNetworkConfig, "chainType">;
loggerConfig?: LoggerConfig;
tracingConfig?: TracingConfigWithBuffers;
jsonRpcRequestWrapper?: JsonRpcRequestWrapperFunction;
coverageConfig?: CoverageConfig;
gasReportConfig?: GasReportConfig;
}
export class EdrProvider extends BaseProvider {
readonly #jsonRpcRequestWrapper?: JsonRpcRequestWrapperFunction;
#provider: Provider | undefined;
#nextRequestId = 1;
/**
* Creates a new instance of `EdrProvider`.
*/
public static async create({
chainDescriptors,
networkConfig,
loggerConfig = { enabled: false },
tracingConfig = {},
jsonRpcRequestWrapper,
coverageConfig,
gasReportConfig,
}: EdrProviderConfig): Promise<EdrProvider> {
const printLineFn = loggerConfig.printLineFn ?? printLine;
const replaceLastLineFn = loggerConfig.replaceLastLineFn ?? replaceLastLine;
const providerConfig = await getProviderConfig(
networkConfig,
coverageConfig,
gasReportConfig,
chainDescriptors,
);
let edrProvider: EdrProvider;
// We need to catch errors here, as the provider creation can panic unexpectedly,
// and we want to make sure such a crash is propagated as a ProviderError.
try {
const contractDecoder = ContractDecoder.withContracts(tracingConfig);
const context = await getGlobalEdrContext();
const provider = await context.createProvider(
hardhatChainTypeToEdrChainType(networkConfig.chainType),
providerConfig,
{
enable: loggerConfig.enabled || networkConfig.loggingEnabled,
decodeConsoleLogInputsCallback: (inputs: ArrayBuffer[]) => {
return ConsoleLogger.getDecodedLogs(
inputs.map((input) => {
return Buffer.from(input);
}),
);
},
printLineCallback: (message: string, replace: boolean) => {
if (replace) {
replaceLastLineFn(message);
} else {
printLineFn(message);
}
},
},
{
subscriptionCallback: (event: SubscriptionEvent) => {
edrProvider.onSubscriptionEvent(event);
},
},
contractDecoder,
);
edrProvider = new EdrProvider(provider, jsonRpcRequestWrapper);
} catch (error) {
ensureError(error);
// eslint-disable-next-line no-restricted-syntax -- allow throwing UnknownError
throw new UnknownError(error.message, error);
}
return edrProvider;
}
/**
* @private
*
* This constructor is intended for internal use only.
* Use the static method {@link EdrProvider.create} to create an instance of
* `EdrProvider`.
*/
private constructor(
provider: Provider,
jsonRpcRequestWrapper?: JsonRpcRequestWrapperFunction,
) {
super();
this.#provider = provider;
this.#jsonRpcRequestWrapper = jsonRpcRequestWrapper;
}
public async request(
requestArguments: RequestArguments,
): Promise<SuccessfulJsonRpcResponse["result"]> {
if (this.#provider === 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,
this.#handleRequest.bind(this),
);
} else {
jsonRpcResponse = await this.#handleRequest(jsonRpcRequest);
}
// this can only happen if a wrapper doesn't call the default
// behavior as the default throws on FailedJsonRpcResponse
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);
}
// Override EDR version string with Hardhat version string with EDR backend,
// e.g. `HardhatNetwork/2.19.0/@nomicfoundation/edr/0.2.0-dev`
if (jsonRpcRequest.method === "web3_clientVersion") {
assertHardhatInvariant(
typeof jsonRpcResponse.result === "string",
"Invalid client version response",
);
return clientVersion(jsonRpcResponse.result);
} else if (
jsonRpcRequest.method === "debug_traceTransaction" ||
jsonRpcRequest.method === "debug_traceCall"
) {
assertHardhatInvariant(
isDebugTraceResult(jsonRpcResponse.result),
"Invalid debug trace response",
);
return edrRpcDebugTraceToHardhat(jsonRpcResponse.result);
} else {
return jsonRpcResponse.result;
}
}
public async close(): Promise<void> {
// Clear the provider reference to help with garbage collection
this.#provider = undefined;
}
public async addCompilationResult(
solcVersion: string,
compilerInput: any,
compilerOutput: any,
): Promise<void> {
if (this.#provider === undefined) {
throw new HardhatError(HardhatError.ERRORS.CORE.NETWORK.PROVIDER_CLOSED);
}
await this.#provider.addCompilationResult(
solcVersion,
compilerInput,
compilerOutput,
);
}
async #handleEdrResponse(
edrResponse: Response,
): Promise<SuccessfulJsonRpcResponse> {
let jsonRpcResponse: JsonRpcResponse;
if (typeof edrResponse.data === "string") {
jsonRpcResponse = JSON.parse(edrResponse.data);
} else {
jsonRpcResponse = edrResponse.data;
}
if (isFailedJsonRpcResponse(jsonRpcResponse)) {
const responseError = jsonRpcResponse.error;
let error;
let stackTrace: SolidityStackTrace | null = null;
try {
stackTrace = edrResponse.stackTrace();
} catch (e) {
if (e instanceof Error) {
await sendErrorTelemetry(new EdrProviderStackTraceGenerationError(e));
}
log("Failed to get stack trace: %O", e);
}
if (stackTrace !== null) {
// If we have a stack trace, we know that the json rpc response data
// is an object with the data and transactionHash fields
assertHardhatInvariant(
isEdrProviderErrorData(responseError.data),
"Invalid error data",
);
error = createSolidityErrorWithStackTrace(
responseError.message,
stackTrace,
responseError.data.data,
responseError.data.transactionHash,
);
} else {
error =
responseError.code === InvalidArgumentsError.CODE
? new InvalidArgumentsError(responseError.message)
: new ProviderError(responseError.message, responseError.code);
error.data = responseError.data;
}
/* eslint-disable-next-line no-restricted-syntax -- we may throw
non-Hardaht errors inside of an EthereumProvider */
throw error;
}
return jsonRpcResponse;
}
public onSubscriptionEvent(event: SubscriptionEvent): void {
const subscription = numberToHexString(event.filterId);
const results = Array.isArray(event.result) ? event.result : [event.result];
for (const result of results) {
this.#emitLegacySubscriptionEvent(subscription, result);
this.#emitEip1193SubscriptionEvent(subscription, result);
}
}
#emitLegacySubscriptionEvent(subscription: string, result: unknown) {
this.emit("notification", {
subscription,
result,
});
}
#emitEip1193SubscriptionEvent(subscription: string, result: unknown) {
const message: EthSubscription = {
type: "eth_subscription",
data: {
subscription,
result,
},
};
this.emit("message", message);
}
async #handleRequest(request: JsonRpcRequest): Promise<JsonRpcResponse> {
assertHardhatInvariant(
this.#provider !== undefined,
"The provider is not defined",
);
const stringifiedArgs = JSON.stringify(request);
let edrResponse: Response;
// We need to catch errors here, as the provider creation can panic unexpectedly,
// and we want to make sure such a crash is propagated as a ProviderError.
try {
edrResponse = await this.#provider.handleRequest(stringifiedArgs);
} catch (error) {
ensureError(error);
// eslint-disable-next-line no-restricted-syntax -- allow throwing UnknownError
throw new UnknownError(error.message, error);
}
return this.#handleEdrResponse(edrResponse);
}
}
export async function getProviderConfig(
networkConfig: RequireField<EdrNetworkConfig, "chainType">,
coverageConfig: CoverageConfig | undefined,
gasReportConfig: GasReportConfig | undefined,
chainDescriptors: ChainDescriptorsConfig,
): Promise<ProviderConfig> {
const specId = hardhatHardforkToEdrSpecId(
networkConfig.hardfork,
networkConfig.chainType,
);
const ownedAccounts = await hardhatAccountsToEdrOwnedAccounts(
networkConfig.accounts,
);
const genesisState: Map<Uint8Array, AccountOverride> = new Map(
ownedAccounts.map(({ secretKey, balance }) => {
const address = hexToBytes(addr.fromPrivateKey(secretKey));
const accountOverride: AccountOverride = {
address,
balance: BigInt(balance),
code: new Uint8Array(), // Empty account code, removing potential delegation code when forking
};
return [address, accountOverride];
}),
);
const chainGenesisState =
networkConfig.forking !== undefined
? [] // TODO: Add support for overriding remote fork state when the local fork is different
: networkConfig.chainType === OPTIMISM_CHAIN_TYPE
? opGenesisState(opHardforkFromString(specId))
: l1GenesisState(l1HardforkFromString(specId));
for (const account of chainGenesisState) {
const existingOverride = genesisState.get(account.address);
if (existingOverride !== undefined) {
// Favor the genesis state specified by the user
account.balance = account.balance ?? existingOverride.balance;
account.nonce = account.nonce ?? existingOverride.nonce;
account.code = account.code ?? existingOverride.code;
account.storage = account.storage ?? existingOverride.storage;
} else {
genesisState.set(account.address, account);
}
}
return {
allowBlocksWithSameTimestamp: networkConfig.allowBlocksWithSameTimestamp,
allowUnlimitedContractSize: networkConfig.allowUnlimitedContractSize,
bailOnCallFailure: networkConfig.throwOnCallFailures,
bailOnTransactionFailure: networkConfig.throwOnTransactionFailures,
blockGasLimit: networkConfig.blockGasLimit,
chainId: BigInt(networkConfig.chainId),
coinbase: networkConfig.coinbase,
fork: await hardhatForkingConfigToEdrForkConfig(
networkConfig.forking,
chainDescriptors,
networkConfig.chainType,
),
genesisState: Array.from(genesisState.values()),
hardfork: specId,
initialBaseFeePerGas: networkConfig.initialBaseFeePerGas,
initialDate: BigInt(toSeconds(networkConfig.initialDate)),
minGasPrice: networkConfig.minGasPrice,
mining: {
autoMine: networkConfig.mining.auto,
interval: hardhatMiningIntervalToEdrMiningInterval(
networkConfig.mining.interval,
),
memPool: {
order: hardhatMempoolOrderToEdrMineOrdering(
networkConfig.mining.mempool.order,
),
},
},
networkId: BigInt(networkConfig.networkId),
observability: {
codeCoverage: coverageConfig,
gasReport: gasReportConfig,
},
ownedAccounts: ownedAccounts.map((account) => account.secretKey),
precompileOverrides: [],
};
}