From 3ccca1a93dcea0b3139d25385f3a27bb99342c58 Mon Sep 17 00:00:00 2001 From: ChrisD <18092467+ChristopherDedominici@users.noreply.github.com> Date: Wed, 27 Nov 2024 11:41:50 +0100 Subject: [PATCH 01/17] remove reworked-files --- .../accounts/automatic-sender-provider.ts | 32 - .../accounts/derive-private-keys.ts | 63 -- .../accounts/fixed-sender-provider.ts | 21 - .../accounts/hd-wallet.ts | 33 - .../accounts/local-accounts.ts | 332 -------- .../accounts/sender.ts | 51 -- .../accounts/types.ts | 9 - .../chain-id/chain-id-validator.ts | 41 - .../chain-id/chain-id.ts | 58 -- .../gas-properties/automatic-gas-price.ts | 217 ----- .../gas-properties/automatic-gas.ts | 37 - .../gas-properties/fixed-gas-price.ts | 35 - .../gas-properties/fixed-gas.ts | 29 - .../multiplied-gas-estimation.ts | 87 -- .../json-rpc-request-modifier.ts | 272 ------- .../json-rpc-request-modifiers/utils.ts | 10 - .../e2e/accounts/automatic-sender.ts | 48 -- .../e2e/accounts/fixed-sender.ts | 41 - .../e2e/accounts/hd-wallets.ts | 81 -- .../e2e/accounts/local-accounts.ts | 134 ---- .../e2e/chain-id.ts | 133 --- .../e2e/gas/automatic-gas-price.ts | 55 -- .../e2e/gas/automatic-gas.ts | 55 -- .../e2e/gas/fixed-gas-price.ts | 45 -- .../e2e/gas/fixed-gas.ts | 55 -- .../ethereum-mocked-provider.ts | 77 -- .../json-rpc-request-modifiers/hooks-mock.ts | 70 -- .../unit/accounts/automatic-sender.ts | 71 -- .../unit/accounts/fixed-sender.ts | 60 -- .../unit/accounts/hd-wallet.ts | 144 ---- .../unit/accounts/local-accounts.ts | 754 ------------------ .../unit/chain-id/chain-id.ts | 81 -- .../gas-properties/automatic-gas-price.ts | 362 --------- .../unit/gas-properties/automatic-gas.ts | 123 --- .../unit/gas-properties/fixed-gas-price.ts | 68 -- .../unit/gas-properties/fixed-gas.ts | 66 -- 36 files changed, 3850 deletions(-) delete mode 100644 v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/accounts/automatic-sender-provider.ts delete mode 100644 v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/accounts/derive-private-keys.ts delete mode 100644 v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/accounts/fixed-sender-provider.ts delete mode 100644 v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/accounts/hd-wallet.ts delete mode 100644 v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/accounts/local-accounts.ts delete mode 100644 v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/accounts/sender.ts delete mode 100644 v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/accounts/types.ts delete mode 100644 v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/chain-id/chain-id-validator.ts delete mode 100644 v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/chain-id/chain-id.ts delete mode 100644 v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/gas-properties/automatic-gas-price.ts delete mode 100644 v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/gas-properties/automatic-gas.ts delete mode 100644 v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/gas-properties/fixed-gas-price.ts delete mode 100644 v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/gas-properties/fixed-gas.ts delete mode 100644 v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/gas-properties/multiplied-gas-estimation.ts delete mode 100644 v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/json-rpc-request-modifier.ts delete mode 100644 v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/utils.ts delete mode 100644 v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/e2e/accounts/automatic-sender.ts delete mode 100644 v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/e2e/accounts/fixed-sender.ts delete mode 100644 v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/e2e/accounts/hd-wallets.ts delete mode 100644 v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/e2e/accounts/local-accounts.ts delete mode 100644 v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/e2e/chain-id.ts delete mode 100644 v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/e2e/gas/automatic-gas-price.ts delete mode 100644 v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/e2e/gas/automatic-gas.ts delete mode 100644 v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/e2e/gas/fixed-gas-price.ts delete mode 100644 v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/e2e/gas/fixed-gas.ts delete mode 100644 v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/ethereum-mocked-provider.ts delete mode 100644 v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/hooks-mock.ts delete mode 100644 v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/unit/accounts/automatic-sender.ts delete mode 100644 v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/unit/accounts/fixed-sender.ts delete mode 100644 v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/unit/accounts/hd-wallet.ts delete mode 100644 v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/unit/accounts/local-accounts.ts delete mode 100644 v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/unit/chain-id/chain-id.ts delete mode 100644 v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/unit/gas-properties/automatic-gas-price.ts delete mode 100644 v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/unit/gas-properties/automatic-gas.ts delete mode 100644 v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/unit/gas-properties/fixed-gas-price.ts delete mode 100644 v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/unit/gas-properties/fixed-gas.ts diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/accounts/automatic-sender-provider.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/accounts/automatic-sender-provider.ts deleted file mode 100644 index e4137f18e9..0000000000 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/accounts/automatic-sender-provider.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { assertHardhatInvariant } from "@ignored/hardhat-vnext-errors"; - -import { Sender } from "./sender.js"; - -/** - * This class automatically retrieves and caches the first available account from the connected provider. - * It overrides the getSender method of the base class to request the list of accounts if the first account has not been fetched yet, - * ensuring dynamic selection of the sender for all JSON-RPC requests without requiring manual input. - */ -export class AutomaticSender extends Sender { - #alreadyFetchedAccounts = false; - #firstAccount: string | undefined; - - protected async getSender(): Promise { - if (this.#alreadyFetchedAccounts === false) { - const accounts = await this.provider.request({ - method: "eth_accounts", - }); - - // TODO: This shouldn't be an exception but a failed JSON response! - assertHardhatInvariant( - Array.isArray(accounts), - "eth_accounts response should be an array", - ); - - this.#firstAccount = accounts[0]; - this.#alreadyFetchedAccounts = true; - } - - return this.#firstAccount; - } -} diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/accounts/derive-private-keys.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/accounts/derive-private-keys.ts deleted file mode 100644 index 9e9b1b5928..0000000000 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/accounts/derive-private-keys.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { HardhatError } from "@ignored/hardhat-vnext-errors"; -import { mnemonicToSeedSync } from "ethereum-cryptography/bip39"; -import { HDKey } from "ethereum-cryptography/hdkey"; - -const HD_PATH_REGEX = /^m(:?\/\d+'?)+\/?$/; - -export function derivePrivateKeys( - mnemonic: string, - hdpath: string, - initialIndex: number, - count: number, - passphrase: string, -): Buffer[] { - if (hdpath.match(HD_PATH_REGEX) === null) { - throw new HardhatError(HardhatError.ERRORS.NETWORK.INVALID_HD_PATH, { - path: hdpath, - }); - } - - if (!hdpath.endsWith("/")) { - hdpath += "/"; - } - - const privateKeys: Buffer[] = []; - - for (let i = initialIndex; i < initialIndex + count; i++) { - const privateKey = deriveKeyFromMnemonicAndPath( - mnemonic, - hdpath + i.toString(), - passphrase, - ); - - if (privateKey === undefined) { - throw new HardhatError(HardhatError.ERRORS.NETWORK.CANT_DERIVE_KEY, { - mnemonic, - path: hdpath, - }); - } - - privateKeys.push(privateKey); - } - - return privateKeys; -} - -function deriveKeyFromMnemonicAndPath( - mnemonic: string, - hdPath: string, - passphrase: string, -): Buffer | undefined { - // NOTE: If mnemonic has space or newline at the beginning or end, it will be trimmed. - // This is because mnemonic containing them may generate different private keys. - const trimmedMnemonic = mnemonic.trim(); - - const seed = mnemonicToSeedSync(trimmedMnemonic, passphrase); - - const masterKey = HDKey.fromMasterSeed(seed); - const derived = masterKey.derive(hdPath); - - return derived.privateKey === null - ? undefined - : Buffer.from(derived.privateKey); -} diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/accounts/fixed-sender-provider.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/accounts/fixed-sender-provider.ts deleted file mode 100644 index 6680bfac34..0000000000 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/accounts/fixed-sender-provider.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { EthereumProvider } from "../../../../../types/providers.js"; - -import { Sender } from "./sender.js"; - -/** - * This class provides a fixed sender address for transactions. - * It overrides the getSender method of the base class to always return the sender address specified during instantiation, - * ensuring that all JSON-RPC requests use this fixed sender. - */ -export class FixedSender extends Sender { - readonly #sender: string; - - constructor(provider: EthereumProvider, sender: string) { - super(provider); - this.#sender = sender; - } - - protected async getSender(): Promise { - return this.#sender; - } -} diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/accounts/hd-wallet.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/accounts/hd-wallet.ts deleted file mode 100644 index ab1ccd7f08..0000000000 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/accounts/hd-wallet.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { EthereumProvider } from "../../../../../types/providers.js"; - -import { bytesToHexString } from "@ignored/hardhat-vnext-utils/hex"; - -import { derivePrivateKeys } from "./derive-private-keys.js"; -import { LocalAccounts } from "./local-accounts.js"; - -export class HDWallet extends LocalAccounts { - constructor( - provider: EthereumProvider, - mnemonic: string, - hdpath: string = "m/44'/60'/0'/0/", - initialIndex: number = 0, - count: number = 10, - passphrase: string = "", - ) { - // NOTE: If mnemonic has space or newline at the beginning or end, it will be trimmed. - // This is because mnemonic containing them may generate different private keys. - const trimmedMnemonic = mnemonic.trim(); - - const privateKeys = derivePrivateKeys( - trimmedMnemonic, - hdpath, - initialIndex, - count, - passphrase, - ); - - const privateKeysAsHex = privateKeys.map((pk) => bytesToHexString(pk)); - - super(provider, privateKeysAsHex); - } -} diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/accounts/local-accounts.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/accounts/local-accounts.ts deleted file mode 100644 index 28325b94c7..0000000000 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/accounts/local-accounts.ts +++ /dev/null @@ -1,332 +0,0 @@ -import type { - EthereumProvider, - JsonRpcRequest, - JsonRpcResponse, -} from "../../../../../types/providers.js"; -import type { RpcTransactionRequest } from "../../rpc/types/tx-request.js"; - -import { - assertHardhatInvariant, - HardhatError, -} from "@ignored/hardhat-vnext-errors"; -import { toBigInt } from "@ignored/hardhat-vnext-utils/bigint"; -import { - bytesToHexString, - hexStringToBigInt, - hexStringToBytes, -} from "@ignored/hardhat-vnext-utils/hex"; -import { addr, Transaction } from "micro-eth-signer"; -import * as typed from "micro-eth-signer/typed-data"; -import { signTyped } from "micro-eth-signer/typed-data"; - -import { getRequestParams } from "../../json-rpc.js"; -import { rpcAddress } from "../../rpc/types/address.js"; -import { rpcAny } from "../../rpc/types/any.js"; -import { rpcData } from "../../rpc/types/data.js"; -import { rpcTransactionRequest } from "../../rpc/types/tx-request.js"; -import { validateParams } from "../../rpc/validate-params.js"; -import { ChainId } from "../chain-id/chain-id.js"; - -export class LocalAccounts extends ChainId { - readonly #addressToPrivateKey: Map = new Map(); - - constructor( - provider: EthereumProvider, - localAccountsHexPrivateKeys: string[], - ) { - super(provider); - - this.#initializePrivateKeys(localAccountsHexPrivateKeys); - } - - public async resolveRequest( - jsonRpcRequest: JsonRpcRequest, - ): Promise { - if ( - jsonRpcRequest.method === "eth_accounts" || - jsonRpcRequest.method === "eth_requestAccounts" - ) { - return this.#createJsonRpcResponse(jsonRpcRequest.id, [ - ...this.#addressToPrivateKey.keys(), - ]); - } - - const params = getRequestParams(jsonRpcRequest); - - if (jsonRpcRequest.method === "eth_sign") { - if (params.length > 0) { - const [address, data] = validateParams(params, rpcAddress, rpcData); - - if (address !== undefined) { - if (data === undefined) { - throw new HardhatError( - HardhatError.ERRORS.NETWORK.ETHSIGN_MISSING_DATA_PARAM, - ); - } - - const privateKey = this.#getPrivateKeyForAddress(address); - return this.#createJsonRpcResponse( - jsonRpcRequest.id, - typed.personal.sign(data, privateKey), - ); - } - } - } - - if (jsonRpcRequest.method === "personal_sign") { - if (params.length > 0) { - const [data, address] = validateParams(params, rpcData, rpcAddress); - - if (data !== undefined) { - if (address === undefined) { - throw new HardhatError( - HardhatError.ERRORS.NETWORK.PERSONALSIGN_MISSING_ADDRESS_PARAM, - ); - } - - const privateKey = this.#getPrivateKeyForAddress(address); - return this.#createJsonRpcResponse( - jsonRpcRequest.id, - typed.personal.sign(data, privateKey), - ); - } - } - } - - if (jsonRpcRequest.method === "eth_signTypedData_v4") { - const [address, data] = validateParams(params, rpcAddress, rpcAny); - - if (data === undefined) { - throw new HardhatError( - HardhatError.ERRORS.NETWORK.ETHSIGN_MISSING_DATA_PARAM, - ); - } - - let typedMessage = data; - if (typeof data === "string") { - try { - typedMessage = JSON.parse(data); - } catch { - throw new HardhatError( - HardhatError.ERRORS.NETWORK.ETHSIGN_TYPED_DATA_V4_INVALID_DATA_PARAM, - ); - } - } - - // if we don't manage the address, the method is forwarded - const privateKey = this.#getPrivateKeyForAddressOrNull(address); - if (privateKey !== null) { - return this.#createJsonRpcResponse( - jsonRpcRequest.id, - signTyped(typedMessage, privateKey), - ); - } - } - - return null; - } - - public async modifyRequest(jsonRpcRequest: JsonRpcRequest): Promise { - const params = getRequestParams(jsonRpcRequest); - - if (jsonRpcRequest.method === "eth_sendTransaction" && params.length > 0) { - const [txRequest] = validateParams(params, rpcTransactionRequest); - - if (txRequest.gas === undefined) { - throw new HardhatError( - HardhatError.ERRORS.NETWORK.MISSING_TX_PARAM_TO_SIGN_LOCALLY, - { param: "gas" }, - ); - } - - if (txRequest.from === undefined) { - throw new HardhatError( - HardhatError.ERRORS.NETWORK.MISSING_TX_PARAM_TO_SIGN_LOCALLY, - { param: "from" }, - ); - } - - const hasGasPrice = txRequest.gasPrice !== undefined; - const hasEip1559Fields = - txRequest.maxFeePerGas !== undefined || - txRequest.maxPriorityFeePerGas !== undefined; - - if (!hasGasPrice && !hasEip1559Fields) { - throw new HardhatError( - HardhatError.ERRORS.NETWORK.MISSING_FEE_PRICE_FIELDS, - ); - } - - if (hasGasPrice && hasEip1559Fields) { - throw new HardhatError( - HardhatError.ERRORS.NETWORK.INCOMPATIBLE_FEE_PRICE_FIELDS, - ); - } - - if (hasEip1559Fields && txRequest.maxFeePerGas === undefined) { - throw new HardhatError( - HardhatError.ERRORS.NETWORK.MISSING_TX_PARAM_TO_SIGN_LOCALLY, - { param: "maxFeePerGas" }, - ); - } - - if (hasEip1559Fields && txRequest.maxPriorityFeePerGas === undefined) { - throw new HardhatError( - HardhatError.ERRORS.NETWORK.MISSING_TX_PARAM_TO_SIGN_LOCALLY, - { param: "maxPriorityFeePerGas" }, - ); - } - - if (txRequest.nonce === undefined) { - txRequest.nonce = await this.#getNonce(txRequest.from); - } - - const privateKey = this.#getPrivateKeyForAddress(txRequest.from); - - const chainId = await this.getChainId(); - - const rawTransaction = await this.#getSignedTransaction( - txRequest, - chainId, - privateKey, - ); - - jsonRpcRequest.method = "eth_sendRawTransaction"; - jsonRpcRequest.params = [bytesToHexString(rawTransaction)]; - } - } - - #initializePrivateKeys(localAccountsHexPrivateKeys: string[]) { - const privateKeys: Uint8Array[] = localAccountsHexPrivateKeys.map((h) => - hexStringToBytes(h), - ); - - for (const pk of privateKeys) { - const address = addr.fromPrivateKey(pk).toLowerCase(); - this.#addressToPrivateKey.set(address, pk); - } - } - - #getPrivateKeyForAddress(address: Uint8Array): Uint8Array { - const pk = this.#addressToPrivateKey.get(bytesToHexString(address)); - if (pk === undefined) { - throw new HardhatError(HardhatError.ERRORS.NETWORK.NOT_LOCAL_ACCOUNT, { - account: bytesToHexString(address), - }); - } - - return pk; - } - - #getPrivateKeyForAddressOrNull(address: Uint8Array): Uint8Array | null { - try { - return this.#getPrivateKeyForAddress(address); - } catch { - return null; - } - } - - async #getNonce(address: Uint8Array): Promise { - const response = await this.provider.request({ - method: "eth_getTransactionCount", - params: [bytesToHexString(address), "pending"], - }); - - assertHardhatInvariant( - typeof response === "string", - "response should be a string", - ); - - return hexStringToBigInt(response); - } - - async #getSignedTransaction( - transactionRequest: RpcTransactionRequest, - chainId: number, - privateKey: Uint8Array, - ): Promise { - const txData = { - ...transactionRequest, - gasLimit: transactionRequest.gas, - }; - - const accessList = txData.accessList?.map(({ address, storageKeys }) => { - return { - address: addr.addChecksum(bytesToHexString(address)), - storageKeys: - storageKeys !== null - ? storageKeys.map((k) => bytesToHexString(k)) - : [], - }; - }); - - // TODO: Fix after the alpha release - assertHardhatInvariant( - txData.to !== undefined, - "The alpha version doesn't support deploying contracts with local accounts yet", - ); - - const checksummedAddress = addr.addChecksum( - bytesToHexString(txData.to).toLowerCase(), - ); - - assertHardhatInvariant( - txData.nonce !== undefined, - "nonce should be defined", - ); - - let transaction; - if (txData.maxFeePerGas !== undefined) { - transaction = Transaction.prepare({ - type: "eip1559", - to: checksummedAddress, - nonce: txData.nonce, - chainId: txData.chainId ?? toBigInt(chainId), - value: txData.value !== undefined ? txData.value : 0n, - data: txData.data !== undefined ? bytesToHexString(txData.data) : "", - gasLimit: txData.gasLimit, - maxFeePerGas: txData.maxFeePerGas, - maxPriorityFeePerGas: txData.maxPriorityFeePerGas, - accessList: accessList ?? [], - }); - } else if (accessList !== undefined) { - transaction = Transaction.prepare({ - type: "eip2930", - to: checksummedAddress, - nonce: txData.nonce, - chainId: txData.chainId ?? toBigInt(chainId), - value: txData.value !== undefined ? txData.value : 0n, - data: txData.data !== undefined ? bytesToHexString(txData.data) : "", - gasPrice: txData.gasPrice ?? 0n, - gasLimit: txData.gasLimit, - accessList, - }); - } else { - transaction = Transaction.prepare({ - type: "legacy", - to: checksummedAddress, - nonce: txData.nonce, - chainId: txData.chainId ?? toBigInt(chainId), - value: txData.value !== undefined ? txData.value : 0n, - data: txData.data !== undefined ? bytesToHexString(txData.data) : "", - gasPrice: txData.gasPrice ?? 0n, - gasLimit: txData.gasLimit, - }); - } - - const signedTransaction = transaction.signBy(privateKey); - - return signedTransaction.toRawBytes(); - } - - #createJsonRpcResponse( - id: number | string, - result: unknown, - ): JsonRpcResponse { - return { - jsonrpc: "2.0", - id, - result, - }; - } -} diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/accounts/sender.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/accounts/sender.ts deleted file mode 100644 index 1817e082d6..0000000000 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/accounts/sender.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { JsonRpcTransactionData } from "./types.js"; -import type { - EthereumProvider, - JsonRpcRequest, -} from "../../../../../types/providers.js"; - -import { HardhatError } from "@ignored/hardhat-vnext-errors"; - -import { getRequestParams } from "../../json-rpc.js"; - -/** - * This code defines an abstract class that modifies JSON-RPC requests. - * It checks if the request is related to transactions and ensures that the "from" field is populated with a sender account if it's missing. - * If no account is available for sending transactions, it throws an error. - * The class also provides a mechanism to retrieve the sender account, which must be implemented by subclasses. - */ -export abstract class Sender { - protected readonly provider: EthereumProvider; - - constructor(provider: EthereumProvider) { - this.provider = provider; - } - - public async modifyRequest(jsonRpcRequest: JsonRpcRequest): Promise { - const method = jsonRpcRequest.method; - const params = getRequestParams(jsonRpcRequest); - - if ( - method === "eth_sendTransaction" || - method === "eth_call" || - method === "eth_estimateGas" - ) { - // TODO: from V2 - Should we validate this type? - const tx: JsonRpcTransactionData = params[0]; - - if (tx !== undefined && tx.from === undefined) { - const senderAccount = await this.getSender(); - - if (senderAccount !== undefined) { - tx.from = senderAccount; - } else if (method === "eth_sendTransaction") { - throw new HardhatError( - HardhatError.ERRORS.NETWORK.NO_REMOTE_ACCOUNT_AVAILABLE, - ); - } - } - } - } - - protected abstract getSender(): Promise; -} diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/accounts/types.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/accounts/types.ts deleted file mode 100644 index 565d514fc6..0000000000 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/accounts/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface JsonRpcTransactionData { - from?: string; - to?: string; - gas?: string | number; - gasPrice?: string | number; - value?: string | number; - data?: string; - nonce?: string | number; -} diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/chain-id/chain-id-validator.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/chain-id/chain-id-validator.ts deleted file mode 100644 index 5a974308d6..0000000000 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/chain-id/chain-id-validator.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { EthereumProvider } from "../../../../../types/providers.js"; - -import { HardhatError } from "@ignored/hardhat-vnext-errors"; - -import { ChainId } from "./chain-id.js"; - -/** - * This class extends `ChainId` to validate that the current provider's chain ID matches - * an expected value. If the actual chain ID differs from the expected one, it throws a - * HardhatError to signal a network configuration mismatch. Once validated, further checks - * are skipped to avoid redundant validations. - */ -export class ChainIdValidator extends ChainId { - readonly #expectedChainId: number; - - #alreadyValidated = false; - readonly #chainId: number | undefined; - - constructor(provider: EthereumProvider, expectedChainId: number) { - super(provider); - this.#expectedChainId = expectedChainId; - } - - public async validate(): Promise { - if (!this.#alreadyValidated) { - const actualChainId = await this.getChainId(); - - if (actualChainId !== this.#expectedChainId) { - throw new HardhatError( - HardhatError.ERRORS.NETWORK.INVALID_GLOBAL_CHAIN_ID, - { - configChainId: this.#expectedChainId, - connectionChainId: actualChainId, - }, - ); - } - - this.#alreadyValidated = true; - } - } -} diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/chain-id/chain-id.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/chain-id/chain-id.ts deleted file mode 100644 index d7eee16b60..0000000000 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/chain-id/chain-id.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { EthereumProvider } from "../../../../../types/providers.js"; - -import { assertHardhatInvariant } from "@ignored/hardhat-vnext-errors"; -import { - hexStringToNumber, - isPrefixedHexString, -} from "@ignored/hardhat-vnext-utils/hex"; - -/** - * This class is responsible for retrieving the chain ID of the network. - * It uses the provider to fetch the chain ID via two methods: 'eth_chainId' and, - * as a fallback, 'net_version' if the first one fails. The chain ID is cached - * after being retrieved to avoid redundant requests. - */ -export abstract class ChainId { - protected readonly provider: EthereumProvider; - - #chainId: number | undefined; - - constructor(provider: EthereumProvider) { - this.provider = provider; - } - - protected async getChainId(): Promise { - if (this.#chainId === undefined) { - try { - this.#chainId = await this.getChainIdFromEthChainId(); - } catch { - // If eth_chainId fails we default to net_version - this.#chainId = await this.#getChainIdFromEthNetVersion(); - } - } - - return this.#chainId; - } - - protected async getChainIdFromEthChainId(): Promise { - const id = await this.provider.request({ - method: "eth_chainId", - }); - - assertHardhatInvariant(typeof id === "string", "id should be a string"); - - return hexStringToNumber(id); - } - - async #getChainIdFromEthNetVersion(): Promise { - const id = await this.provider.request({ - method: "net_version", - }); - - assertHardhatInvariant(typeof id === "string", "id should be a string"); - - // There's a node returning this as decimal instead of QUANTITY. - // TODO: from V2 - Document here which node does that - return isPrefixedHexString(id) ? hexStringToNumber(id) : parseInt(id, 10); - } -} diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/gas-properties/automatic-gas-price.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/gas-properties/automatic-gas-price.ts deleted file mode 100644 index 0f8f7b462f..0000000000 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/gas-properties/automatic-gas-price.ts +++ /dev/null @@ -1,217 +0,0 @@ -import type { - EthereumProvider, - RequestArguments, -} from "../../../../../types/providers.js"; - -import { assertHardhatInvariant } from "@ignored/hardhat-vnext-errors"; -import { - hexStringToBigInt, - numberToHexString, -} from "@ignored/hardhat-vnext-utils/hex"; -import { isObject } from "@ignored/hardhat-vnext-utils/lang"; - -import { getRequestParams } from "../../json-rpc.js"; - -/** - * This class automatically adjusts transaction requests to include appropriately estimated gas prices. - * It ensures that gas prices are set correctly. - */ -export class AutomaticGasPrice { - readonly #provider: EthereumProvider; - - // We pay the max base fee that can be required if the next - // EIP1559_BASE_FEE_MAX_FULL_BLOCKS_PREFERENCE are full. - public static readonly EIP1559_BASE_FEE_MAX_FULL_BLOCKS_PREFERENCE: bigint = - 3n; - - // See eth_feeHistory for an explanation of what this means - public static readonly EIP1559_REWARD_PERCENTILE = 50; - - constructor(provider: EthereumProvider) { - this.#provider = provider; - } - - #nodeHasFeeHistory?: boolean; - #nodeSupportsEIP1559?: boolean; - - public async modifyRequest(args: RequestArguments): Promise { - if (args.method !== "eth_sendTransaction") { - return; - } - - const params = getRequestParams(args); - - // TODO: from V2 - Should we validate this type? - const tx = params[0]; - - if (tx === undefined) { - return; - } - - // We don't need to do anything in these cases - if ( - tx.gasPrice !== undefined || - (tx.maxFeePerGas !== undefined && tx.maxPriorityFeePerGas !== undefined) - ) { - return; - } - - let suggestedEip1559Values = await this.#suggestEip1559FeePriceValues(); - - // eth_feeHistory failed, so we send a legacy one - if ( - tx.maxFeePerGas === undefined && - tx.maxPriorityFeePerGas === undefined && - suggestedEip1559Values === undefined - ) { - tx.gasPrice = numberToHexString(await this.#getGasPrice()); - return; - } - - // If eth_feeHistory failed, but the user still wants to send an EIP-1559 tx - // we use the gasPrice as default values. - if (suggestedEip1559Values === undefined) { - const gasPrice = await this.#getGasPrice(); - - suggestedEip1559Values = { - maxFeePerGas: gasPrice, - maxPriorityFeePerGas: gasPrice, - }; - } - - let maxFeePerGas = - tx.maxFeePerGas !== undefined - ? hexStringToBigInt(tx.maxFeePerGas) - : suggestedEip1559Values.maxFeePerGas; - - const maxPriorityFeePerGas = - tx.maxPriorityFeePerGas !== undefined - ? hexStringToBigInt(tx.maxPriorityFeePerGas) - : suggestedEip1559Values.maxPriorityFeePerGas; - - if (maxFeePerGas < maxPriorityFeePerGas) { - maxFeePerGas += maxPriorityFeePerGas; - } - - tx.maxFeePerGas = numberToHexString(maxFeePerGas); - tx.maxPriorityFeePerGas = numberToHexString(maxPriorityFeePerGas); - - return; - } - - async #getGasPrice(): Promise { - const response = await this.#provider.request({ - method: "eth_gasPrice", - }); - - assertHardhatInvariant( - typeof response === "string", - "response should return a string", - ); - - return hexStringToBigInt(response); - } - - async #suggestEip1559FeePriceValues(): Promise< - | { - maxFeePerGas: bigint; - maxPriorityFeePerGas: bigint; - } - | undefined - > { - if (this.#nodeSupportsEIP1559 === undefined) { - const block = await this.#provider.request({ - method: "eth_getBlockByNumber", - params: ["latest", false], - }); - - assertHardhatInvariant( - isObject(block), - "block should be a non null object", - ); - - this.#nodeSupportsEIP1559 = - "baseFeePerGas" in block && block.baseFeePerGas !== undefined; - } - - if ( - this.#nodeHasFeeHistory === false || - this.#nodeSupportsEIP1559 === false - ) { - return; - } - - try { - const response = await this.#provider.request({ - method: "eth_feeHistory", - params: [ - "0x1", - "latest", - [AutomaticGasPrice.EIP1559_REWARD_PERCENTILE], - ], - }); - - assertHardhatInvariant( - typeof response === "object" && - response !== null && - "baseFeePerGas" in response && - "reward" in response && - Array.isArray(response.baseFeePerGas) && - Array.isArray(response.reward), - "response should be an object with baseFeePerGas and reward properties", - ); - - let maxPriorityFeePerGas = hexStringToBigInt(response.reward[0][0]); - - if (maxPriorityFeePerGas === 0n) { - try { - const suggestedMaxPriorityFeePerGas = await this.#provider.request({ - method: "eth_maxPriorityFeePerGas", - params: [], - }); - - assertHardhatInvariant( - typeof suggestedMaxPriorityFeePerGas === "string", - "suggestedMaxPriorityFeePerGas should be a string", - ); - - maxPriorityFeePerGas = hexStringToBigInt( - suggestedMaxPriorityFeePerGas, - ); - } catch { - // if eth_maxPriorityFeePerGas does not exist, use 1 wei - maxPriorityFeePerGas = 1n; - } - } - - // If after all of these we still have a 0 wei maxPriorityFeePerGas, we - // use 1 wei. This is to improve the UX of the automatic gas price - // on chains that are very empty (i.e local testnets). This will be very - // unlikely to trigger on a live chain. - if (maxPriorityFeePerGas === 0n) { - maxPriorityFeePerGas = 1n; - } - - return { - // Each block increases the base fee by 1/8 at most, when full. - // We have the next block's base fee, so we compute a cap for the - // next N blocks here. - - maxFeePerGas: - (hexStringToBigInt(response.baseFeePerGas[1]) * - 9n ** - (AutomaticGasPrice.EIP1559_BASE_FEE_MAX_FULL_BLOCKS_PREFERENCE - - 1n)) / - 8n ** - (AutomaticGasPrice.EIP1559_BASE_FEE_MAX_FULL_BLOCKS_PREFERENCE - - 1n), - - maxPriorityFeePerGas, - }; - } catch { - this.#nodeHasFeeHistory = false; - - return undefined; - } - } -} diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/gas-properties/automatic-gas.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/gas-properties/automatic-gas.ts deleted file mode 100644 index 333155539b..0000000000 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/gas-properties/automatic-gas.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { - EthereumProvider, - RequestArguments, -} from "../../../../../types/providers.js"; - -import { getRequestParams } from "../../json-rpc.js"; - -import { MultipliedGasEstimation } from "./multiplied-gas-estimation.js"; - -export const DEFAULT_GAS_MULTIPLIER = 1; - -/** - * This class modifies transaction requests by automatically estimating the gas required, - * applying a multiplier to the estimated gas. It extends the `MultipliedGasEstimation` class - * to handle the gas estimation logic. If no gas value is provided in the transaction, - * the gas is automatically estimated and multiplied before being added to the request. - */ -export class AutomaticGas extends MultipliedGasEstimation { - constructor( - provider: EthereumProvider, - gasMultiplier: number = DEFAULT_GAS_MULTIPLIER, - ) { - super(provider, gasMultiplier); - } - - public async modifyRequest(args: RequestArguments): Promise { - if (args.method === "eth_sendTransaction") { - const params = getRequestParams(args); - - // TODO: from V2 - Should we validate this type? - const tx = params[0]; - if (tx !== undefined && tx.gas === undefined) { - tx.gas = await this.getMultipliedGasEstimation(params); - } - } - } -} diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/gas-properties/fixed-gas-price.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/gas-properties/fixed-gas-price.ts deleted file mode 100644 index 4c4d4dd86d..0000000000 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/gas-properties/fixed-gas-price.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { RequestArguments } from "../../../../../types/providers.js"; -import type { PrefixedHexString } from "@ignored/hardhat-vnext-utils/hex"; - -import { getRequestParams } from "../../json-rpc.js"; - -/** - * This class ensures that a fixed gas price is applied to transaction requests. - * For `eth_sendTransaction` requests, it sets the gasPrice field with the value provided via the class constructor, if it hasn't been specified already. - */ -export class FixedGasPrice { - readonly #gasPrice: PrefixedHexString; - - constructor(gasPrice: PrefixedHexString) { - this.#gasPrice = gasPrice; - } - - public modifyRequest(args: RequestArguments): void { - if (args.method === "eth_sendTransaction") { - const params = getRequestParams(args); - - // TODO: from V2 - Should we validate this type? - const tx = params[0]; - - // Temporary change to ignore EIP-1559 - if ( - tx !== undefined && - tx.gasPrice === undefined && - tx.maxFeePerGas === undefined && - tx.maxPriorityFeePerGas === undefined - ) { - tx.gasPrice = this.#gasPrice; - } - } - } -} diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/gas-properties/fixed-gas.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/gas-properties/fixed-gas.ts deleted file mode 100644 index b559c1b784..0000000000 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/gas-properties/fixed-gas.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { RequestArguments } from "../../../../../types/providers.js"; -import type { PrefixedHexString } from "@ignored/hardhat-vnext-utils/hex"; - -import { getRequestParams } from "../../json-rpc.js"; - -/** - * This class ensures that a fixed gas is applied to transaction requests. - * For `eth_sendTransaction` requests, it sets the gas field with the value provided via the class constructor, if it hasn't been specified already. - */ -export class FixedGas { - readonly #gas: PrefixedHexString; - - constructor(gas: PrefixedHexString) { - this.#gas = gas; - } - - public modifyRequest(args: RequestArguments): void { - if (args.method === "eth_sendTransaction") { - const params = getRequestParams(args); - - // TODO: from V2 - Should we validate this type? - const tx = params[0]; - - if (tx !== undefined && tx.gas === undefined) { - tx.gas = this.#gas; - } - } - } -} diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/gas-properties/multiplied-gas-estimation.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/gas-properties/multiplied-gas-estimation.ts deleted file mode 100644 index 1e629236ed..0000000000 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/gas-properties/multiplied-gas-estimation.ts +++ /dev/null @@ -1,87 +0,0 @@ -import type { EthereumProvider } from "../../../../../types/providers.js"; - -import { assertHardhatInvariant } from "@ignored/hardhat-vnext-errors"; -import { ensureError } from "@ignored/hardhat-vnext-utils/error"; -import { - hexStringToNumber, - numberToHexString, -} from "@ignored/hardhat-vnext-utils/hex"; - -/** - * This class handles gas estimation for transactions by applying a multiplier to the estimated gas value. - * It requests a gas estimation from the provider and multiplies it by a predefined gas multiplier, ensuring the gas does not exceed the block's gas limit. - * If an execution error occurs, the method returns the block's gas limit instead. - * The block gas limit is cached after the first retrieval to optimize performance. - */ -export abstract class MultipliedGasEstimation { - readonly #provider: EthereumProvider; - readonly #gasMultiplier: number; - - #blockGasLimit: number | undefined; - - constructor(provider: EthereumProvider, gasMultiplier: number) { - this.#provider = provider; - this.#gasMultiplier = gasMultiplier; - } - - protected async getMultipliedGasEstimation(params: any[]): Promise { - try { - const realEstimation = await this.#provider.request({ - method: "eth_estimateGas", - params, - }); - - assertHardhatInvariant( - typeof realEstimation === "string", - "realEstimation should be a string", - ); - - if (this.#gasMultiplier === 1) { - return realEstimation; - } - - const normalGas = hexStringToNumber(realEstimation); - - const gasLimit = await this.#getBlockGasLimit(); - - const multiplied = Math.floor(normalGas * this.#gasMultiplier); - - const gas = multiplied > gasLimit ? gasLimit - 1 : multiplied; - - return numberToHexString(gas); - } catch (error) { - ensureError(error); - - if (error.message.toLowerCase().includes("execution error")) { - const blockGasLimitTmp = await this.#getBlockGasLimit(); - return numberToHexString(blockGasLimitTmp); - } - - throw error; - } - } - - async #getBlockGasLimit(): Promise { - if (this.#blockGasLimit === undefined) { - const latestBlock = await this.#provider.request({ - method: "eth_getBlockByNumber", - params: ["latest", false], - }); - - assertHardhatInvariant( - typeof latestBlock === "object" && - latestBlock !== null && - "gasLimit" in latestBlock && - typeof latestBlock.gasLimit === "string", - "latestBlock should have a gasLimit", - ); - - const fetchedGasLimit = hexStringToNumber(latestBlock.gasLimit); - - // We store a lower value in case the gas limit varies slightly - this.#blockGasLimit = Math.floor(fetchedGasLimit * 0.95); - } - - return this.#blockGasLimit; - } -} diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/json-rpc-request-modifier.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/json-rpc-request-modifier.ts deleted file mode 100644 index f44baaacb5..0000000000 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/json-rpc-request-modifier.ts +++ /dev/null @@ -1,272 +0,0 @@ -import type { - ChainType, - NetworkConnection, -} from "../../../../types/network.js"; -import type { - EthereumProvider, - JsonRpcRequest, - JsonRpcResponse, -} from "../../../../types/providers.js"; -import type { - HDAccountsUserConfig, - HttpNetworkAccountsUserConfig, - NetworkConfig, -} from "@ignored/hardhat-vnext/types/config"; - -import { numberToHexString } from "@ignored/hardhat-vnext-utils/hex"; -import { deepClone } from "@ignored/hardhat-vnext-utils/lang"; - -import { AutomaticSender } from "./accounts/automatic-sender-provider.js"; -import { FixedSender } from "./accounts/fixed-sender-provider.js"; -import { HDWallet } from "./accounts/hd-wallet.js"; -import { LocalAccounts } from "./accounts/local-accounts.js"; -import { ChainIdValidator } from "./chain-id/chain-id-validator.js"; -import { AutomaticGasPrice } from "./gas-properties/automatic-gas-price.js"; -import { AutomaticGas } from "./gas-properties/automatic-gas.js"; -import { FixedGasPrice } from "./gas-properties/fixed-gas-price.js"; -import { FixedGas } from "./gas-properties/fixed-gas.js"; -import { isHttpNetworkConfig } from "./utils.js"; - -/** - * This class modifies JSON-RPC requests for transactions based on network configurations. - * It manages parameters such as gas, gas price, chain ID validation, and account details to ensure accurate transaction settings. - * Additionally, for certain "accounts" scenarios, it can return a response directly. - * Requests are cloned to prevent interference with other handlers. - */ -export class JsonRpcRequestModifier { - readonly #provider: EthereumProvider; - readonly #networkConfig: NetworkConfig; - - // accounts - #localAccounts: LocalAccounts | undefined; - #hdWallet: HDWallet | undefined; - #automaticSender: AutomaticSender | undefined; - #fixedSender: FixedSender | undefined; - - // chainId - #chainIdValidator: ChainIdValidator | undefined; - - // gas - #automaticGas: AutomaticGas | undefined; - #fixedGas: FixedGas | undefined; - - // gas price - #automaticGasPrice: AutomaticGasPrice | undefined; - #fixedGasPrice: FixedGasPrice | undefined; - - constructor(nextNetworkConnection: NetworkConnection) { - this.#provider = nextNetworkConnection.provider; - this.#networkConfig = nextNetworkConnection.networkConfig; - } - - /** - * Processes a JSON-RPC request and conditionally returns a JSON-RPC response if the request matches - * specific conditions. - * - * @param {JsonRpcRequest} jsonRpcRequest - The JSON-RPC request to be processed. - * @returns {Promise} - Returns a JSON-RPC response if the conditions are met, or null otherwise. - */ - public async getResponse( - jsonRpcRequest: JsonRpcRequest, - ): Promise { - if (isHttpNetworkConfig(this.#networkConfig)) { - const accounts = this.#networkConfig.accounts; - - if (Array.isArray(accounts)) { - if (this.#localAccounts === undefined) { - // TODO: Maybe we can avoid resolving all of them here. - const resolvedAccounts = await Promise.all( - accounts.map((acc) => acc.getHexString()), - ); - this.#localAccounts = new LocalAccounts( - this.#provider, - resolvedAccounts, - ); - } - - return this.#localAccounts.resolveRequest(jsonRpcRequest); - } else if (this.#isHDAccountsConfig(accounts)) { - if (this.#hdWallet === undefined) { - this.#hdWallet = new HDWallet( - this.#provider, - accounts.mnemonic, - accounts.path, - accounts.initialIndex, - accounts.count, - accounts.passphrase, - ); - } - - return this.#hdWallet.resolveRequest(jsonRpcRequest); - } - } - - return null; - } - - /** - * Creates a modified copy of a JSON-RPC request by cloning the original and applying changes - * based on account, gas, gas price, and chain ID configurations if the request matches - * specific conditions. - * - * @param {JsonRpcRequest} jsonRpcRequest - The JSON-RPC request to be cloned and modified. - * @returns {Promise} - A modified JSON-RPC request based on the current configurations. - */ - public async createModifiedJsonRpcRequest( - jsonRpcRequest: JsonRpcRequest, - ): Promise { - // We clone the request to avoid interfering with other hook handlers that - // might be using the original request. - // The "newJsonRpcRequest" inside this class is modified by reference. - const newJsonRpcRequest = await deepClone(jsonRpcRequest); - - await this.#validateChainIdIfNeeded(newJsonRpcRequest); - - await this.#modifyGasAndGasPriceIfNeeded(newJsonRpcRequest); - - await this.#modifyAccountsIfNeeded(newJsonRpcRequest); - - return newJsonRpcRequest; - } - - async #modifyAccountsIfNeeded(jsonRpcRequest: JsonRpcRequest): Promise { - if (isHttpNetworkConfig(this.#networkConfig)) { - const accounts = this.#networkConfig.accounts; - - if (Array.isArray(accounts)) { - if (this.#localAccounts === undefined) { - const resolvedAccounts = await Promise.all( - accounts.map((acc) => acc.getHexString()), - ); - - this.#localAccounts = new LocalAccounts( - this.#provider, - resolvedAccounts, - ); - } - - await this.#localAccounts.modifyRequest(jsonRpcRequest); - } else if (this.#isHDAccountsConfig(accounts)) { - if (this.#hdWallet === undefined) { - this.#hdWallet = new HDWallet( - this.#provider, - accounts.mnemonic, - accounts.path, - accounts.initialIndex, - accounts.count, - accounts.passphrase, - ); - } - - await this.#hdWallet.modifyRequest(jsonRpcRequest); - } - } - - if (this.#networkConfig.from !== undefined) { - if (this.#fixedSender === undefined) { - this.#fixedSender = new FixedSender( - this.#provider, - this.#networkConfig.from, - ); - } - - await this.#fixedSender.modifyRequest(jsonRpcRequest); - } else { - if (this.#automaticSender === undefined) { - this.#automaticSender = new AutomaticSender(this.#provider); - } - - await this.#automaticSender.modifyRequest(jsonRpcRequest); - } - } - - async #modifyGasAndGasPriceIfNeeded( - jsonRpcRequest: JsonRpcRequest, - ): Promise { - if ( - this.#networkConfig.gas === undefined || - this.#networkConfig.gas === "auto" - ) { - if (this.#automaticGas === undefined) { - this.#automaticGas = new AutomaticGas( - this.#provider, - this.#networkConfig.gasMultiplier, - ); - } - - await this.#automaticGas.modifyRequest(jsonRpcRequest); - } else { - if (this.#fixedGas === undefined) { - this.#fixedGas = new FixedGas( - numberToHexString(this.#networkConfig.gas), - ); - } - - this.#fixedGas.modifyRequest(jsonRpcRequest); - } - - if ( - this.#networkConfig.gasPrice === undefined || - this.#networkConfig.gasPrice === "auto" - ) { - // If you use a hook handler that signs locally, you are required to - // have all the transaction fields available, including the - // gasPrice / maxFeePerGas & maxPriorityFeePerGas. - // - // We never use those when using EDR Network, as we sign within the - // EDR Network itself. This means that we don't need to provide all the - // fields, as the missing ones will be resolved there. - // - // EDR Network handles this in a more performant way, so we don't use - // the AutomaticGasPrice for it. - if (isHttpNetworkConfig(this.#networkConfig)) { - if (this.#automaticGasPrice === undefined) { - this.#automaticGasPrice = new AutomaticGasPrice(this.#provider); - } - - await this.#automaticGasPrice.modifyRequest(jsonRpcRequest); - } - } else { - if (this.#fixedGasPrice === undefined) { - this.#fixedGasPrice = new FixedGasPrice( - numberToHexString(this.#networkConfig.gasPrice), - ); - } - - this.#fixedGasPrice.modifyRequest(jsonRpcRequest); - } - } - - async #validateChainIdIfNeeded( - jsonRpcRequest: JsonRpcRequest, - ): Promise { - // Avoid recursive behavior, as the chainIdValidator will call these methods. - // This check prevents an infinite loop of chainIdValidator calling itself repeatedly. - if ( - jsonRpcRequest.method === "eth_chainId" || - jsonRpcRequest.method === "net_version" - ) { - return; - } - - if ( - isHttpNetworkConfig(this.#networkConfig) && - this.#networkConfig.chainId !== undefined - ) { - if (this.#chainIdValidator === undefined) { - this.#chainIdValidator = new ChainIdValidator( - this.#provider, - this.#networkConfig.chainId, - ); - } - - await this.#chainIdValidator.validate(); - } - } - - #isHDAccountsConfig( - accounts?: HttpNetworkAccountsUserConfig, - ): accounts is HDAccountsUserConfig { - return accounts !== undefined && Object.keys(accounts).includes("mnemonic"); - } -} diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/utils.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/utils.ts deleted file mode 100644 index 4dbe7f4067..0000000000 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/utils.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { - HttpNetworkConfig, - NetworkConfig, -} from "@ignored/hardhat-vnext/types/config"; - -export function isHttpNetworkConfig( - netConfig: Partial, -): netConfig is HttpNetworkConfig { - return netConfig.type === "http"; -} diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/e2e/accounts/automatic-sender.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/e2e/accounts/automatic-sender.ts deleted file mode 100644 index 323496bae9..0000000000 --- a/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/e2e/accounts/automatic-sender.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { - GenericChainType, - NetworkConnection, -} from "../../../../../../../src/types/network.js"; - -import assert from "node:assert/strict"; -import { beforeEach, describe, it } from "node:test"; - -import { numberToHexString } from "@ignored/hardhat-vnext-utils/hex"; - -import { getJsonRpcRequest } from "../../../../../../../src/internal/builtin-plugins/network-manager/json-rpc.js"; -import { createMockedNetworkHre } from "../../hooks-mock.js"; - -// Test that the request and its additional sub-request (when present) -// are correctly modified in the "onRequest" hook handler. -// These tests simulate a real scenario where the user calls "await connection.provider.request(jsonRpcRequest)". -describe("e2e - AutomaticSender", () => { - let connection: NetworkConnection; - - beforeEach(async () => { - const hre = await createMockedNetworkHre( - {}, - { - eth_accounts: ["0x123006d4548a3ac17d72b372ae1e416bf65b8eaf"], - }, - ); - - connection = await hre.network.connect(); - }); - - it("should set the from value into the transaction", async () => { - const tx = { - to: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", - gas: numberToHexString(21000), - gasPrice: numberToHexString(678912), - nonce: numberToHexString(0), - value: numberToHexString(1), - }; - - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [tx]); - - const res = await connection.provider.request(jsonRpcRequest); - - assert.ok(Array.isArray(res), "res should be an array"); - - assert.equal(res[0].from, "0x123006d4548a3ac17d72b372ae1e416bf65b8eaf"); - }); -}); diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/e2e/accounts/fixed-sender.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/e2e/accounts/fixed-sender.ts deleted file mode 100644 index 78517b7960..0000000000 --- a/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/e2e/accounts/fixed-sender.ts +++ /dev/null @@ -1,41 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, it } from "node:test"; - -import { numberToHexString } from "@ignored/hardhat-vnext-utils/hex"; - -import { getJsonRpcRequest } from "../../../../../../../src/internal/builtin-plugins/network-manager/json-rpc.js"; -import { createMockedNetworkHre } from "../../hooks-mock.js"; - -// Test that the request and its additional sub-request (when present) -// are correctly modified in the "onRequest" hook handler. -// These tests simulate a real scenario where the user calls "await connection.provider.request(jsonRpcRequest)". -describe("e2e - FixedSender", () => { - it("should set the from value into the transaction", async () => { - const hre = await createMockedNetworkHre({ - networks: { - hardhat: { - type: "edr", - from: "0x2a97a65d5673a2c61e95ce33cecadf24f654f96d", - }, - }, - }); - - const connection = await hre.network.connect(); - - const tx = { - to: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", - gas: numberToHexString(21000), - gasPrice: numberToHexString(678912), - nonce: numberToHexString(0), - value: numberToHexString(1), - }; - - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [tx]); - - const res = await connection.provider.request(jsonRpcRequest); - - assert.ok(Array.isArray(res), "res should be an array"); - - assert.equal(res[0].from, "0x2a97a65d5673a2c61e95ce33cecadf24f654f96d"); - }); -}); diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/e2e/accounts/hd-wallets.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/e2e/accounts/hd-wallets.ts deleted file mode 100644 index 6808c1dd9a..0000000000 --- a/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/e2e/accounts/hd-wallets.ts +++ /dev/null @@ -1,81 +0,0 @@ -import type { HttpNetworkHDAccountsConfig } from "../../../../../../../src/types/config.js"; - -import assert from "node:assert/strict"; -import { describe, it } from "node:test"; - -import { HardhatError } from "@ignored/hardhat-vnext-errors"; -import { assertRejectsWithHardhatError } from "@nomicfoundation/hardhat-test-utils"; - -import { getJsonRpcRequest } from "../../../../../../../src/internal/builtin-plugins/network-manager/json-rpc.js"; -import { createMockedNetworkHre } from "../../hooks-mock.js"; - -const HD_ACCOUNT: HttpNetworkHDAccountsConfig = { - mnemonic: - "couch hunt wisdom giant regret supreme issue sing enroll ankle type husband", - path: "m/44'/60'/0'/0/", - initialIndex: 0, - count: 2, - passphrase: "", -}; - -// Test that the request and its additional sub-request (when present) -// are correctly modified or resolved in the "onRequest" hook handler. -// These tests simulate a real scenario where the user calls "await connection.provider.request(jsonRpcRequest)". -describe("e2e - HDWallet", () => { - it("should generate 2 accounts", async () => { - const hre = await createMockedNetworkHre( - { - networks: { - localhost: { - type: "http", - url: "http://localhost:8545", - accounts: HD_ACCOUNT, - }, - }, - }, - { - eth_chainId: "0x7a69", - }, - ); - - const connection = await hre.network.connect("localhost"); - - const jsonRpcRequest = getJsonRpcRequest(1, "eth_accounts"); - - const res = await connection.provider.request(jsonRpcRequest); - - assert.ok(Array.isArray(res), "res should be an array"); - - assert.deepEqual(res, [ - "0x4f3e91d2cacd82fffd1f33a0d26d4078401986e9", - "0x2a97a65d5673a2c61e95ce33cecadf24f654f96d", - ]); - }); - - it("should throw if the path is invalid", async () => { - const hre = await createMockedNetworkHre( - { - networks: { - localhost: { - type: "http", - url: "http://localhost:8545", - accounts: { ...HD_ACCOUNT, path: "m/" }, - }, - }, - }, - { - eth_chainId: "0x7a69", - }, - ); - - const connection = await hre.network.connect("localhost"); - - const jsonRpcRequest = getJsonRpcRequest(1, "eth_accounts"); - - await assertRejectsWithHardhatError( - () => connection.provider.request(jsonRpcRequest), - HardhatError.ERRORS.NETWORK.INVALID_HD_PATH, - { path: "m/" }, - ); - }); -}); diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/e2e/accounts/local-accounts.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/e2e/accounts/local-accounts.ts deleted file mode 100644 index 4c500fbb8a..0000000000 --- a/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/e2e/accounts/local-accounts.ts +++ /dev/null @@ -1,134 +0,0 @@ -import type { NetworkConnection } from "../../../../../../../src/types/network.js"; - -import assert from "node:assert/strict"; -import { beforeEach, describe, it } from "node:test"; - -import { HardhatError } from "@ignored/hardhat-vnext-errors"; -import { - hexStringToBytes, - numberToHexString, -} from "@ignored/hardhat-vnext-utils/hex"; -import { assertRejectsWithHardhatError } from "@nomicfoundation/hardhat-test-utils"; -import { addr } from "micro-eth-signer"; - -import { getJsonRpcRequest } from "../../../../../../../src/internal/builtin-plugins/network-manager/json-rpc.js"; -import { createMockedNetworkHre } from "../../hooks-mock.js"; - -const MOCK_PROVIDER_CHAIN_ID = 31337; - -// Test that the request and its additional sub-request (when present) -// are correctly modified or resolved in the "onRequest" hook handler. -// These tests simulate a real scenario where the user calls "await connection.provider.request(jsonRpcRequest)". -describe("e2e - LocalAccounts", () => { - let connection: NetworkConnection; - - const accounts = [ - "0xb2e31025a2474b37e4c2d2931929a00b5752b98a3af45e3fd9a62ddc3cdf370e", - "0x6d7229c1db5892730b84b4bc10543733b72cabf4cd3130d910faa8e459bb8eca", - "0x6d4ec871d9b5469119bbfc891e958b6220d076a6849006098c370c8af5fc7776", - "0xec02c2b7019e75378a05018adc30a0252ba705670acb383a1d332e57b0b792d2", - ]; - - beforeEach(async () => { - const hre = await createMockedNetworkHre( - { - networks: { - localhost: { - type: "http", - url: "http://localhost:8545", - accounts, - }, - }, - }, - { - net_version: numberToHexString(MOCK_PROVIDER_CHAIN_ID), - eth_getTransactionCount: numberToHexString(0x8), - eth_accounts: [], - }, - ); - - connection = await hre.network.connect("localhost"); - }); - - it("should return the account addresses in eth_accounts", async () => { - const jsonRpcRequest = getJsonRpcRequest(1, "eth_accounts"); - - const res = await connection.provider.request(jsonRpcRequest); - - assert.ok(Array.isArray(res), "res should be an array"); - - assert.equal(res[0], addr.fromPrivateKey(accounts[0]).toLowerCase()); - assert.equal(res[1], addr.fromPrivateKey(accounts[1]).toLowerCase()); - }); - - it("should send eip1559 txs if the eip1559 fields are present", async () => { - const tx = { - from: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", - to: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", - gas: numberToHexString(30000), - nonce: numberToHexString(0), - value: numberToHexString(1), - chainId: numberToHexString(MOCK_PROVIDER_CHAIN_ID), - maxFeePerGas: numberToHexString(12), - maxPriorityFeePerGas: numberToHexString(2), - }; - - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [tx]); - - const res = await connection.provider.request(jsonRpcRequest); - - assert.ok(Array.isArray(res), "params should be an array"); - - const rawTransaction = hexStringToBytes(res[0]); - - // The tx type is encoded in the first byte, and it must be the EIP-1559 one - assert.equal(rawTransaction[0], 2); - }); - - it("should throw if trying to send from an account that isn't local", async () => { - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ - { - from: "0x000006d4548a3ac17d72b372ae1e416bf65b8ead", - to: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", - gas: numberToHexString(21000), - gasPrice: numberToHexString(678912), - nonce: numberToHexString(0), - value: numberToHexString(1), - }, - ]); - - await assertRejectsWithHardhatError( - () => connection.provider.request(jsonRpcRequest), - HardhatError.ERRORS.NETWORK.NOT_LOCAL_ACCOUNT, - { account: "0x000006d4548a3ac17d72b372ae1e416bf65b8ead" }, - ); - }); - - it("should not modify the json rpc request for other methods", async () => { - const input = [1, 2]; - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sarasa", input); - - const res = await connection.provider.request(jsonRpcRequest); - - assert.deepEqual(res, jsonRpcRequest.params); - }); - - it("should not modify the json rpc request if no address is given", async () => { - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sign"); - - const res = await connection.provider.request(jsonRpcRequest); - - assert.deepEqual(res, jsonRpcRequest.params); - }); - - it("should not modify the json rpc request if the address isn't one of the local ones", async () => { - const jsonRpcRequest = getJsonRpcRequest(1, "eth_signTypedData_v4", [ - "0x000006d4548a3ac17d72b372ae1e416bf65b8ead", - {}, - ]); - - const res = await connection.provider.request(jsonRpcRequest); - - assert.deepEqual(res, jsonRpcRequest.params); - }); -}); diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/e2e/chain-id.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/e2e/chain-id.ts deleted file mode 100644 index 5b6d9fe494..0000000000 --- a/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/e2e/chain-id.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { beforeEach, describe, it } from "node:test"; - -import { HardhatError } from "@ignored/hardhat-vnext-errors"; -import { assertRejectsWithHardhatError } from "@nomicfoundation/hardhat-test-utils"; - -import { createMockedNetworkHre } from "../hooks-mock.js"; - -// Test that the request and its additional sub-request (when present) -// are correctly modified in the "onRequest" hook handler. -// These tests simulate a real scenario where the user calls "await connection.provider.request(jsonRpcRequest)". -describe("e2e - ChainIdValidator", () => { - describe("eth_chainId", () => { - it("should not fail because the chain id is the same as the one returned by eth_chainId", async () => { - const hre = await createMockedNetworkHre( - { - networks: { - localhost: { - type: "http", - url: "http://localhost:8545", - chainId: 1, - }, - }, - }, - { - eth_chainId: "0x1", - }, - ); - - // Use the localhost network for these tests because the modifier is only - // applicable to HTTP networks. EDR networks do not require this modifier. - const connection = await hre.network.connect("localhost"); - - await connection.provider.request({ - method: "eth_sendTransaction", - params: [], - }); - }); - - it("should fail because the chain id is different from the one returned by eth_chainId", async () => { - const hre = await createMockedNetworkHre( - { - networks: { - localhost: { - type: "http", - url: "http://localhost:8545", - chainId: 2, - }, - }, - }, - { - eth_chainId: "0x1", - }, - ); - - const connection = await hre.network.connect("localhost"); - - await assertRejectsWithHardhatError( - connection.provider.request({ - method: "eth_sendTransaction", - params: [], - }), - HardhatError.ERRORS.NETWORK.INVALID_GLOBAL_CHAIN_ID, - { - configChainId: 2, - connectionChainId: 1, - }, - ); - }); - }); - - describe("net_version", () => { - beforeEach(async () => {}); - - it("should not fail because the chain id is the same as the one returned by net_version", async () => { - const hre = await createMockedNetworkHre( - { - networks: { - localhost: { - type: "http", - url: "http://localhost:8545", - chainId: 1, - }, - }, - }, - { - eth_chainId: undefined, // simulate an error for the method eth_chainId - net_version: "0x1", - }, - ); - - // Use the localhost network for these tests because the modifier is only - // applicable to HTTP networks. EDR networks do not require this modifier. - const connection = await hre.network.connect("localhost"); - - await connection.provider.request({ - method: "eth_sendTransaction", - params: [], - }); - }); - - it("should fail because the chain id is different from the one returned by net_version", async () => { - const hre = await createMockedNetworkHre( - { - networks: { - localhost: { - type: "http", - url: "http://localhost:8545", - chainId: 2, - }, - }, - }, - { - eth_chainId: undefined, // simulate an error for the method eth_chainId - net_version: "0x1", - }, - ); - - const connection = await hre.network.connect("localhost"); - - await assertRejectsWithHardhatError( - connection.provider.request({ - method: "eth_sendTransaction", - params: [], - }), - HardhatError.ERRORS.NETWORK.INVALID_GLOBAL_CHAIN_ID, - { - configChainId: 2, - connectionChainId: 1, - }, - ); - }); - }); -}); diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/e2e/gas/automatic-gas-price.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/e2e/gas/automatic-gas-price.ts deleted file mode 100644 index ce3753083f..0000000000 --- a/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/e2e/gas/automatic-gas-price.ts +++ /dev/null @@ -1,55 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, it } from "node:test"; - -import { numberToHexString } from "@ignored/hardhat-vnext-utils/hex"; - -import { getJsonRpcRequest } from "../../../../../../../src/internal/builtin-plugins/network-manager/json-rpc.js"; -import { createMockedNetworkHre } from "../../hooks-mock.js"; - -const LATEST_BASE_FEE_IN_MOCKED_PROVIDER = 80; - -// Test that the request and its additional sub-request (when present) -// are correctly modified in the "onRequest" hook handler. -// These tests simulate a real scenario where the user calls "await connection.provider.request(jsonRpcRequest)". -describe("e2e - AutomaticGasPrice", () => { - it("should use the reward return value as default maxPriorityFeePerGas", async () => { - const hre = await createMockedNetworkHre( - {}, - { - eth_feeHistory: { - baseFeePerGas: [ - numberToHexString(LATEST_BASE_FEE_IN_MOCKED_PROVIDER), - numberToHexString( - Math.floor((LATEST_BASE_FEE_IN_MOCKED_PROVIDER * 9) / 8), - ), - ], - reward: [["0x4"]], - }, - - eth_getBlockByNumber: { - baseFeePerGas: "0x1", - }, - }, - ); - // Use the localhost network for this test because the modifier is only - // applicable to HTTP networks. EDR networks do not require this modifier. - const connection = await hre.network.connect("localhost"); - - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ - { - from: "0x0000000000000000000000000000000000000011", - to: "0x0000000000000000000000000000000000000011", - value: 1, - gas: 1, - maxFeePerGas: "0x99", - }, - ]); - - const res = await connection.provider.request(jsonRpcRequest); - - assert.ok(Array.isArray(res), "res should be an array"); - - assert.equal(res[0].maxPriorityFeePerGas, "0x4"); - assert.equal(res[0].maxFeePerGas, "0x99"); - }); -}); diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/e2e/gas/automatic-gas.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/e2e/gas/automatic-gas.ts deleted file mode 100644 index 9d84176fc4..0000000000 --- a/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/e2e/gas/automatic-gas.ts +++ /dev/null @@ -1,55 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, it } from "node:test"; - -import { numberToHexString } from "@ignored/hardhat-vnext-utils/hex"; - -import { getJsonRpcRequest } from "../../../../../../../src/internal/builtin-plugins/network-manager/json-rpc.js"; -import { createMockedNetworkHre } from "../../hooks-mock.js"; - -const FIXED_GAS_LIMIT = 1231; -const GAS_MULTIPLIER = 1.337; - -// Test that the request and its additional sub-request (when present) -// are correctly modified in the "onRequest" hook handler. -// These tests simulate a real scenario where the user calls "await connection.provider.request(jsonRpcRequest)". -describe("e2e - AutomaticGas", () => { - it("should estimate gas automatically if not present", async () => { - const hre = await createMockedNetworkHre( - { - networks: { - hardhat: { - type: "edr", - gas: "auto", - gasMultiplier: GAS_MULTIPLIER, - }, - }, - }, - { - eth_getBlockByNumber: { - gasLimit: numberToHexString(FIXED_GAS_LIMIT * 1000), - }, - eth_estimateGas: numberToHexString(FIXED_GAS_LIMIT), - }, - ); - - const connection = await hre.network.connect(); - - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ - { - from: "0x0000000000000000000000000000000000000011", - to: "0x0000000000000000000000000000000000000011", - value: 1, - gasPrice: 1, - }, - ]); - - const res = await connection.provider.request(jsonRpcRequest); - - assert.ok(Array.isArray(res), "res should be an array"); - - assert.equal( - res[0].gas, - numberToHexString(Math.floor(FIXED_GAS_LIMIT * GAS_MULTIPLIER)), - ); - }); -}); diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/e2e/gas/fixed-gas-price.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/e2e/gas/fixed-gas-price.ts deleted file mode 100644 index 4073b787dc..0000000000 --- a/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/e2e/gas/fixed-gas-price.ts +++ /dev/null @@ -1,45 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, it } from "node:test"; - -import { numberToHexString } from "@ignored/hardhat-vnext-utils/hex"; - -import { getJsonRpcRequest } from "../../../../../../../src/internal/builtin-plugins/network-manager/json-rpc.js"; -import { createMockedNetworkHre } from "../../hooks-mock.js"; - -// Test that the request and its additional sub-request (when present) -// are correctly modified in the "onRequest" hook handler. -// These tests simulate a real scenario where the user calls "await connection.provider.request(jsonRpcRequest)". -describe("e2e - FixedGasPrice", () => { - const FIXED_GAS_PRICE = 1232n; - - it("should set the fixed gas price if not present", async () => { - const hre = await createMockedNetworkHre( - { - networks: { - hardhat: { - type: "edr", - gasPrice: FIXED_GAS_PRICE, - }, - }, - }, - {}, - ); - - const connection = await hre.network.connect(); - - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ - { - from: "0x0000000000000000000000000000000000000011", - to: "0x0000000000000000000000000000000000000011", - value: 1, - gas: 1, - }, - ]); - - const res = await connection.provider.request(jsonRpcRequest); - - assert.ok(Array.isArray(res), "res should be an array"); - - assert.equal(res[0].gasPrice, numberToHexString(FIXED_GAS_PRICE)); - }); -}); diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/e2e/gas/fixed-gas.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/e2e/gas/fixed-gas.ts deleted file mode 100644 index 141cc11a14..0000000000 --- a/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/e2e/gas/fixed-gas.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type { - GenericChainType, - NetworkConnection, -} from "../../../../../../../src/types/network.js"; - -import assert from "node:assert/strict"; -import { beforeEach, describe, it } from "node:test"; - -import { numberToHexString } from "@ignored/hardhat-vnext-utils/hex"; - -import { getJsonRpcRequest } from "../../../../../../../src/internal/builtin-plugins/network-manager/json-rpc.js"; -import { createMockedNetworkHre } from "../../hooks-mock.js"; - -const FIXED_GAS_LIMIT = 1231n; - -// Test that the request and its additional sub-request (when present) -// are correctly modified in the "onRequest" hook handler. -// These tests simulate a real scenario where the user calls "await connection.provider.request(jsonRpcRequest)". -describe("e2e - FixedGas", () => { - let connection: NetworkConnection; - - beforeEach(async () => { - const hre = await createMockedNetworkHre( - { - networks: { - localhost: { - type: "http", - url: "http://localhost:8545", - gas: FIXED_GAS_LIMIT, - }, - }, - }, - {}, - ); - - connection = await hre.network.connect("localhost"); - }); - - it("should set the fixed gas if not present", async () => { - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ - { - from: "0x0000000000000000000000000000000000000011", - to: "0x0000000000000000000000000000000000000011", - value: 1, - gasPrice: 1, - }, - ]); - - const res = await connection.provider.request(jsonRpcRequest); - - assert.ok(Array.isArray(res), "res should be an array"); - - assert.equal(res[0].gas, numberToHexString(FIXED_GAS_LIMIT)); - }); -}); diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/ethereum-mocked-provider.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/ethereum-mocked-provider.ts deleted file mode 100644 index acc3e21e35..0000000000 --- a/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/ethereum-mocked-provider.ts +++ /dev/null @@ -1,77 +0,0 @@ -import type { - EIP1193Provider, - JsonRpcRequest, - JsonRpcResponse, - RequestArguments, -} from "../../../../../src/types/providers.js"; - -import EventEmitter from "node:events"; - -// This mock is used in the unit tests to simulate the return value of the "request" method -export class EthereumMockedProvider - extends EventEmitter - implements EIP1193Provider -{ - // Record - readonly #returnValues: Record = {}; - - readonly #latestParams: Record = {}; - - readonly #numberOfCalls: { [method: string]: number } = {}; - - // If a lambda is passed as value, it's return value is used. - public setReturnValue(method: string, value: any): void { - this.#returnValues[method] = value; - } - - public getNumberOfCalls(method: string): number { - if (this.#numberOfCalls[method] === undefined) { - return 0; - } - - return this.#numberOfCalls[method]; - } - - public getLatestParams(method: string): any { - return this.#latestParams[method]; - } - - public getTotalNumberOfCalls(): number { - return Object.values(this.#numberOfCalls).reduce((p, c) => p + c, 0); - } - - public async request({ - method, - params = [], - }: RequestArguments): Promise { - // stringify the params to make sure they are serializable - JSON.stringify(params); - - this.#latestParams[method] = params; - - if (this.#numberOfCalls[method] === undefined) { - this.#numberOfCalls[method] = 1; - } else { - this.#numberOfCalls[method] += 1; - } - - let ret = this.#returnValues[method]; - - if (ret instanceof Function) { - ret = ret(); - } - - return ret; - } - - public send(_method: string, _params?: unknown[]): Promise { - return Promise.resolve(null); - } - - public sendAsync( - _jsonRpcRequest: JsonRpcRequest, - _callback: (error: any, jsonRpcResponse: JsonRpcResponse) => void, - ): void {} - - public async close(): Promise {} -} diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/hooks-mock.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/hooks-mock.ts deleted file mode 100644 index 1a62f253d6..0000000000 --- a/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/hooks-mock.ts +++ /dev/null @@ -1,70 +0,0 @@ -import type { HardhatUserConfig } from "../../../../../src/config.js"; -import type { HookContext } from "../../../../../src/types/hooks.js"; -import type { HardhatRuntimeEnvironment } from "../../../../../src/types/hre.js"; -import type { - ChainType, - NetworkConnection, -} from "../../../../../src/types/network.js"; -import type { - JsonRpcRequest, - JsonRpcResponse, -} from "../../../../../src/types/providers.js"; - -import { createHardhatRuntimeEnvironment } from "../../../../../src/hre.js"; - -// We override the network hook handlers to set two dynamic hook handler -// categories, which will take precedence over the default ones. -// -// This means that they are going to be the first ones to be called, so we -// set the handler that we want to test as the first one, and the mocked -// one as the second one. -// -// In this way, the function "next" in the first handler will call the mock. -export async function createMockedNetworkHre( - hardhatUserConfig: HardhatUserConfig, - returnValues: Record = {}, -): Promise { - const mockedResponse: JsonRpcResponse = { - jsonrpc: "2.0", - id: 1, - result: [], - }; - - const hre = await createHardhatRuntimeEnvironment(hardhatUserConfig); - - hre.hooks.registerHandlers("network", { - onRequest: async ( - _context: HookContext, - _networkConnection: NetworkConnection, - jsonRpcRequest: JsonRpcRequest, - _next: ( - nextContext: HookContext, - nextNetworkConnection: NetworkConnection, - nextJsonRpcRequest: JsonRpcRequest, - ) => Promise, - ) => { - if (returnValues[jsonRpcRequest.method] !== undefined) { - mockedResponse.result = returnValues[jsonRpcRequest.method]; - return mockedResponse; - } - - // Returns the modified jsonRpcRequest as a response, so it can be verified in the test that it - // was successfully modified - mockedResponse.result = jsonRpcRequest.params; - return mockedResponse; - }, - }); - - // We set the default network hook handlers again, to take precedence over the - // mocked one. - const { default: networkHookHandlerHandlersFactory } = await import( - "../../../../../src/internal/builtin-plugins/network-manager/hook-handlers/network.js" - ); - - hre.hooks.registerHandlers( - "network", - await networkHookHandlerHandlersFactory(), - ); - - return hre; -} diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/unit/accounts/automatic-sender.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/unit/accounts/automatic-sender.ts deleted file mode 100644 index 4fe6fa8cbf..0000000000 --- a/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/unit/accounts/automatic-sender.ts +++ /dev/null @@ -1,71 +0,0 @@ -import type { JsonRpcTransactionData } from "../../../../../../../src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/accounts/types.js"; - -import assert from "node:assert/strict"; -import { before, describe, it } from "node:test"; - -import { numberToHexString } from "@ignored/hardhat-vnext-utils/hex"; - -import { AutomaticSender } from "../../../../../../../src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/accounts/automatic-sender-provider.js"; -import { - getJsonRpcRequest, - getRequestParams, -} from "../../../../../../../src/internal/builtin-plugins/network-manager/json-rpc.js"; -import { EthereumMockedProvider } from "../../ethereum-mocked-provider.js"; - -describe("AutomaticSender", function () { - let automaticSender: AutomaticSender; - let mockedProvider: EthereumMockedProvider; - let tx: JsonRpcTransactionData; - - before(() => { - mockedProvider = new EthereumMockedProvider(); - - mockedProvider.setReturnValue("eth_accounts", [ - "0x123006d4548a3ac17d72b372ae1e416bf65b8eaf", - ]); - - automaticSender = new AutomaticSender(mockedProvider); - - tx = { - to: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", - gas: numberToHexString(21000), - gasPrice: numberToHexString(678912), - nonce: numberToHexString(0), - value: numberToHexString(1), - }; - }); - - it("should set the from value into the transaction", async () => { - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [tx]); - - await automaticSender.modifyRequest(jsonRpcRequest); - - assert.equal( - getRequestParams(jsonRpcRequest)[0].from, - "0x123006d4548a3ac17d72b372ae1e416bf65b8eaf", - ); - }); - - it("should not replace transaction's from", async () => { - tx.from = "0x000006d4548a3ac17d72b372ae1e416bf65b8ead"; - - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [tx]); - - assert.equal( - getRequestParams(jsonRpcRequest)[0].from, - "0x000006d4548a3ac17d72b372ae1e416bf65b8ead", - ); - }); - - it("should not fail on eth_calls if provider doesn't have any accounts", async () => { - mockedProvider.setReturnValue("eth_accounts", []); - - tx.value = "asd"; - - const jsonRpcRequest = getJsonRpcRequest(1, "eth_call", [tx]); - - await automaticSender.modifyRequest(jsonRpcRequest); - - assert.equal(getRequestParams(jsonRpcRequest)[0].value, "asd"); - }); -}); diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/unit/accounts/fixed-sender.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/unit/accounts/fixed-sender.ts deleted file mode 100644 index e6c2f6f5a3..0000000000 --- a/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/unit/accounts/fixed-sender.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { JsonRpcTransactionData } from "../../../../../../../src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/accounts/types.js"; - -import assert from "node:assert/strict"; -import { before, describe, it } from "node:test"; - -import { numberToHexString } from "@ignored/hardhat-vnext-utils/hex"; - -import { FixedSender } from "../../../../../../../src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/accounts/fixed-sender-provider.js"; -import { - getJsonRpcRequest, - getRequestParams, -} from "../../../../../../../src/internal/builtin-plugins/network-manager/json-rpc.js"; -import { EthereumMockedProvider } from "../../ethereum-mocked-provider.js"; - -describe("FixedSender", function () { - let fixedSender: FixedSender; - let mockedProvider: EthereumMockedProvider; - let tx: JsonRpcTransactionData; - - before(() => { - mockedProvider = new EthereumMockedProvider(); - - fixedSender = new FixedSender( - mockedProvider, - "0x2a97a65d5673a2c61e95ce33cecadf24f654f96d", - ); - - tx = { - to: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", - gas: numberToHexString(21000), - gasPrice: numberToHexString(678912), - nonce: numberToHexString(0), - value: numberToHexString(1), - }; - }); - - it("should set the from value into the transaction", async () => { - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [tx]); - - await fixedSender.modifyRequest(jsonRpcRequest); - - assert.equal( - getRequestParams(jsonRpcRequest)[0].from, - "0x2a97a65d5673a2c61e95ce33cecadf24f654f96d", - ); - }); - - it("should not replace transaction's from", async () => { - tx.from = "0x000006d4548a3ac17d72b372ae1e416bf65b8ead"; - - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [tx]); - - await fixedSender.modifyRequest(jsonRpcRequest); - - assert.equal( - getRequestParams(jsonRpcRequest)[0].from, - "0x000006d4548a3ac17d72b372ae1e416bf65b8ead", - ); - }); -}); diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/unit/accounts/hd-wallet.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/unit/accounts/hd-wallet.ts deleted file mode 100644 index 8b7d62ae0a..0000000000 --- a/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/unit/accounts/hd-wallet.ts +++ /dev/null @@ -1,144 +0,0 @@ -import type { JsonRpcResponse } from "../../../../../../../src/types/providers.js"; - -import assert from "node:assert/strict"; -import { beforeEach, describe, it } from "node:test"; - -import { HardhatError } from "@ignored/hardhat-vnext-errors"; -import { assertThrowsHardhatError } from "@nomicfoundation/hardhat-test-utils"; - -import { HDWallet } from "../../../../../../../src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/accounts/hd-wallet.js"; -import { getJsonRpcRequest } from "../../../../../../../src/internal/builtin-plugins/network-manager/json-rpc.js"; -import { EthereumMockedProvider } from "../../ethereum-mocked-provider.js"; - -function getResult(res: JsonRpcResponse | null): string[] { - assert.ok(res !== null, "res should not be null"); - assert.ok("result" in res, "res should have the property 'result'"); - assert.ok(Array.isArray(res.result), "res.result should be an array"); - - return res.result; -} - -describe("HDWallet", () => { - let hdWallet: HDWallet; - let mockedProvider: EthereumMockedProvider; - - const mnemonic = - "couch hunt wisdom giant regret supreme issue sing enroll ankle type husband"; - const hdpath = "m/44'/60'/0'/0/"; - - beforeEach(() => { - mockedProvider = new EthereumMockedProvider(); - - hdWallet = new HDWallet(mockedProvider, mnemonic, hdpath); - }); - - it("should generate a valid address", async () => { - const jsonRpcRequest = getJsonRpcRequest(1, "eth_accounts"); - - const res = await hdWallet.resolveRequest(jsonRpcRequest); - - const result = getResult(res); - - assert.equal(result[0], "0x4f3e91d2cacd82fffd1f33a0d26d4078401986e9"); - }); - - it("should generate a valid address with passphrase", async () => { - const passphrase = "this is a secret"; - - hdWallet = new HDWallet( - mockedProvider, - mnemonic, - hdpath, - 0, - 10, - passphrase, - ); - - const jsonRpcRequest = getJsonRpcRequest(1, "eth_accounts"); - - const res = await hdWallet.resolveRequest(jsonRpcRequest); - - const result = getResult(res); - - assert.equal(result[0], "0x6955b833d195e49c07fc56fbf0ec387325facb87"); - }); - - it("should generate a valid address when given a different index", async () => { - hdWallet = new HDWallet(mockedProvider, mnemonic, hdpath, 1); - - const jsonRpcRequest = getJsonRpcRequest(1, "eth_accounts"); - - const res = await hdWallet.resolveRequest(jsonRpcRequest); - - const result = getResult(res); - - assert.equal(result[0], "0x2a97a65d5673a2c61e95ce33cecadf24f654f96d"); - }); - - it("should generate 2 accounts", async () => { - hdWallet = new HDWallet(mockedProvider, mnemonic, hdpath, 0, 2); - - const jsonRpcRequest = getJsonRpcRequest(1, "eth_accounts"); - - const res = await hdWallet.resolveRequest(jsonRpcRequest); - - const result = getResult(res); - - assert.deepEqual(result, [ - "0x4f3e91d2cacd82fffd1f33a0d26d4078401986e9", - "0x2a97a65d5673a2c61e95ce33cecadf24f654f96d", - ]); - }); - - describe("HDPath formatting", () => { - it("should work if it doesn't end in a /", async () => { - hdWallet = new HDWallet(mockedProvider, mnemonic, "m/44'/60'/0'/0"); - - const jsonRpcRequest = getJsonRpcRequest(1, "eth_accounts"); - - const res = await hdWallet.resolveRequest(jsonRpcRequest); - - const result = getResult(res); - - assert.equal(result[0], "0x4f3e91d2cacd82fffd1f33a0d26d4078401986e9"); - }); - - it("should throw if the path is invalid", () => { - assertThrowsHardhatError( - () => new HDWallet(mockedProvider, mnemonic, ""), - HardhatError.ERRORS.NETWORK.INVALID_HD_PATH, - { path: "" }, - ); - - assertThrowsHardhatError( - () => new HDWallet(mockedProvider, mnemonic, "m/"), - HardhatError.ERRORS.NETWORK.INVALID_HD_PATH, - { path: "m/" }, - ); - - assertThrowsHardhatError( - () => new HDWallet(mockedProvider, mnemonic, "m//"), - HardhatError.ERRORS.NETWORK.INVALID_HD_PATH, - { path: "m//" }, - ); - - assertThrowsHardhatError( - () => new HDWallet(mockedProvider, mnemonic, "m/'"), - HardhatError.ERRORS.NETWORK.INVALID_HD_PATH, - { path: "m/'" }, - ); - - assertThrowsHardhatError( - () => new HDWallet(mockedProvider, mnemonic, "m/0''"), - HardhatError.ERRORS.NETWORK.INVALID_HD_PATH, - { path: "m/0''" }, - ); - - assertThrowsHardhatError( - () => new HDWallet(mockedProvider, mnemonic, "ghj"), - HardhatError.ERRORS.NETWORK.INVALID_HD_PATH, - { path: "ghj" }, - ); - }); - }); -}); diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/unit/accounts/local-accounts.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/unit/accounts/local-accounts.ts deleted file mode 100644 index 5ed79f9e8e..0000000000 --- a/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/unit/accounts/local-accounts.ts +++ /dev/null @@ -1,754 +0,0 @@ -import assert from "node:assert/strict"; -import { beforeEach, describe, it } from "node:test"; - -import { HardhatError } from "@ignored/hardhat-vnext-errors"; -import { - hexStringToBytes, - numberToHexString, -} from "@ignored/hardhat-vnext-utils/hex"; -import { assertRejectsWithHardhatError } from "@nomicfoundation/hardhat-test-utils"; -import { addr } from "micro-eth-signer"; - -import { LocalAccounts } from "../../../../../../../src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/accounts/local-accounts.js"; -import { getJsonRpcRequest } from "../../../../../../../src/internal/builtin-plugins/network-manager/json-rpc.js"; -import { EthereumMockedProvider } from "../../ethereum-mocked-provider.js"; - -// This is a valid raw EIP_2930 tx checked in a local hardhat node, -// where the sender account had funds and the chain id was 123. -const EXPECTED_RAW_TX = - "0x01f89a7b800182753094b5bc06d4548a3ac17d72b372ae1e416bf65b8e" + - "ad0180f838f79457d7ad4d3f0c74e3766874cf06fa1dc23c21f7e8e1a0a5" + - "0e92910457911e0e22d6dd1672f440a37b590b231d8309101255290f5394" + - "ec80a02b2fca5e2cf3569d29693e965f045529efa6a54bf0ab11104dd4ea" + - "8b2ca3daf7a06025c30f36a179a09b9952e025632a65f220ec385eccd23a" + - "1fb952976eace481"; - -const EXPECTED_RAW_TX_VALUES = { - from: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", - to: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", - gas: "0x7530", - gasLimit: "0x7530", - gasPrice: "0x1", - nonce: "0x0", - value: "0x1", - accessList: [ - { - address: "0x57d7ad4d3f0c74e3766874cf06fa1dc23c21f7e8", - storageKeys: [ - "0xa50e92910457911e0e22d6dd1672f440a37b590b231d8309101255290f5394ec", - ], - }, - ], -}; - -/** - * Validate that `rawTx` is an EIP-2930 transaction that has - * the same values as `tx` - */ -function validateRawEIP2930Transaction(tx: any) { - assert.equal(EXPECTED_RAW_TX_VALUES.from, tx.from); - assert.equal(EXPECTED_RAW_TX_VALUES.to, tx.to); - - assert.equal(EXPECTED_RAW_TX_VALUES.gas, tx.gas); - assert.equal(EXPECTED_RAW_TX_VALUES.gasPrice, tx.gasPrice); - assert.equal(EXPECTED_RAW_TX_VALUES.nonce, tx.nonce); - assert.equal(EXPECTED_RAW_TX_VALUES.value, tx.value); - assert.deepEqual(EXPECTED_RAW_TX_VALUES.accessList, tx.accessList); -} - -const MOCK_PROVIDER_CHAIN_ID = 123; - -describe("LocalAccounts", () => { - let localAccounts: LocalAccounts; - - let mockedProvider: EthereumMockedProvider; - - const accounts = [ - "0xb2e31025a2474b37e4c2d2931929a00b5752b98a3af45e3fd9a62ddc3cdf370e", - "0x6d7229c1db5892730b84b4bc10543733b72cabf4cd3130d910faa8e459bb8eca", - "0x6d4ec871d9b5469119bbfc891e958b6220d076a6849006098c370c8af5fc7776", - "0xec02c2b7019e75378a05018adc30a0252ba705670acb383a1d332e57b0b792d2", - ]; - - beforeEach(() => { - mockedProvider = new EthereumMockedProvider(); - - mockedProvider.setReturnValue( - "net_version", - numberToHexString(MOCK_PROVIDER_CHAIN_ID), - ); - mockedProvider.setReturnValue( - "eth_getTransactionCount", - numberToHexString(0x8), - ); - mockedProvider.setReturnValue("eth_accounts", []); - - localAccounts = new LocalAccounts(mockedProvider, accounts); - }); - - describe("resolveRequest", () => { - it("should return the account addresses in eth_accounts", async () => { - const jsonRpcRequest = getJsonRpcRequest(1, "eth_accounts"); - - const res = await localAccounts.resolveRequest(jsonRpcRequest); - - assert.ok(res !== null, "res should not be null"); - assert.ok("result" in res, "res should have the property 'result'"); - assert.ok(Array.isArray(res.result), "res.result should be an array"); - - assert.equal( - res.result[0], - addr.fromPrivateKey(accounts[0]).toLowerCase(), - ); - assert.equal( - res.result[1], - addr.fromPrivateKey(accounts[1]).toLowerCase(), - ); - }); - - it("should return the account addresses in eth_requestAccounts", async () => { - const jsonRpcRequest = getJsonRpcRequest(1, "eth_requestAccounts"); - - const res = await localAccounts.resolveRequest(jsonRpcRequest); - - assert.ok(res !== null, "res should not be null"); - assert.ok("result" in res, "res should have the property 'result'"); - assert.ok(Array.isArray(res.result), "res.result should be an array"); - - assert.equal( - res.result[0], - addr.fromPrivateKey(accounts[0]).toLowerCase(), - ); - assert.equal( - res.result[1], - addr.fromPrivateKey(accounts[1]).toLowerCase(), - ); - }); - - it("should forward other methods", async () => { - const input = [1, 2]; - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sarasa", input); - - const res = await localAccounts.resolveRequest(jsonRpcRequest); - - assert.deepEqual(res, null); - }); - - describe("eth_sign", () => { - it("should be compatible with parity's implementation", async () => { - // This test was created by using Parity Ethereum - // v2.2.5-beta-7fbcdfeed-20181213 and calling eth_sign - localAccounts = new LocalAccounts(mockedProvider, [ - "0x6e59a6617c48d76d3b21d722eaba867e16ecf54ab3da7a93724f51812bc6d1aa", - ]); - - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sign", [ - "0x24f1a362780503D762060C1683864C4066A74b05", - "0x41206d657373616765", - ]); - - const res = await localAccounts.resolveRequest(jsonRpcRequest); - - assert.ok(res !== null, "res should not be null"); - assert.ok("result" in res, "res should have the property 'result'"); - - assert.equal( - res.result, - "0x25c349f668c90a890c84aa79a78cf6c74e96483b43ec3ed06aa8aec835477c034aa096e883cc9871aa4ffdffd9f21f6ee4aa4b70f478ad56a18971e4ec2c753e1b", - ); - }); - - it("should be compatible with ganache-cli's implementation", async () => { - // This test was created by using Ganache CLI v6.1.6 (ganache-core: 2.1.5) - localAccounts = new LocalAccounts(mockedProvider, [ - "0xf159c85082f4dd4ee472583a37a1b5683c727ec99708f3d94ff05faa7a7a70ce", - ]); - - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sign", [ - "0x0a929c90dd22f0fb09ec38983780530ee30a29a3", - "0x41206d657373616765", - ]); - - const res = await localAccounts.resolveRequest(jsonRpcRequest); - - assert.ok(res !== null, "res should not be null"); - assert.ok("result" in res, "res should have the property 'result'"); - assert.ok( - typeof res.result === "string", - "res.result should be a string", - ); - - // This test is weird because ganache encodes the v param of the signature - // differently than the rest. It subtracts 27 from it before serializing. - assert.equal( - res.result.slice(0, -2), - "0x84d993fc1b54926db1b6b81544aada29f0f36850a83dc979e8bacfa87e7c7cb11689b2f4ca64697842c42bb7e0cb02dff1851b42e25e62858f27f57bd00ff74b00".slice( - 0, - -2, - ), - ); - }); - - it("should be compatible with geth's implementation", async () => { - // This test was created by using Geth 1.8.20-stable - localAccounts = new LocalAccounts(mockedProvider, [ - "0xf2d19e944851ea0faa9440e24a22ddab850210cae46b306a3fde4c98b22a0dcb", - ]); - - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sign", [ - "0x5Fd8509eABccFFec1d2530e48F55545B49Bd5B5e", - "0x41206d657373616765", - ]); - - const res = await localAccounts.resolveRequest(jsonRpcRequest); - - assert.ok(res !== null, "res should not be null"); - assert.ok("result" in res, "res should have the property 'result'"); - - assert.equal( - res.result, - "0x88c6ac158d40e84f519fbb48b6a1355a31202b684163f637fe5c92cc1109acbe5c79a2dd95a8aecff45756c6fc3b4fc8aef345179605bcead2916dd533fb22651b", - ); - }); - - it("should throw if no data is given", async () => { - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sign", [ - addr.fromPrivateKey(accounts[0]), - ]); - - // eslint-disable-next-line no-restricted-syntax -- not a Hardhat error - await assert.rejects(localAccounts.resolveRequest(jsonRpcRequest)); - }); - - it("should throw if the address isn't one of the local ones", async () => { - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sign", [ - "0x000006d4548a3ac17d72b372ae1e416bf65b8ead", - "0x00", - ]); - - await assertRejectsWithHardhatError( - () => localAccounts.resolveRequest(jsonRpcRequest), - HardhatError.ERRORS.NETWORK.NOT_LOCAL_ACCOUNT, - { - account: "0x000006d4548a3ac17d72b372ae1e416bf65b8ead", - }, - ); - }); - - it("should just forward if no address is given", async () => { - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sign"); - - const res = await localAccounts.resolveRequest(jsonRpcRequest); - - assert.deepEqual(res, null); - }); - }); - - describe("eth_signTypedData_v4", () => { - it("should be compatible with EIP-712 example", async () => { - // This test was taken from the `eth_signTypedData` example from the - // EIP-712 specification. - // - localAccounts = new LocalAccounts(mockedProvider, [ - // keccak256("cow") - "0xc85ef7d79691fe79573b1a7064c19c1a9819ebdbd1faaab1a8ec92344438aaf4", - ]); - - const jsonRpcRequest = getJsonRpcRequest(1, "eth_signTypedData_v4", [ - "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", - { - types: { - EIP712Domain: [ - { name: "name", type: "string" }, - { name: "version", type: "string" }, - { name: "chainId", type: "uint256" }, - { name: "verifyingContract", type: "address" }, - ], - Person: [ - { name: "name", type: "string" }, - { name: "wallet", type: "address" }, - ], - Mail: [ - { name: "from", type: "Person" }, - { name: "to", type: "Person" }, - { name: "contents", type: "string" }, - ], - }, - primaryType: "Mail", - domain: { - name: "Ether Mail", - version: "1", - chainId: 1, - verifyingContract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC", - }, - message: { - from: { - name: "Cow", - wallet: "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", - }, - to: { - name: "Bob", - wallet: "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB", - }, - contents: "Hello, Bob!", - }, - }, - ]); - - const res = await localAccounts.resolveRequest(jsonRpcRequest); - - assert.ok(res !== null, "res should not be null"); - assert.ok("result" in res, "res should have the property 'result'"); - - assert.equal( - res.result, - "0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b915621c", - ); - }); - - it("should be compatible with stringified JSON input", async () => { - localAccounts = new LocalAccounts(mockedProvider, [ - // keccak256("cow") - "0xc85ef7d79691fe79573b1a7064c19c1a9819ebdbd1faaab1a8ec92344438aaf4", - ]); - - const jsonRpcRequest = getJsonRpcRequest(1, "eth_signTypedData_v4", [ - "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", - JSON.stringify({ - types: { - EIP712Domain: [ - { name: "name", type: "string" }, - { name: "version", type: "string" }, - { name: "chainId", type: "uint256" }, - { name: "verifyingContract", type: "address" }, - ], - Person: [ - { name: "name", type: "string" }, - { name: "wallet", type: "address" }, - ], - Mail: [ - { name: "from", type: "Person" }, - { name: "to", type: "Person" }, - { name: "contents", type: "string" }, - ], - }, - primaryType: "Mail", - domain: { - name: "Ether Mail", - version: "1", - chainId: 1, - verifyingContract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC", - }, - message: { - from: { - name: "Cow", - wallet: "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", - }, - to: { - name: "Bob", - wallet: "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB", - }, - contents: "Hello, Bob!", - }, - }), - ]); - - const res = await localAccounts.resolveRequest(jsonRpcRequest); - - assert.ok(res !== null, "res should not be null"); - assert.ok("result" in res, "res should have the property 'result'"); - - assert.equal( - res.result, - "0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b915621c", - ); - }); - - it("should throw if data string input is not JSON", async () => { - const jsonRpcRequest = getJsonRpcRequest(1, "eth_signTypedData_v4", [ - "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", - "}thisisnotvalidjson{", - ]); - - await assertRejectsWithHardhatError( - () => localAccounts.resolveRequest(jsonRpcRequest), - HardhatError.ERRORS.NETWORK.ETHSIGN_TYPED_DATA_V4_INVALID_DATA_PARAM, - {}, - ); - }); - - it("should throw if no data is given", async () => { - const jsonRpcRequest = getJsonRpcRequest(1, "eth_signTypedData_v4", [ - addr.fromPrivateKey(accounts[0]), - ]); - - await assertRejectsWithHardhatError( - () => localAccounts.resolveRequest(jsonRpcRequest), - HardhatError.ERRORS.NETWORK.ETHSIGN_MISSING_DATA_PARAM, - {}, - ); - }); - - it("should just forward if the address isn't one of the local ones", async () => { - const jsonRpcRequest = getJsonRpcRequest(1, "eth_signTypedData_v4", [ - "0x000006d4548a3ac17d72b372ae1e416bf65b8ead", - {}, - ]); - - const res = await localAccounts.resolveRequest(jsonRpcRequest); - - assert.deepEqual(res, null); - }); - }); - - describe("personal_sign", () => { - it("should be compatible with geth's implementation", async () => { - // This test was created by using Geth 1.10.12-unstable and calling personal_sign - localAccounts = new LocalAccounts(mockedProvider, [ - "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", - ]); - - const jsonRpcRequest = getJsonRpcRequest(1, "personal_sign", [ - "0x5417aa2a18a44da0675524453ff108c545382f0d7e26605c56bba47c21b5e979", - "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", - ]); - - const res = await localAccounts.resolveRequest(jsonRpcRequest); - - assert.ok(res !== null, "res should not be null"); - assert.ok("result" in res, "res should have the property 'result'"); - - assert.equal( - res.result, - "0x9c73dd4937a37eecab3abb54b74b6ec8e500080431d36afedb1726624587ee6710296e10c1194dded7376f13ff03ef6c9e797eb86bae16c20c57776fc69344271c", - ); - }); - - it("should be compatible with metamask's implementation", async () => { - // This test was created by using Metamask 10.3.0 - localAccounts = new LocalAccounts(mockedProvider, [ - "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", - ]); - - const jsonRpcRequest = getJsonRpcRequest(1, "personal_sign", [ - "0x7699f568ecd7753e6ddf75a42fa4c2cc86cbbdc704c9eb1a6b6d4b9d8b8d1519", - "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", - ]); - - const res = await localAccounts.resolveRequest(jsonRpcRequest); - - assert.ok(res !== null, "res should not be null"); - assert.ok("result" in res, "res should have the property 'result'"); - - assert.equal( - res.result, - "0x2875e4206c9fe3b229291c81f95cc4f421e2f4d3e023f5b4041daa56ab4000977010b47a3c01036ec8a6a0872aec2ab285150f003d01b0d8da60c1cceb9154181c", - ); - }); - }); - }); - - describe("modifyRequest", () => { - it("should, given two identical tx, return the same raw transaction", async () => { - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ - { - from: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", - to: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", - gas: numberToHexString(21000), - gasPrice: numberToHexString(678912), - nonce: numberToHexString(0), - value: numberToHexString(1), - }, - ]); - - await localAccounts.modifyRequest(jsonRpcRequest); - - // This transaction was submitted to a blockchain and accepted, so the signature must be valid - const expectedRaw = - "0xf86480830a5c0082520894b5bc06d4548a3ac17d72b372ae1" + - "e416bf65b8ead018082011aa0614471b82c6ffedd4722ca5faa7f9b309a923661a4b2" + - "adc1a53a3ebe8c4d1f0aa06aebf2fbbe82703e5075965c65c776a9caeeff4b637f203" + - "d65383e1ed2e22654"; - - assert.deepEqual(jsonRpcRequest, { - jsonrpc: "2.0", - id: 1, - method: "eth_sendRawTransaction", - params: [expectedRaw], - }); - }); - - it("should send eip1559 txs if the eip1559 fields are present", async () => { - const tx = { - from: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", - to: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", - gas: numberToHexString(30000), - nonce: numberToHexString(0), - value: numberToHexString(1), - chainId: numberToHexString(MOCK_PROVIDER_CHAIN_ID), - maxFeePerGas: numberToHexString(12), - maxPriorityFeePerGas: numberToHexString(2), - }; - - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [tx]); - - await localAccounts.modifyRequest(jsonRpcRequest); - - assert.ok( - Array.isArray(jsonRpcRequest.params), - "params should be an array", - ); - - const rawTransaction = hexStringToBytes(jsonRpcRequest.params[0]); - - // The tx type is encoded in the first byte, and it must be the EIP-1559 one - assert.equal(rawTransaction[0], 2); - }); - - it("should send access list transactions", async () => { - const tx = { - from: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", - to: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", - gas: numberToHexString(30000), - gasPrice: numberToHexString(1), - nonce: numberToHexString(0), - value: numberToHexString(1), - chainId: numberToHexString(MOCK_PROVIDER_CHAIN_ID), - accessList: [ - { - address: "0x57d7ad4d3f0c74e3766874cf06fa1dc23c21f7e8", - storageKeys: [ - "0xa50e92910457911e0e22d6dd1672f440a37b590b231d8309101255290f5394ec", - ], - }, - ], - }; - - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [tx]); - - await localAccounts.modifyRequest(jsonRpcRequest); - - assert.ok( - Array.isArray(jsonRpcRequest.params), - "params should be an array", - ); - - const rawTransaction = jsonRpcRequest.params[0]; - - assert.equal(rawTransaction, EXPECTED_RAW_TX); - - validateRawEIP2930Transaction(tx); - }); - - it("should add the chainId value if it's missing", async () => { - const tx = { - from: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", - to: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", - gas: numberToHexString(30000), - gasPrice: numberToHexString(1), - nonce: numberToHexString(0), - value: numberToHexString(1), - accessList: [ - { - address: "0x57d7ad4d3f0c74e3766874cf06fa1dc23c21f7e8", - storageKeys: [ - "0xa50e92910457911e0e22d6dd1672f440a37b590b231d8309101255290f5394ec", - ], - }, - ], - }; - - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [tx]); - - await localAccounts.modifyRequest(jsonRpcRequest); - - assert.ok( - Array.isArray(jsonRpcRequest.params), - "params should be an array", - ); - - const rawTransaction = jsonRpcRequest.params[0]; - - assert.equal(rawTransaction, EXPECTED_RAW_TX); - - validateRawEIP2930Transaction(tx); - }); - - it("should get the nonce if not provided", async () => { - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ - { - from: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", - to: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", - gas: numberToHexString(21000), - gasPrice: numberToHexString(678912), - value: numberToHexString(1), - }, - ]); - - await localAccounts.modifyRequest(jsonRpcRequest); - - assert.equal( - mockedProvider.getNumberOfCalls("eth_getTransactionCount"), - 1, - ); - }); - - it("should throw when calling sendTransaction without gas", async () => { - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ - { - from: addr.fromPrivateKey(accounts[0]), - to: "0x2a97a65d5673a2c61e95ce33cecadf24f654f96d", - gasPrice: numberToHexString(0x3b9aca00), - nonce: numberToHexString(0x8), - }, - ]); - - await assertRejectsWithHardhatError( - () => localAccounts.modifyRequest(jsonRpcRequest), - HardhatError.ERRORS.NETWORK.MISSING_TX_PARAM_TO_SIGN_LOCALLY, - { param: "gas" }, - ); - }); - - it("should throw when calling sendTransaction without gasPrice, maxFeePerGas and maxPriorityFeePerGas", async () => { - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ - { - from: addr.fromPrivateKey(accounts[0]), - to: "0x2a97a65d5673a2c61e95ce33cecadf24f654f96d", - nonce: numberToHexString(0x8), - gas: numberToHexString(123), - }, - ]); - - await assertRejectsWithHardhatError( - () => localAccounts.modifyRequest(jsonRpcRequest), - HardhatError.ERRORS.NETWORK.MISSING_FEE_PRICE_FIELDS, - {}, - ); - }); - - it("should throw when calling sendTransaction with gasPrice and EIP1559 fields", async () => { - const jsonRpcRequest1 = getJsonRpcRequest(1, "eth_sendTransaction", [ - { - from: addr.fromPrivateKey(accounts[0]), - to: "0x2a97a65d5673a2c61e95ce33cecadf24f654f96d", - nonce: numberToHexString(0x8), - gas: numberToHexString(123), - gasPrice: numberToHexString(1), - maxFeePerGas: numberToHexString(1), - }, - ]); - - const jsonRpcRequest2 = getJsonRpcRequest(1, "eth_sendTransaction", [ - { - from: addr.fromPrivateKey(accounts[0]), - to: "0x2a97a65d5673a2c61e95ce33cecadf24f654f96d", - nonce: numberToHexString(0x8), - gas: numberToHexString(123), - gasPrice: numberToHexString(1), - maxPriorityFeePerGas: numberToHexString(1), - }, - ]); - - await assertRejectsWithHardhatError( - () => localAccounts.modifyRequest(jsonRpcRequest1), - HardhatError.ERRORS.NETWORK.INCOMPATIBLE_FEE_PRICE_FIELDS, - {}, - ); - - await assertRejectsWithHardhatError( - () => localAccounts.modifyRequest(jsonRpcRequest2), - HardhatError.ERRORS.NETWORK.INCOMPATIBLE_FEE_PRICE_FIELDS, - {}, - ); - }); - - it("should throw when only one EIP1559 field is provided", async () => { - const jsonRpcRequest1 = getJsonRpcRequest(1, "eth_sendTransaction", [ - { - from: addr.fromPrivateKey(accounts[0]), - to: "0x2a97a65d5673a2c61e95ce33cecadf24f654f96d", - nonce: numberToHexString(0x8), - gas: numberToHexString(123), - maxFeePerGas: numberToHexString(1), - }, - ]); - - const jsonRpcRequest2 = getJsonRpcRequest(1, "eth_sendTransaction", [ - { - from: addr.fromPrivateKey(accounts[0]), - to: "0x2a97a65d5673a2c61e95ce33cecadf24f654f96d", - nonce: numberToHexString(0x8), - gas: numberToHexString(123), - maxPriorityFeePerGas: numberToHexString(1), - }, - ]); - - await assertRejectsWithHardhatError( - () => localAccounts.modifyRequest(jsonRpcRequest1), - HardhatError.ERRORS.NETWORK.MISSING_TX_PARAM_TO_SIGN_LOCALLY, - { param: "maxPriorityFeePerGas" }, - ); - - await assertRejectsWithHardhatError( - () => localAccounts.modifyRequest(jsonRpcRequest2), - HardhatError.ERRORS.NETWORK.MISSING_TX_PARAM_TO_SIGN_LOCALLY, - { param: "maxFeePerGas" }, - ); - }); - - it("should throw if trying to send from an account that isn't local", async () => { - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ - { - from: "0x000006d4548a3ac17d72b372ae1e416bf65b8ead", - to: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", - gas: numberToHexString(21000), - gasPrice: numberToHexString(678912), - nonce: numberToHexString(0), - value: numberToHexString(1), - }, - ]); - - await assertRejectsWithHardhatError( - () => localAccounts.modifyRequest(jsonRpcRequest), - HardhatError.ERRORS.NETWORK.NOT_LOCAL_ACCOUNT, - { account: "0x000006d4548a3ac17d72b372ae1e416bf65b8ead" }, - ); - }); - - it("should not modify the json rpc request for other methods", async () => { - const input = [1, 2]; - const originalJsonRpcRequest = getJsonRpcRequest(1, "eth_sarasa", input); - - const jsonRpcRequest = { ...originalJsonRpcRequest }; - - await localAccounts.modifyRequest(jsonRpcRequest); - - assert.deepEqual(jsonRpcRequest, originalJsonRpcRequest); - }); - - it("should not modify the json rpc request if no address is given", async () => { - const originalJsonRpcRequest = getJsonRpcRequest(1, "eth_sign"); - - const jsonRpcRequest = { ...originalJsonRpcRequest }; - - await localAccounts.modifyRequest(jsonRpcRequest); - - assert.deepEqual(jsonRpcRequest, originalJsonRpcRequest); - }); - - it("should not modify the json rpc request if the address isn't one of the local ones", async () => { - const originalJsonRpcRequest = getJsonRpcRequest( - 1, - "eth_signTypedData_v4", - ["0x000006d4548a3ac17d72b372ae1e416bf65b8ead", {}], - ); - - const jsonRpcRequest = { ...originalJsonRpcRequest }; - - await localAccounts.modifyRequest(jsonRpcRequest); - - assert.deepEqual(jsonRpcRequest, originalJsonRpcRequest); - }); - }); -}); diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/unit/chain-id/chain-id.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/unit/chain-id/chain-id.ts deleted file mode 100644 index bfbf6e1b3b..0000000000 --- a/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/unit/chain-id/chain-id.ts +++ /dev/null @@ -1,81 +0,0 @@ -import assert from "node:assert/strict"; -import { beforeEach, describe, it } from "node:test"; - -import { HardhatError } from "@ignored/hardhat-vnext-errors"; -import { assertRejectsWithHardhatError } from "@nomicfoundation/hardhat-test-utils"; - -import { ChainIdValidator } from "../../../../../../../src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/chain-id/chain-id-validator.js"; -import { EthereumMockedProvider } from "../../ethereum-mocked-provider.js"; - -describe("ChainIdValidator", () => { - let mockedProvider: EthereumMockedProvider; - - beforeEach(() => { - mockedProvider = new EthereumMockedProvider(); - }); - - it("should fail when the configured chain id does not match the real chain id", async () => { - mockedProvider.setReturnValue("eth_chainId", () => "0x1"); - - const chainIdValidatorProvider = new ChainIdValidator(mockedProvider, 666); - - await assertRejectsWithHardhatError( - chainIdValidatorProvider.validate(), - HardhatError.ERRORS.NETWORK.INVALID_GLOBAL_CHAIN_ID, - { - configChainId: 666, - connectionChainId: 1, - }, - ); - }); - - it("should call the provider only once", async () => { - mockedProvider.setReturnValue("eth_chainId", () => "0x1"); - - const chainIdValidatorProvider = new ChainIdValidator(mockedProvider, 1); - - await chainIdValidatorProvider.validate(); - await chainIdValidatorProvider.validate(); - - assert.equal(mockedProvider.getTotalNumberOfCalls(), 1); - }); - - it("should use eth_chainId if supported", async () => { - mockedProvider.setReturnValue("eth_chainId", "0x1"); - mockedProvider.setReturnValue("net_version", "0x2"); - - const chainIdValidatorProvider = new ChainIdValidator(mockedProvider, 1); - - await chainIdValidatorProvider.validate(); - - // It should not fail because the chain id is the same as the one returned by eth_chainId - }); - - it("should use net_version if eth_chainId is not supported", async () => { - mockedProvider.setReturnValue("eth_chainId", () => { - throw new Error("Unsupported method"); - }); - mockedProvider.setReturnValue("net_version", "0x2"); - - const chainIdValidatorProvider = new ChainIdValidator(mockedProvider, 2); - - await chainIdValidatorProvider.validate(); - - // It should not fail because the chain id is the same as the one returned by net_version - }); - - it("should throw if both eth_chainId and net_version fail", async () => { - mockedProvider.setReturnValue("eth_chainId", () => { - throw new Error("Unsupported method"); - }); - - mockedProvider.setReturnValue("net_version", () => { - throw new Error("Unsupported method"); - }); - - const chainIdValidatorProvider = new ChainIdValidator(mockedProvider, 1); - - // eslint-disable-next-line no-restricted-syntax -- a non hardhat error is expected - await assert.rejects(() => chainIdValidatorProvider.validate()); - }); -}); diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/unit/gas-properties/automatic-gas-price.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/unit/gas-properties/automatic-gas-price.ts deleted file mode 100644 index 892753f1ba..0000000000 --- a/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/unit/gas-properties/automatic-gas-price.ts +++ /dev/null @@ -1,362 +0,0 @@ -import assert from "node:assert/strict"; -import { beforeEach, describe, it } from "node:test"; - -import { numberToHexString } from "@ignored/hardhat-vnext-utils/hex"; - -import { AutomaticGasPrice } from "../../../../../../../src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/gas-properties/automatic-gas-price.js"; -import { - getJsonRpcRequest, - getRequestParams, -} from "../../../../../../../src/internal/builtin-plugins/network-manager/json-rpc.js"; -import { EthereumMockedProvider } from "../../ethereum-mocked-provider.js"; - -describe("AutomaticGasPrice", () => { - let automaticGasPriceProvider: AutomaticGasPrice; - let mockedProvider: EthereumMockedProvider; - - const FIXED_GAS_PRICE = 1232; - - beforeEach(async () => { - mockedProvider = new EthereumMockedProvider(); - - mockedProvider.setReturnValue( - "eth_gasPrice", - numberToHexString(FIXED_GAS_PRICE), - ); - - automaticGasPriceProvider = new AutomaticGasPrice(mockedProvider); - }); - - describe("when the fee price values are provided", () => { - it("shouldn't replace the provided gasPrice", async () => { - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ - { - from: "0x0000000000000000000000000000000000000011", - to: "0x0000000000000000000000000000000000000011", - value: 1, - gasPrice: 456, - }, - ]); - - automaticGasPriceProvider.modifyRequest(jsonRpcRequest); - - assert.equal(getRequestParams(jsonRpcRequest)[0].gasPrice, 456); - }); - - it("shouldn't replace the provided maxFeePerGas and maxPriorityFeePerGas values", async () => { - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ - { - from: "0x0000000000000000000000000000000000000011", - to: "0x0000000000000000000000000000000000000011", - value: 1, - maxFeePerGas: 456, - maxPriorityFeePerGas: 789, - }, - ]); - - automaticGasPriceProvider.modifyRequest(jsonRpcRequest); - - assert.equal(getRequestParams(jsonRpcRequest)[0].maxFeePerGas, 456); - assert.equal( - getRequestParams(jsonRpcRequest)[0].maxPriorityFeePerGas, - 789, - ); - }); - }); - - describe("Default fee price values", () => { - describe("When eth_feeHistory is available and EIP1559 is supported", () => { - const latestBaseFeeInMockedProvider = 80; - - beforeEach(() => { - mockedProvider.setReturnValue("eth_feeHistory", { - baseFeePerGas: [ - numberToHexString(latestBaseFeeInMockedProvider), - numberToHexString( - Math.floor((latestBaseFeeInMockedProvider * 9) / 8), - ), - ], - reward: [["0x4"]], - }); - - mockedProvider.setReturnValue("eth_getBlockByNumber", { - baseFeePerGas: "0x1", - }); - }); - - it("should use the reward return value as default maxPriorityFeePerGas", async () => { - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ - { - from: "0x0000000000000000000000000000000000000011", - to: "0x0000000000000000000000000000000000000011", - value: 1, - maxFeePerGas: "0x99", - }, - ]); - - await automaticGasPriceProvider.modifyRequest(jsonRpcRequest); - - assert.equal( - getRequestParams(jsonRpcRequest)[0].maxPriorityFeePerGas, - "0x4", - ); - assert.equal(getRequestParams(jsonRpcRequest)[0].maxFeePerGas, "0x99"); - }); - - it("should add the reward to the maxFeePerGas if not big enough", async () => { - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ - { - from: "0x0000000000000000000000000000000000000011", - to: "0x0000000000000000000000000000000000000011", - value: 1, - maxFeePerGas: "0x1", - }, - ]); - - await automaticGasPriceProvider.modifyRequest(jsonRpcRequest); - - assert.equal( - getRequestParams(jsonRpcRequest)[0].maxPriorityFeePerGas, - "0x4", - ); - assert.equal(getRequestParams(jsonRpcRequest)[0].maxFeePerGas, "0x5"); - }); - - it("should use the expected max base fee of N blocks in the future if maxFeePerGas is missing", async () => { - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ - { - from: "0x0000000000000000000000000000000000000011", - to: "0x0000000000000000000000000000000000000011", - value: 1, - maxPriorityFeePerGas: "0x1", - }, - ]); - - const expectedBaseFee = Math.floor( - latestBaseFeeInMockedProvider * - (9 / 8) ** - Number( - AutomaticGasPrice.EIP1559_BASE_FEE_MAX_FULL_BLOCKS_PREFERENCE, - ), - ); - - await automaticGasPriceProvider.modifyRequest(jsonRpcRequest); - - assert.equal( - getRequestParams(jsonRpcRequest)[0].maxPriorityFeePerGas, - "0x1", - ); - assert.equal( - getRequestParams(jsonRpcRequest)[0].maxFeePerGas, - numberToHexString(expectedBaseFee), - ); - }); - }); - - describe("when the eth_feeHistory result causes maxPriorityFeePerGas to be 0 and eth_maxPriorityFeePerGas doesn't exist", () => { - const latestBaseFeeInMockedProvider = 80; - - beforeEach(() => { - mockedProvider.setReturnValue("eth_feeHistory", { - baseFeePerGas: [ - numberToHexString(latestBaseFeeInMockedProvider), - numberToHexString( - Math.floor((latestBaseFeeInMockedProvider * 9) / 8), - ), - ], - reward: [["0x0"]], - }); - - mockedProvider.setReturnValue("eth_getBlockByNumber", { - baseFeePerGas: "0x1", - }); - }); - - it("should use a non-zero maxPriorityFeePerGas", async () => { - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ - { - from: "0x0000000000000000000000000000000000000011", - to: "0x0000000000000000000000000000000000000011", - value: 1, - }, - ]); - - const expectedBaseFee = Math.floor( - latestBaseFeeInMockedProvider * - (9 / 8) ** - Number( - AutomaticGasPrice.EIP1559_BASE_FEE_MAX_FULL_BLOCKS_PREFERENCE, - ), - ); - - await automaticGasPriceProvider.modifyRequest(jsonRpcRequest); - - assert.equal( - getRequestParams(jsonRpcRequest)[0].maxPriorityFeePerGas, - "0x1", - ); - assert.equal( - getRequestParams(jsonRpcRequest)[0].maxFeePerGas, - numberToHexString(expectedBaseFee), - ); - }); - }); - - describe("when the eth_feeHistory result causes maxPriorityFeePerGas to be 0 and eth_maxPriorityFeePerGas exists", () => { - const latestBaseFeeInMockedProvider = 80; - - beforeEach(() => { - mockedProvider.setReturnValue("eth_feeHistory", { - baseFeePerGas: [ - numberToHexString(latestBaseFeeInMockedProvider), - numberToHexString( - Math.floor((latestBaseFeeInMockedProvider * 9) / 8), - ), - ], - reward: [["0x0"]], - }); - - mockedProvider.setReturnValue("eth_getBlockByNumber", { - baseFeePerGas: "0x1", - }); - - mockedProvider.setReturnValue("eth_maxPriorityFeePerGas", "0x12"); - }); - - it("should use the result of eth_maxPriorityFeePerGas as maxPriorityFeePerGas", async () => { - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ - { - from: "0x0000000000000000000000000000000000000011", - to: "0x0000000000000000000000000000000000000011", - value: 1, - }, - ]); - - const expectedBaseFee = Math.floor( - latestBaseFeeInMockedProvider * - (9 / 8) ** - Number( - AutomaticGasPrice.EIP1559_BASE_FEE_MAX_FULL_BLOCKS_PREFERENCE, - ), - ); - - await automaticGasPriceProvider.modifyRequest(jsonRpcRequest); - - assert.equal( - getRequestParams(jsonRpcRequest)[0].maxPriorityFeePerGas, - "0x12", - ); - assert.equal( - getRequestParams(jsonRpcRequest)[0].maxFeePerGas, - numberToHexString(expectedBaseFee), - ); - }); - }); - - describe("when eth_feeHistory is available and EIP1559 is not supported", () => { - const latestBaseFeeInMockedProvider = 80; - - beforeEach(() => { - mockedProvider.setReturnValue("eth_feeHistory", { - baseFeePerGas: [ - numberToHexString(latestBaseFeeInMockedProvider), - numberToHexString( - Math.floor((latestBaseFeeInMockedProvider * 9) / 8), - ), - ], - reward: [["0x4"]], - }); - - mockedProvider.setReturnValue("eth_getBlockByNumber", {}); - }); - - runTestUseLegacyGasPrice(); - }); - - describe("when eth_feeHistory is not available", () => { - beforeEach(() => { - mockedProvider.setReturnValue("eth_getBlockByNumber", {}); - }); - - runTestUseLegacyGasPrice(); - }); - - /** - * Group of tests that expect gasPrice to be used instead of EIP1559 fields - */ - function runTestUseLegacyGasPrice() { - it("should use gasPrice when nothing is provided", async () => { - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ - { - from: "0x0000000000000000000000000000000000000011", - to: "0x0000000000000000000000000000000000000011", - value: 1, - }, - ]); - - await automaticGasPriceProvider.modifyRequest(jsonRpcRequest); - - assert.equal( - getRequestParams(jsonRpcRequest)[0].gasPrice, - numberToHexString(FIXED_GAS_PRICE), - ); - }); - - it("should use gasPrice as default maxPriorityFeePerGas, adding it to maxFeePerGas if necessary", async () => { - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ - { - from: "0x0000000000000000000000000000000000000011", - to: "0x0000000000000000000000000000000000000011", - value: 1, - maxFeePerGas: "0x1", - }, - ]); - - await automaticGasPriceProvider.modifyRequest(jsonRpcRequest); - - assert.equal( - getRequestParams(jsonRpcRequest)[0].maxPriorityFeePerGas, - numberToHexString(FIXED_GAS_PRICE), - ); - assert.equal( - getRequestParams(jsonRpcRequest)[0].maxFeePerGas, - numberToHexString(FIXED_GAS_PRICE + 1), - ); - }); - - it("should use gasPrice as default maxFeePerGas, fixing maxPriorityFee to it if necessary", async () => { - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ - { - from: "0x0000000000000000000000000000000000000011", - to: "0x0000000000000000000000000000000000000011", - value: 1, - maxPriorityFeePerGas: numberToHexString(FIXED_GAS_PRICE + 2), - }, - ]); - - await automaticGasPriceProvider.modifyRequest(jsonRpcRequest); - - assert.equal( - getRequestParams(jsonRpcRequest)[0].maxPriorityFeePerGas, - numberToHexString(FIXED_GAS_PRICE + 2), - ); - assert.equal( - getRequestParams(jsonRpcRequest)[0].maxFeePerGas, - numberToHexString(FIXED_GAS_PRICE * 2 + 2), - ); - }); - } - }); - - it("should forward the other calls", async () => { - const jsonRpcRequest = getJsonRpcRequest( - 1, - "eth_getBlockByNumber", - [1, 2, 3, 4], - ); - - await automaticGasPriceProvider.modifyRequest(jsonRpcRequest); - - assert.deepEqual(jsonRpcRequest.params, [1, 2, 3, 4]); - }); -}); diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/unit/gas-properties/automatic-gas.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/unit/gas-properties/automatic-gas.ts deleted file mode 100644 index 4fd5fc4f09..0000000000 --- a/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/unit/gas-properties/automatic-gas.ts +++ /dev/null @@ -1,123 +0,0 @@ -import assert from "node:assert/strict"; -import { beforeEach, describe, it } from "node:test"; - -import { numberToHexString } from "@ignored/hardhat-vnext-utils/hex"; - -import { - AutomaticGas, - DEFAULT_GAS_MULTIPLIER, -} from "../../../../../../../src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/gas-properties/automatic-gas.js"; -import { - getJsonRpcRequest, - getRequestParams, -} from "../../../../../../../src/internal/builtin-plugins/network-manager/json-rpc.js"; -import { EthereumMockedProvider } from "../../ethereum-mocked-provider.js"; - -describe("AutomaticGas", () => { - let automaticGas: AutomaticGas; - let mockedProvider: EthereumMockedProvider; - - const FIXED_GAS_LIMIT = 1231; - const GAS_MULTIPLIER = 1.337; - - beforeEach(() => { - mockedProvider = new EthereumMockedProvider(); - - mockedProvider.setReturnValue("eth_getBlockByNumber", { - gasLimit: numberToHexString(FIXED_GAS_LIMIT * 1000), - }); - - mockedProvider.setReturnValue( - "eth_estimateGas", - numberToHexString(FIXED_GAS_LIMIT), - ); - - automaticGas = new AutomaticGas(mockedProvider, GAS_MULTIPLIER); - }); - - it("should estimate gas automatically if not present", async () => { - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ - { - from: "0x0000000000000000000000000000000000000011", - to: "0x0000000000000000000000000000000000000011", - value: 1, - }, - ]); - - await automaticGas.modifyRequest(jsonRpcRequest); - - assert.equal( - getRequestParams(jsonRpcRequest)[0].gas, - numberToHexString(Math.floor(FIXED_GAS_LIMIT * GAS_MULTIPLIER)), - ); - }); - - it("should support different gas multipliers", async () => { - const GAS_MULTIPLIER2 = 123; - - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ - { - from: "0x0000000000000000000000000000000000000011", - to: "0x0000000000000000000000000000000000000011", - value: 1, - }, - ]); - - automaticGas = new AutomaticGas(mockedProvider, GAS_MULTIPLIER2); - - await automaticGas.modifyRequest(jsonRpcRequest); - - assert.equal( - getRequestParams(jsonRpcRequest)[0].gas, - numberToHexString(Math.floor(FIXED_GAS_LIMIT * GAS_MULTIPLIER2)), - ); - }); - - it("should have a default multiplier", async () => { - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ - { - from: "0x0000000000000000000000000000000000000011", - to: "0x0000000000000000000000000000000000000011", - value: 1, - }, - ]); - - automaticGas = new AutomaticGas(mockedProvider); - - await automaticGas.modifyRequest(jsonRpcRequest); - - assert.equal( - getRequestParams(jsonRpcRequest)[0].gas, - numberToHexString(Math.floor(FIXED_GAS_LIMIT * DEFAULT_GAS_MULTIPLIER)), - ); - }); - - it("shouldn't replace the provided gas", async () => { - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ - { - from: "0x0000000000000000000000000000000000000011", - to: "0x0000000000000000000000000000000000000011", - value: 1, - gas: 567, - }, - ]); - - await automaticGas.modifyRequest(jsonRpcRequest); - - assert.equal(getRequestParams(jsonRpcRequest)[0].gas, 567); - }); - - it("should forward the other calls", async () => { - const jsonRpcRequest = getJsonRpcRequest(1, "eth_randomMethod", [ - { - from: "0x0000000000000000000000000000000000000011", - to: "0x0000000000000000000000000000000000000011", - value: 1, - }, - ]); - - await automaticGas.modifyRequest(jsonRpcRequest); - - assert.equal(getRequestParams(jsonRpcRequest)[0].gas, undefined); - }); -}); diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/unit/gas-properties/fixed-gas-price.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/unit/gas-properties/fixed-gas-price.ts deleted file mode 100644 index 280b4e42e9..0000000000 --- a/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/unit/gas-properties/fixed-gas-price.ts +++ /dev/null @@ -1,68 +0,0 @@ -import assert from "node:assert/strict"; -import { beforeEach, describe, it } from "node:test"; - -import { numberToHexString } from "@ignored/hardhat-vnext-utils/hex"; - -import { FixedGasPrice } from "../../../../../../../src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/gas-properties/fixed-gas-price.js"; -import { - getJsonRpcRequest, - getRequestParams, -} from "../../../../../../../src/internal/builtin-plugins/network-manager/json-rpc.js"; - -describe("FixedGasPrice", () => { - let fixedGasPriceProvider: FixedGasPrice; - - const FIXED_GAS_PRICE = 1234n; - - beforeEach(() => { - fixedGasPriceProvider = new FixedGasPrice( - numberToHexString(FIXED_GAS_PRICE), - ); - }); - - it("should set the fixed gasPrice if not present", async () => { - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ - { - from: "0x0000000000000000000000000000000000000011", - to: "0x0000000000000000000000000000000000000011", - value: 1, - }, - ]); - - fixedGasPriceProvider.modifyRequest(jsonRpcRequest); - - assert.equal( - getRequestParams(jsonRpcRequest)[0].gasPrice, - numberToHexString(FIXED_GAS_PRICE), - ); - }); - - it("shouldn't replace the provided gasPrice", async () => { - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ - { - from: "0x0000000000000000000000000000000000000011", - to: "0x0000000000000000000000000000000000000011", - value: 1, - gasPrice: 14567, - }, - ]); - - fixedGasPriceProvider.modifyRequest(jsonRpcRequest); - - assert.equal(getRequestParams(jsonRpcRequest)[0].gasPrice, 14567); - }); - - it("should forward the other calls and not modify the gasPrice", async () => { - const jsonRpcRequest = getJsonRpcRequest(1, "eth_gasPrice", [ - { - from: "0x0000000000000000000000000000000000000011", - to: "0x0000000000000000000000000000000000000011", - value: 1, - }, - ]); - - fixedGasPriceProvider.modifyRequest(jsonRpcRequest); - - assert.equal(getRequestParams(jsonRpcRequest)[0].gas, undefined); - }); -}); diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/unit/gas-properties/fixed-gas.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/unit/gas-properties/fixed-gas.ts deleted file mode 100644 index b62b35fa9b..0000000000 --- a/v-next/hardhat/test/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/unit/gas-properties/fixed-gas.ts +++ /dev/null @@ -1,66 +0,0 @@ -import assert from "node:assert/strict"; -import { beforeEach, describe, it } from "node:test"; - -import { numberToHexString } from "@ignored/hardhat-vnext-utils/hex"; - -import { FixedGas } from "../../../../../../../src/internal/builtin-plugins/network-manager/json-rpc-request-modifiers/gas-properties/fixed-gas.js"; -import { - getJsonRpcRequest, - getRequestParams, -} from "../../../../../../../src/internal/builtin-plugins/network-manager/json-rpc.js"; - -describe("FixedGas", () => { - let fixedGas: FixedGas; - - const FIXED_GAS_LIMIT = 1233n; - - beforeEach(() => { - fixedGas = new FixedGas(numberToHexString(FIXED_GAS_LIMIT)); - }); - - it("should set the fixed gas if not present", async () => { - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ - { - from: "0x0000000000000000000000000000000000000011", - to: "0x0000000000000000000000000000000000000011", - value: 1, - }, - ]); - - fixedGas.modifyRequest(jsonRpcRequest); - - assert.equal( - getRequestParams(jsonRpcRequest)[0].gas, - numberToHexString(FIXED_GAS_LIMIT), - ); - }); - - it("shouldn't replace the provided gas", async () => { - const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ - { - from: "0x0000000000000000000000000000000000000011", - to: "0x0000000000000000000000000000000000000011", - value: 1, - gas: 1456, - }, - ]); - - fixedGas.modifyRequest(jsonRpcRequest); - - assert.equal(getRequestParams(jsonRpcRequest)[0].gas, 1456); - }); - - it("should forward the other calls and not modify the gas", async () => { - const jsonRpcRequest = getJsonRpcRequest(1, "eth_estimateGas", [ - { - from: "0x0000000000000000000000000000000000000011", - to: "0x0000000000000000000000000000000000000011", - value: 1, - }, - ]); - - fixedGas.modifyRequest(jsonRpcRequest); - - assert.equal(getRequestParams(jsonRpcRequest)[0].gas, undefined); - }); -}); From 05ce4ca76cdcb33d17d96e566d0e2268eb0062d5 Mon Sep 17 00:00:00 2001 From: ChrisD <18092467+ChristopherDedominici@users.noreply.github.com> Date: Wed, 27 Nov 2024 12:01:12 +0100 Subject: [PATCH 02/17] new handlers logic + chain-id handler --- .../network-manager/hook-handlers/network.ts | 52 +++++---- .../handlers/chain-id/chain-id.ts | 58 ++++++++++ .../handlers/chain-id/handler.ts | 58 ++++++++++ .../request-handlers/hanlders-array.ts | 33 ++++++ .../network-manager/request-handlers/types.ts | 20 ++++ .../network-manager/request-handlers/e2e.ts | 34 ++++++ .../ethereum-mocked-provider.ts | 77 +++++++++++++ .../handlers/chain-id/handler.ts | 106 ++++++++++++++++++ .../request-handlers/hooks-mock.ts | 70 ++++++++++++ 9 files changed, 484 insertions(+), 24 deletions(-) create mode 100644 v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/chain-id/chain-id.ts create mode 100644 v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/chain-id/handler.ts create mode 100644 v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/hanlders-array.ts create mode 100644 v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/types.ts create mode 100644 v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/e2e.ts create mode 100644 v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/ethereum-mocked-provider.ts create mode 100644 v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/chain-id/handler.ts create mode 100644 v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/hooks-mock.ts diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/hook-handlers/network.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/hook-handlers/network.ts index 4be55e27ec..5c7f807b1a 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/hook-handlers/network.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/hook-handlers/network.ts @@ -2,6 +2,7 @@ import type { JsonRpcRequest, JsonRpcResponse, } from "../../../../types/providers.js"; +import type { RequestHandler } from "../request-handlers/types.js"; import type { HookContext, NetworkHooks, @@ -11,14 +12,18 @@ import type { NetworkConnection, } from "@ignored/hardhat-vnext/types/network"; -import { JsonRpcRequestModifier } from "../json-rpc-request-modifiers/json-rpc-request-modifier.js"; +import { deepClone } from "@ignored/hardhat-vnext-utils/lang"; + +import { isJsonRpcResponse } from "../json-rpc.js"; +import { createHandlersArray } from "../request-handlers/hanlders-array.js"; export default async (): Promise> => { - // This map is necessary because Hardhat V3 supports multiple network connections, requiring us to track them - // to apply the appropriate modifiers to each request. - // When a connection is closed, it is removed from the map. Refer to "closeConnection" at the end of the file. - const jsonRpcRequestModifiers: Map = - new Map(); + // This map is essential for managing multiple network connections in Hardhat V3. + // Since Hardhat V3 supports multiple connections, we use this map to track each one + // and associate it with the corresponding handlers array. + // When a connection is closed, its associated handlers array is removed from the map. + // See the "closeConnection" function at the end of the file for more details. + const requestHandlersPerConnection: Map = new Map(); const handlers: Partial = { async onRequest( @@ -31,31 +36,30 @@ export default async (): Promise> => { nextJsonRpcRequest: JsonRpcRequest, ) => Promise, ) { - let jsonRpcRequestModifier = jsonRpcRequestModifiers.get( + let requestHandlers = requestHandlersPerConnection.get( networkConnection.id, ); - if (jsonRpcRequestModifier === undefined) { - jsonRpcRequestModifier = new JsonRpcRequestModifier(networkConnection); - - jsonRpcRequestModifiers.set( - networkConnection.id, - jsonRpcRequestModifier, - ); + if (requestHandlers === undefined) { + requestHandlers = createHandlersArray(networkConnection); + requestHandlersPerConnection.set(networkConnection.id, requestHandlers); } - const newJsonRpcRequest = - await jsonRpcRequestModifier.createModifiedJsonRpcRequest( - jsonRpcRequest, - ); + // We clone the request to avoid interfering with other hook handlers that + // might be using the original request. + let request = await deepClone(jsonRpcRequest); + + for (const handler of requestHandlers) { + const newRequestOrResponse = await handler.handle(request); - const res = await jsonRpcRequestModifier.getResponse(jsonRpcRequest); + if (isJsonRpcResponse(newRequestOrResponse)) { + return newRequestOrResponse; + } - if (res !== null) { - return res; + request = newRequestOrResponse; } - return next(context, networkConnection, newJsonRpcRequest); + return next(context, networkConnection, request); }, async closeConnection( @@ -66,8 +70,8 @@ export default async (): Promise> => { nextNetworkConnection: NetworkConnection, ) => Promise, ): Promise { - if (jsonRpcRequestModifiers.has(networkConnection.id) === true) { - jsonRpcRequestModifiers.delete(networkConnection.id); + if (requestHandlersPerConnection.has(networkConnection.id) === true) { + requestHandlersPerConnection.delete(networkConnection.id); } return next(context, networkConnection); diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/chain-id/chain-id.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/chain-id/chain-id.ts new file mode 100644 index 0000000000..f90506fd35 --- /dev/null +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/chain-id/chain-id.ts @@ -0,0 +1,58 @@ +import type { EthereumProvider } from "../../../../../../types/providers.js"; + +import { assertHardhatInvariant } from "@ignored/hardhat-vnext-errors"; +import { + hexStringToNumber, + isPrefixedHexString, +} from "@ignored/hardhat-vnext-utils/hex"; + +/** + * This class is responsible for retrieving the chain ID of the network. + * It uses the provider to fetch the chain ID via two methods: 'eth_chainId' and, + * as a fallback, 'net_version' if the first one fails. The chain ID is cached + * after being retrieved to avoid redundant requests. + */ +export class ChainId { + readonly #provider: EthereumProvider; + + #chainId: number | undefined; + + constructor(provider: EthereumProvider) { + this.#provider = provider; + } + + public async getChainId(): Promise { + if (this.#chainId === undefined) { + try { + this.#chainId = await this.#getChainIdFromEthChainId(); + } catch { + // If eth_chainId fails we default to net_version + this.#chainId = await this.#getChainIdFromEthNetVersion(); + } + } + + return this.#chainId; + } + + async #getChainIdFromEthChainId(): Promise { + const id = await this.#provider.request({ + method: "eth_chainId", + }); + + assertHardhatInvariant(typeof id === "string", "id should be a string"); + + return hexStringToNumber(id); + } + + async #getChainIdFromEthNetVersion(): Promise { + const id = await this.#provider.request({ + method: "net_version", + }); + + assertHardhatInvariant(typeof id === "string", "id should be a string"); + + // There's a node returning this as decimal instead of QUANTITY. + // TODO: from V2 - Document here which node does that + return isPrefixedHexString(id) ? hexStringToNumber(id) : parseInt(id, 10); + } +} diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/chain-id/handler.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/chain-id/handler.ts new file mode 100644 index 0000000000..6db2351324 --- /dev/null +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/chain-id/handler.ts @@ -0,0 +1,58 @@ +import type { + EthereumProvider, + JsonRpcRequest, + JsonRpcResponse, +} from "../../../../../../types/providers.js"; +import type { RequestHandler } from "../../types.js"; + +import { HardhatError } from "@ignored/hardhat-vnext-errors"; + +import { ChainId } from "./chain-id.js"; + +/** + * This class validates that the current provider's chain ID matches + * an expected value. If the actual chain ID differs from the expected one, it throws a + * HardhatError to signal a network configuration mismatch. Once validated, further checks + * are skipped to avoid redundant validations. + */ +export class ChainIdValidatorHandler implements RequestHandler { + readonly #chainId: ChainId; + readonly #expectedChainId: number; + #alreadyValidated = false; + + constructor(provider: EthereumProvider, expectedChainId: number) { + this.#chainId = new ChainId(provider); + this.#expectedChainId = expectedChainId; + } + + public async handle( + jsonRpcRequest: JsonRpcRequest, + ): Promise { + if ( + jsonRpcRequest.method === "eth_chainId" || + jsonRpcRequest.method === "net_version" + ) { + return jsonRpcRequest; + } + + if (this.#alreadyValidated) { + return jsonRpcRequest; + } + + const actualChainId = await this.#chainId.getChainId(); + + if (actualChainId !== this.#expectedChainId) { + throw new HardhatError( + HardhatError.ERRORS.NETWORK.INVALID_GLOBAL_CHAIN_ID, + { + configChainId: this.#expectedChainId, + connectionChainId: actualChainId, + }, + ); + } + + this.#alreadyValidated = true; + + return jsonRpcRequest; + } +} diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/hanlders-array.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/hanlders-array.ts new file mode 100644 index 0000000000..07c20a4380 --- /dev/null +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/hanlders-array.ts @@ -0,0 +1,33 @@ +import type { RequestHandler } from "./types.js"; +import type { + ChainType, + NetworkConnection, +} from "../../../../types/network.js"; + +import { ChainIdValidatorHandler } from "./handlers/chain-id/handler.js"; + +// TODO: finish docs +/** + * + * This function returns an handlers array based on the values in the networkConfig and.... + * The order of the handlers, if all are present, is: chain handler, gas handler and accounts handler. + * The order is important to get a correct result when the handler are executed + */ +export function createHandlersArray( + networkConnection: NetworkConnection, +): RequestHandler[] { + const requestHandlers = []; + + if (networkConnection.networkConfig.type === "http") { + if (networkConnection.networkConfig.chainId !== undefined) { + requestHandlers.push( + new ChainIdValidatorHandler( + networkConnection.provider, + networkConnection.networkConfig.chainId, + ), + ); + } + } + + return requestHandlers; +} diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/types.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/types.ts new file mode 100644 index 0000000000..803f53dd74 --- /dev/null +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/types.ts @@ -0,0 +1,20 @@ +import type { + JsonRpcRequest, + JsonRpcResponse, +} from "../../../../types/providers.js"; + +/** + * Common interface for request handlers, which can either return a new + * modified request, or a response. + * + * If they return a request, it's passed to the next handler, or to the "next" + * function if there are no more handlers. + * + * If they return a response, it's returned immediately. + * + */ +export interface RequestHandler { + handle( + jsonRpcRequest: JsonRpcRequest, + ): Promise; +} diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/e2e.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/e2e.ts new file mode 100644 index 0000000000..ddc509435d --- /dev/null +++ b/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/e2e.ts @@ -0,0 +1,34 @@ +import { describe, it } from "node:test"; + +import { createMockedNetworkHre } from "./hooks-mock.js"; + +// Test that the request and its additional sub-request (when present) +// are correctly modified in the "onRequest" hook handler. +// These tests simulate a real scenario where the user calls "await connection.provider.request(jsonRpcRequest)". +describe("request-handlers - e2e", () => { + it("should successfully executes all the handlers", async () => { + const hre = await createMockedNetworkHre( + { + networks: { + localhost: { + type: "http", + url: "http://localhost:8545", + chainId: 1, + }, + }, + }, + { + eth_chainId: "0x1", + }, + ); + + // Use the localhost network for these tests because the modifier is only + // applicable to HTTP networks. EDR networks do not require this modifier. + const connection = await hre.network.connect("localhost"); + + await connection.provider.request({ + method: "eth_sendTransaction", + params: [], + }); + }); +}); diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/ethereum-mocked-provider.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/ethereum-mocked-provider.ts new file mode 100644 index 0000000000..acc3e21e35 --- /dev/null +++ b/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/ethereum-mocked-provider.ts @@ -0,0 +1,77 @@ +import type { + EIP1193Provider, + JsonRpcRequest, + JsonRpcResponse, + RequestArguments, +} from "../../../../../src/types/providers.js"; + +import EventEmitter from "node:events"; + +// This mock is used in the unit tests to simulate the return value of the "request" method +export class EthereumMockedProvider + extends EventEmitter + implements EIP1193Provider +{ + // Record + readonly #returnValues: Record = {}; + + readonly #latestParams: Record = {}; + + readonly #numberOfCalls: { [method: string]: number } = {}; + + // If a lambda is passed as value, it's return value is used. + public setReturnValue(method: string, value: any): void { + this.#returnValues[method] = value; + } + + public getNumberOfCalls(method: string): number { + if (this.#numberOfCalls[method] === undefined) { + return 0; + } + + return this.#numberOfCalls[method]; + } + + public getLatestParams(method: string): any { + return this.#latestParams[method]; + } + + public getTotalNumberOfCalls(): number { + return Object.values(this.#numberOfCalls).reduce((p, c) => p + c, 0); + } + + public async request({ + method, + params = [], + }: RequestArguments): Promise { + // stringify the params to make sure they are serializable + JSON.stringify(params); + + this.#latestParams[method] = params; + + if (this.#numberOfCalls[method] === undefined) { + this.#numberOfCalls[method] = 1; + } else { + this.#numberOfCalls[method] += 1; + } + + let ret = this.#returnValues[method]; + + if (ret instanceof Function) { + ret = ret(); + } + + return ret; + } + + public send(_method: string, _params?: unknown[]): Promise { + return Promise.resolve(null); + } + + public sendAsync( + _jsonRpcRequest: JsonRpcRequest, + _callback: (error: any, jsonRpcResponse: JsonRpcResponse) => void, + ): void {} + + public async close(): Promise {} +} diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/chain-id/handler.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/chain-id/handler.ts new file mode 100644 index 0000000000..31f93c32ed --- /dev/null +++ b/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/chain-id/handler.ts @@ -0,0 +1,106 @@ +import assert from "node:assert/strict"; +import { beforeEach, describe, it } from "node:test"; + +import { HardhatError } from "@ignored/hardhat-vnext-errors"; +import { + assertRejects, + assertRejectsWithHardhatError, +} from "@nomicfoundation/hardhat-test-utils"; + +import { getJsonRpcRequest } from "../../../../../../../src/internal/builtin-plugins/network-manager/json-rpc.js"; +import { ChainIdValidatorHandler } from "../../../../../../../src/internal/builtin-plugins/network-manager/request-handlers/handlers/chain-id/handler.js"; +import { EthereumMockedProvider } from "../../ethereum-mocked-provider.js"; + +describe("ChainIdValidatorHandler", () => { + let mockedProvider: EthereumMockedProvider; + + const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ + { + from: "0x0000000000000000000000000000000000000011", + to: "0x0000000000000000000000000000000000000011", + }, + ]); + + beforeEach(() => { + mockedProvider = new EthereumMockedProvider(); + }); + + it("should fail when the configured chain id does not match the real chain id", async () => { + mockedProvider.setReturnValue("eth_chainId", () => "0x1"); + + const chainIdValidatorHandler = new ChainIdValidatorHandler( + mockedProvider, + 123, + ); + + await assertRejectsWithHardhatError( + chainIdValidatorHandler.handle(jsonRpcRequest), + HardhatError.ERRORS.NETWORK.INVALID_GLOBAL_CHAIN_ID, + { + configChainId: 123, + connectionChainId: 1, + }, + ); + }); + + it("should call the provider only once", async () => { + mockedProvider.setReturnValue("eth_chainId", () => "0x1"); + + const chainIdValidatorHandler = new ChainIdValidatorHandler( + mockedProvider, + 1, + ); + + await chainIdValidatorHandler.handle(jsonRpcRequest); + await chainIdValidatorHandler.handle(jsonRpcRequest); + + assert.equal(mockedProvider.getTotalNumberOfCalls(), 1); + }); + + it("should use eth_chainId if supported", async () => { + mockedProvider.setReturnValue("eth_chainId", "0x1"); + mockedProvider.setReturnValue("net_version", "0x2"); + + const chainIdValidatorHandler = new ChainIdValidatorHandler( + mockedProvider, + 1, + ); + + await chainIdValidatorHandler.handle(jsonRpcRequest); + + // It should not fail because the chain id is the same as the one returned by eth_chainId + }); + + it("should use net_version if eth_chainId is not supported", async () => { + mockedProvider.setReturnValue("eth_chainId", () => { + throw new Error("Unsupported method"); + }); + mockedProvider.setReturnValue("net_version", "0x2"); + + const chainIdValidatorHandler = new ChainIdValidatorHandler( + mockedProvider, + 2, + ); + + await chainIdValidatorHandler.handle(jsonRpcRequest); + + // It should not fail because the chain id is the same as the one returned by net_version + }); + + it("should throw if both eth_chainId and net_version fail", async () => { + mockedProvider.setReturnValue("eth_chainId", () => { + throw new Error("Unsupported method"); + }); + + mockedProvider.setReturnValue("net_version", () => { + throw new Error("Unsupported method"); + }); + + const chainIdValidatorHandler = new ChainIdValidatorHandler( + mockedProvider, + 1, + ); + + await assertRejects(() => chainIdValidatorHandler.handle(jsonRpcRequest)); + }); +}); diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/hooks-mock.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/hooks-mock.ts new file mode 100644 index 0000000000..1a62f253d6 --- /dev/null +++ b/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/hooks-mock.ts @@ -0,0 +1,70 @@ +import type { HardhatUserConfig } from "../../../../../src/config.js"; +import type { HookContext } from "../../../../../src/types/hooks.js"; +import type { HardhatRuntimeEnvironment } from "../../../../../src/types/hre.js"; +import type { + ChainType, + NetworkConnection, +} from "../../../../../src/types/network.js"; +import type { + JsonRpcRequest, + JsonRpcResponse, +} from "../../../../../src/types/providers.js"; + +import { createHardhatRuntimeEnvironment } from "../../../../../src/hre.js"; + +// We override the network hook handlers to set two dynamic hook handler +// categories, which will take precedence over the default ones. +// +// This means that they are going to be the first ones to be called, so we +// set the handler that we want to test as the first one, and the mocked +// one as the second one. +// +// In this way, the function "next" in the first handler will call the mock. +export async function createMockedNetworkHre( + hardhatUserConfig: HardhatUserConfig, + returnValues: Record = {}, +): Promise { + const mockedResponse: JsonRpcResponse = { + jsonrpc: "2.0", + id: 1, + result: [], + }; + + const hre = await createHardhatRuntimeEnvironment(hardhatUserConfig); + + hre.hooks.registerHandlers("network", { + onRequest: async ( + _context: HookContext, + _networkConnection: NetworkConnection, + jsonRpcRequest: JsonRpcRequest, + _next: ( + nextContext: HookContext, + nextNetworkConnection: NetworkConnection, + nextJsonRpcRequest: JsonRpcRequest, + ) => Promise, + ) => { + if (returnValues[jsonRpcRequest.method] !== undefined) { + mockedResponse.result = returnValues[jsonRpcRequest.method]; + return mockedResponse; + } + + // Returns the modified jsonRpcRequest as a response, so it can be verified in the test that it + // was successfully modified + mockedResponse.result = jsonRpcRequest.params; + return mockedResponse; + }, + }); + + // We set the default network hook handlers again, to take precedence over the + // mocked one. + const { default: networkHookHandlerHandlersFactory } = await import( + "../../../../../src/internal/builtin-plugins/network-manager/hook-handlers/network.js" + ); + + hre.hooks.registerHandlers( + "network", + await networkHookHandlerHandlersFactory(), + ); + + return hre; +} From e27875f667f7ef338f974bec60af9a36a2d3e70e Mon Sep 17 00:00:00 2001 From: ChrisD <18092467+ChristopherDedominici@users.noreply.github.com> Date: Wed, 27 Nov 2024 15:21:28 +0100 Subject: [PATCH 03/17] rename chain-id files --- .../handlers/chain-id/{handler.ts => chain-id-handler.ts} | 0 .../handlers/chain-id/{handler.ts => chain-id-handler.ts} | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/chain-id/{handler.ts => chain-id-handler.ts} (100%) rename v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/chain-id/{handler.ts => chain-id-handler.ts} (98%) diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/chain-id/handler.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/chain-id/chain-id-handler.ts similarity index 100% rename from v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/chain-id/handler.ts rename to v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/chain-id/chain-id-handler.ts diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/chain-id/handler.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/chain-id/chain-id-handler.ts similarity index 98% rename from v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/chain-id/handler.ts rename to v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/chain-id/chain-id-handler.ts index 31f93c32ed..2995ee015a 100644 --- a/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/chain-id/handler.ts +++ b/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/chain-id/chain-id-handler.ts @@ -8,7 +8,7 @@ import { } from "@nomicfoundation/hardhat-test-utils"; import { getJsonRpcRequest } from "../../../../../../../src/internal/builtin-plugins/network-manager/json-rpc.js"; -import { ChainIdValidatorHandler } from "../../../../../../../src/internal/builtin-plugins/network-manager/request-handlers/handlers/chain-id/handler.js"; +import { ChainIdValidatorHandler } from "../../../../../../../src/internal/builtin-plugins/network-manager/request-handlers/handlers/chain-id/chain-id-handler.js"; import { EthereumMockedProvider } from "../../ethereum-mocked-provider.js"; describe("ChainIdValidatorHandler", () => { From ffb423096ed0a3356dcadcee82f80dbbc8466b27 Mon Sep 17 00:00:00 2001 From: ChrisD <18092467+ChristopherDedominici@users.noreply.github.com> Date: Wed, 27 Nov 2024 15:41:58 +0100 Subject: [PATCH 04/17] add automatic and fixed gas handlers --- .../handlers/gas/automatic-gas-handler.ts | 50 +++++++ .../handlers/gas/fixed-gas-handler.ts | 39 ++++++ .../handlers/gas/multiplied-gas-estimation.ts | 87 ++++++++++++ .../network-manager/request-handlers/e2e.ts | 72 +++++++++- .../handlers/gas/automatic-gas-handler.ts | 129 ++++++++++++++++++ .../handlers/gas/fixed-gas-handler.ts | 66 +++++++++ 6 files changed, 440 insertions(+), 3 deletions(-) create mode 100644 v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/automatic-gas-handler.ts create mode 100644 v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/fixed-gas-handler.ts create mode 100644 v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/multiplied-gas-estimation.ts create mode 100644 v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/automatic-gas-handler.ts create mode 100644 v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/fixed-gas-handler.ts diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/automatic-gas-handler.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/automatic-gas-handler.ts new file mode 100644 index 0000000000..163a28c515 --- /dev/null +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/automatic-gas-handler.ts @@ -0,0 +1,50 @@ +import type { + EthereumProvider, + JsonRpcRequest, + JsonRpcResponse, +} from "../../../../../../types/providers.js"; +import type { RequestHandler } from "../../types.js"; + +import { getRequestParams } from "../../../json-rpc.js"; + +import { MultipliedGasEstimation } from "./multiplied-gas-estimation.js"; + +export const DEFAULT_GAS_MULTIPLIER = 1; + +/** + * This class modifies transaction requests by automatically estimating the gas required, + * applying a multiplier to the estimated gas. + * + * It extends the `MultipliedGasEstimation` class + * to handle the gas estimation logic. If no gas value is provided in the transaction, + * the gas is automatically estimated before being added to the request. + */ +export class AutomaticGasHandler + extends MultipliedGasEstimation + implements RequestHandler +{ + constructor( + provider: EthereumProvider, + gasMultiplier: number = DEFAULT_GAS_MULTIPLIER, + ) { + super(provider, gasMultiplier); + } + + public async handle( + jsonRpcRequest: JsonRpcRequest, + ): Promise { + if (jsonRpcRequest.method !== "eth_sendTransaction") { + return jsonRpcRequest; + } + + const params = getRequestParams(jsonRpcRequest); + + // TODO: from V2 - Should we validate this type? + const tx = params[0]; + if (tx !== undefined && tx.gas === undefined) { + tx.gas = await this.getMultipliedGasEstimation(params); + } + + return jsonRpcRequest; + } +} diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/fixed-gas-handler.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/fixed-gas-handler.ts new file mode 100644 index 0000000000..352359f050 --- /dev/null +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/fixed-gas-handler.ts @@ -0,0 +1,39 @@ +import type { + JsonRpcRequest, + JsonRpcResponse, +} from "../../../../../../types/providers.js"; +import type { RequestHandler } from "../../types.js"; +import type { PrefixedHexString } from "@ignored/hardhat-vnext-utils/hex"; + +import { getRequestParams } from "../../../json-rpc.js"; + +/** + * This class ensures that a fixed gas is applied to transaction requests. + * For `eth_sendTransaction` requests, it sets the gas field with the value provided via the class constructor, if it hasn't been specified already. + */ +export class FixedGasHandler implements RequestHandler { + readonly #gas: PrefixedHexString; + + constructor(gas: PrefixedHexString) { + this.#gas = gas; + } + + public async handle( + jsonRpcRequest: JsonRpcRequest, + ): Promise { + if (jsonRpcRequest.method !== "eth_sendTransaction") { + return jsonRpcRequest; + } + + const params = getRequestParams(jsonRpcRequest); + + // TODO: from V2 - Should we validate this type? + const tx = params[0]; + + if (tx !== undefined && tx.gas === undefined) { + tx.gas = this.#gas; + } + + return jsonRpcRequest; + } +} diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/multiplied-gas-estimation.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/multiplied-gas-estimation.ts new file mode 100644 index 0000000000..a7791a1e2d --- /dev/null +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/multiplied-gas-estimation.ts @@ -0,0 +1,87 @@ +import type { EthereumProvider } from "../../../../../../types/providers.js"; + +import { assertHardhatInvariant } from "@ignored/hardhat-vnext-errors"; +import { ensureError } from "@ignored/hardhat-vnext-utils/error"; +import { + hexStringToNumber, + numberToHexString, +} from "@ignored/hardhat-vnext-utils/hex"; + +/** + * This class handles gas estimation for transactions by applying a multiplier to the estimated gas value. + * It requests a gas estimation from the provider and multiplies it by a predefined gas multiplier, ensuring the gas does not exceed the block's gas limit. + * If an execution error occurs, the method returns the block's gas limit instead. + * The block gas limit is cached after the first retrieval to optimize performance. + */ +export abstract class MultipliedGasEstimation { + readonly #provider: EthereumProvider; + readonly #gasMultiplier: number; + + #blockGasLimit: number | undefined; + + constructor(provider: EthereumProvider, gasMultiplier: number) { + this.#provider = provider; + this.#gasMultiplier = gasMultiplier; + } + + protected async getMultipliedGasEstimation(params: any[]): Promise { + try { + const realEstimation = await this.#provider.request({ + method: "eth_estimateGas", + params, + }); + + assertHardhatInvariant( + typeof realEstimation === "string", + "realEstimation should be a string", + ); + + if (this.#gasMultiplier === 1) { + return realEstimation; + } + + const normalGas = hexStringToNumber(realEstimation); + + const gasLimit = await this.#getBlockGasLimit(); + + const multiplied = Math.floor(normalGas * this.#gasMultiplier); + + const gas = multiplied > gasLimit ? gasLimit - 1 : multiplied; + + return numberToHexString(gas); + } catch (error) { + ensureError(error); + + if (error.message.toLowerCase().includes("execution error")) { + const blockGasLimitTmp = await this.#getBlockGasLimit(); + return numberToHexString(blockGasLimitTmp); + } + + throw error; + } + } + + async #getBlockGasLimit(): Promise { + if (this.#blockGasLimit === undefined) { + const latestBlock = await this.#provider.request({ + method: "eth_getBlockByNumber", + params: ["latest", false], + }); + + assertHardhatInvariant( + typeof latestBlock === "object" && + latestBlock !== null && + "gasLimit" in latestBlock && + typeof latestBlock.gasLimit === "string", + "latestBlock should have a gasLimit", + ); + + const fetchedGasLimit = hexStringToNumber(latestBlock.gasLimit); + + // We store a lower value in case the gas limit varies slightly + this.#blockGasLimit = Math.floor(fetchedGasLimit * 0.95); + } + + return this.#blockGasLimit; + } +} diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/e2e.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/e2e.ts index ddc509435d..16bb61995d 100644 --- a/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/e2e.ts +++ b/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/e2e.ts @@ -1,16 +1,66 @@ +import assert from "node:assert/strict"; import { describe, it } from "node:test"; +import { numberToHexString } from "@ignored/hardhat-vnext-utils/hex"; + import { createMockedNetworkHre } from "./hooks-mock.js"; // Test that the request and its additional sub-request (when present) // are correctly modified in the "onRequest" hook handler. // These tests simulate a real scenario where the user calls "await connection.provider.request(jsonRpcRequest)". describe("request-handlers - e2e", () => { - it("should successfully executes all the handlers", async () => { + it("should successfully executes all the handlers setting fixed values", async () => { + // should use the handlers: ChainIdValidatorHandler, FixedGasHandler + + const FIXED_GAS_LIMIT = 1231n; + + const hre = await createMockedNetworkHre( + { + networks: { + localhost: { + gas: FIXED_GAS_LIMIT, + type: "http", + url: "http://localhost:8545", + chainId: 1, + }, + }, + }, + { + eth_chainId: "0x1", + }, + ); + + // Use the localhost network for these tests because the modifier is only + // applicable to HTTP networks. EDR networks do not require this modifier. + const connection = await hre.network.connect("localhost"); + + const res = await connection.provider.request({ + method: "eth_sendTransaction", + params: [ + { + from: "0x0000000000000000000000000000000000000011", + to: "0x0000000000000000000000000000000000000012", + }, + ], + }); + + assert.ok(Array.isArray(res), "res should be an array"); + + assert.equal(res[0].gas, numberToHexString(FIXED_GAS_LIMIT)); + }); + + it("should successfully executes all the handlers setting automatic values", async () => { + // should use the handlers: ChainIdValidatorHandler, AutomaticGasHandler + + const FIXED_GAS_LIMIT = 1231; + const GAS_MULTIPLIER = 1.337; + const hre = await createMockedNetworkHre( { networks: { localhost: { + gas: "auto", + gasMultiplier: GAS_MULTIPLIER, type: "http", url: "http://localhost:8545", chainId: 1, @@ -19,6 +69,10 @@ describe("request-handlers - e2e", () => { }, { eth_chainId: "0x1", + eth_getBlockByNumber: { + gasLimit: numberToHexString(FIXED_GAS_LIMIT * 1000), + }, + eth_estimateGas: numberToHexString(FIXED_GAS_LIMIT), }, ); @@ -26,9 +80,21 @@ describe("request-handlers - e2e", () => { // applicable to HTTP networks. EDR networks do not require this modifier. const connection = await hre.network.connect("localhost"); - await connection.provider.request({ + const res = await connection.provider.request({ method: "eth_sendTransaction", - params: [], + params: [ + { + from: "0x0000000000000000000000000000000000000011", + to: "0x0000000000000000000000000000000000000012", + }, + ], }); + + assert.ok(Array.isArray(res), "res should be an array"); + + assert.equal( + res[0].gas, + numberToHexString(Math.floor(FIXED_GAS_LIMIT * GAS_MULTIPLIER)), + ); }); }); diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/automatic-gas-handler.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/automatic-gas-handler.ts new file mode 100644 index 0000000000..6ca98ba49d --- /dev/null +++ b/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/automatic-gas-handler.ts @@ -0,0 +1,129 @@ +import assert from "node:assert/strict"; +import { beforeEach, describe, it } from "node:test"; + +import { numberToHexString } from "@ignored/hardhat-vnext-utils/hex"; + +import { + getJsonRpcRequest, + getRequestParams, +} from "../../../../../../../src/internal/builtin-plugins/network-manager/json-rpc.js"; +import { + AutomaticGasHandler, + DEFAULT_GAS_MULTIPLIER, +} from "../../../../../../../src/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/automatic-gas-handler.js"; +import { EthereumMockedProvider } from "../../ethereum-mocked-provider.js"; + +describe("AutomaticGasHandler", () => { + let automaticGasHandler: AutomaticGasHandler; + let mockedProvider: EthereumMockedProvider; + + const FIXED_GAS_LIMIT = 1231; + const GAS_MULTIPLIER = 1.337; + + beforeEach(() => { + mockedProvider = new EthereumMockedProvider(); + + mockedProvider.setReturnValue("eth_getBlockByNumber", { + gasLimit: numberToHexString(FIXED_GAS_LIMIT * 1000), + }); + + mockedProvider.setReturnValue( + "eth_estimateGas", + numberToHexString(FIXED_GAS_LIMIT), + ); + + automaticGasHandler = new AutomaticGasHandler( + mockedProvider, + GAS_MULTIPLIER, + ); + }); + + it("should estimate gas automatically if not present", async () => { + const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ + { + from: "0x0000000000000000000000000000000000000011", + to: "0x0000000000000000000000000000000000000011", + value: 1, + }, + ]); + + await automaticGasHandler.handle(jsonRpcRequest); + + assert.equal( + getRequestParams(jsonRpcRequest)[0].gas, + numberToHexString(Math.floor(FIXED_GAS_LIMIT * GAS_MULTIPLIER)), + ); + }); + + it("should support different gas multipliers", async () => { + const GAS_MULTIPLIER2 = 123; + + const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ + { + from: "0x0000000000000000000000000000000000000011", + to: "0x0000000000000000000000000000000000000011", + value: 1, + }, + ]); + + automaticGasHandler = new AutomaticGasHandler( + mockedProvider, + GAS_MULTIPLIER2, + ); + + await automaticGasHandler.handle(jsonRpcRequest); + + assert.equal( + getRequestParams(jsonRpcRequest)[0].gas, + numberToHexString(Math.floor(FIXED_GAS_LIMIT * GAS_MULTIPLIER2)), + ); + }); + + it("should have a default multiplier", async () => { + const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ + { + from: "0x0000000000000000000000000000000000000011", + to: "0x0000000000000000000000000000000000000011", + value: 1, + }, + ]); + + automaticGasHandler = new AutomaticGasHandler(mockedProvider); + + await automaticGasHandler.handle(jsonRpcRequest); + + assert.equal( + getRequestParams(jsonRpcRequest)[0].gas, + numberToHexString(Math.floor(FIXED_GAS_LIMIT * DEFAULT_GAS_MULTIPLIER)), + ); + }); + + it("shouldn't replace the provided gas", async () => { + const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ + { + from: "0x0000000000000000000000000000000000000011", + to: "0x0000000000000000000000000000000000000011", + value: 1, + gas: 567, + }, + ]); + + await automaticGasHandler.handle(jsonRpcRequest); + + assert.equal(getRequestParams(jsonRpcRequest)[0].gas, 567); + }); + + it("should forward the other calls", async () => { + const jsonRpcRequest = getJsonRpcRequest(1, "eth_randomMethod", [ + { + from: "0x0000000000000000000000000000000000000011", + to: "0x0000000000000000000000000000000000000011", + value: 1, + }, + ]); + + await automaticGasHandler.handle(jsonRpcRequest); + + assert.equal(getRequestParams(jsonRpcRequest)[0].gas, undefined); + }); +}); diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/fixed-gas-handler.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/fixed-gas-handler.ts new file mode 100644 index 0000000000..66fbb1f8a7 --- /dev/null +++ b/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/fixed-gas-handler.ts @@ -0,0 +1,66 @@ +import assert from "node:assert/strict"; +import { beforeEach, describe, it } from "node:test"; + +import { numberToHexString } from "@ignored/hardhat-vnext-utils/hex"; + +import { + getJsonRpcRequest, + getRequestParams, +} from "../../../../../../../src/internal/builtin-plugins/network-manager/json-rpc.js"; +import { FixedGasHandler } from "../../../../../../../src/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/fixed-gas-handler.js"; + +describe("FixedGasHandler", () => { + let fixedGasHandler: FixedGasHandler; + + const FIXED_GAS_LIMIT = 1233n; + + beforeEach(() => { + fixedGasHandler = new FixedGasHandler(numberToHexString(FIXED_GAS_LIMIT)); + }); + + it("should set the fixed gas if not present", async () => { + const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ + { + from: "0x0000000000000000000000000000000000000011", + to: "0x0000000000000000000000000000000000000011", + value: 1, + }, + ]); + + fixedGasHandler.handle(jsonRpcRequest); + + assert.equal( + getRequestParams(jsonRpcRequest)[0].gas, + numberToHexString(FIXED_GAS_LIMIT), + ); + }); + + it("shouldn't replace the provided gas", async () => { + const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ + { + from: "0x0000000000000000000000000000000000000011", + to: "0x0000000000000000000000000000000000000011", + value: 1, + gas: 1456, + }, + ]); + + fixedGasHandler.handle(jsonRpcRequest); + + assert.equal(getRequestParams(jsonRpcRequest)[0].gas, 1456); + }); + + it("should forward the other calls and not modify the gas", async () => { + const jsonRpcRequest = getJsonRpcRequest(1, "eth_estimateGas", [ + { + from: "0x0000000000000000000000000000000000000011", + to: "0x0000000000000000000000000000000000000011", + value: 1, + }, + ]); + + fixedGasHandler.handle(jsonRpcRequest); + + assert.equal(getRequestParams(jsonRpcRequest)[0].gas, undefined); + }); +}); From 31720c36945d0c2219fb537cd1e529cb321bd282 Mon Sep 17 00:00:00 2001 From: ChrisD <18092467+ChristopherDedominici@users.noreply.github.com> Date: Wed, 27 Nov 2024 16:16:52 +0100 Subject: [PATCH 05/17] add automatic and fixed gas price handlers --- .../gas/automatic-gas-price-handler.ts | 221 +++++++++++ .../handlers/gas/fixed-gas-price-handler.ts | 45 +++ .../network-manager/request-handlers/e2e.ts | 22 +- .../gas/automatic-gas-price-handler.ts | 362 ++++++++++++++++++ .../handlers/gas/fixed-gas-price-handler.ts | 68 ++++ 5 files changed, 716 insertions(+), 2 deletions(-) create mode 100644 v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/automatic-gas-price-handler.ts create mode 100644 v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/fixed-gas-price-handler.ts create mode 100644 v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/automatic-gas-price-handler.ts create mode 100644 v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/fixed-gas-price-handler.ts diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/automatic-gas-price-handler.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/automatic-gas-price-handler.ts new file mode 100644 index 0000000000..885139ae87 --- /dev/null +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/automatic-gas-price-handler.ts @@ -0,0 +1,221 @@ +import type { + EthereumProvider, + JsonRpcRequest, + JsonRpcResponse, +} from "../../../../../../types/providers.js"; +import type { RequestHandler } from "../../types.js"; + +import { assertHardhatInvariant } from "@ignored/hardhat-vnext-errors"; +import { + hexStringToBigInt, + numberToHexString, +} from "@ignored/hardhat-vnext-utils/hex"; +import { isObject } from "@ignored/hardhat-vnext-utils/lang"; + +import { getRequestParams } from "../../../json-rpc.js"; + +/** + * This class automatically adjusts transaction requests to include appropriately estimated gas prices. + * It ensures that gas prices are set correctly. + */ +export class AutomaticGasPriceHandler implements RequestHandler { + readonly #provider: EthereumProvider; + + // We pay the max base fee that can be required if the next + // EIP1559_BASE_FEE_MAX_FULL_BLOCKS_PREFERENCE are full. + public static readonly EIP1559_BASE_FEE_MAX_FULL_BLOCKS_PREFERENCE: bigint = + 3n; + + // See eth_feeHistory for an explanation of what this means + public static readonly EIP1559_REWARD_PERCENTILE = 50; + + constructor(provider: EthereumProvider) { + this.#provider = provider; + } + + #nodeHasFeeHistory?: boolean; + #nodeSupportsEIP1559?: boolean; + + public async handle( + jsonRpcRequest: JsonRpcRequest, + ): Promise { + if (jsonRpcRequest.method !== "eth_sendTransaction") { + return jsonRpcRequest; + } + + const params = getRequestParams(jsonRpcRequest); + + // TODO: from V2 - Should we validate this type? + const tx = params[0]; + + if (tx === undefined) { + return jsonRpcRequest; + } + + // We don't need to do anything in these cases + if ( + tx.gasPrice !== undefined || + (tx.maxFeePerGas !== undefined && tx.maxPriorityFeePerGas !== undefined) + ) { + return jsonRpcRequest; + } + + let suggestedEip1559Values = await this.#suggestEip1559FeePriceValues(); + + // eth_feeHistory failed, so we send a legacy one + if ( + tx.maxFeePerGas === undefined && + tx.maxPriorityFeePerGas === undefined && + suggestedEip1559Values === undefined + ) { + tx.gasPrice = numberToHexString(await this.#getGasPrice()); + return jsonRpcRequest; + } + + // If eth_feeHistory failed, but the user still wants to send an EIP-1559 tx + // we use the gasPrice as default values. + if (suggestedEip1559Values === undefined) { + const gasPrice = await this.#getGasPrice(); + + suggestedEip1559Values = { + maxFeePerGas: gasPrice, + maxPriorityFeePerGas: gasPrice, + }; + } + + let maxFeePerGas = + tx.maxFeePerGas !== undefined + ? hexStringToBigInt(tx.maxFeePerGas) + : suggestedEip1559Values.maxFeePerGas; + + const maxPriorityFeePerGas = + tx.maxPriorityFeePerGas !== undefined + ? hexStringToBigInt(tx.maxPriorityFeePerGas) + : suggestedEip1559Values.maxPriorityFeePerGas; + + if (maxFeePerGas < maxPriorityFeePerGas) { + maxFeePerGas += maxPriorityFeePerGas; + } + + tx.maxFeePerGas = numberToHexString(maxFeePerGas); + tx.maxPriorityFeePerGas = numberToHexString(maxPriorityFeePerGas); + + return jsonRpcRequest; + } + + async #getGasPrice(): Promise { + const response = await this.#provider.request({ + method: "eth_gasPrice", + }); + + assertHardhatInvariant( + typeof response === "string", + "response should return a string", + ); + + return hexStringToBigInt(response); + } + + async #suggestEip1559FeePriceValues(): Promise< + | { + maxFeePerGas: bigint; + maxPriorityFeePerGas: bigint; + } + | undefined + > { + if (this.#nodeSupportsEIP1559 === undefined) { + const block = await this.#provider.request({ + method: "eth_getBlockByNumber", + params: ["latest", false], + }); + + assertHardhatInvariant( + isObject(block), + "block should be a non null object", + ); + + this.#nodeSupportsEIP1559 = + "baseFeePerGas" in block && block.baseFeePerGas !== undefined; + } + + if ( + this.#nodeHasFeeHistory === false || + this.#nodeSupportsEIP1559 === false + ) { + return; + } + + try { + const response = await this.#provider.request({ + method: "eth_feeHistory", + params: [ + "0x1", + "latest", + [AutomaticGasPriceHandler.EIP1559_REWARD_PERCENTILE], + ], + }); + + assertHardhatInvariant( + typeof response === "object" && + response !== null && + "baseFeePerGas" in response && + "reward" in response && + Array.isArray(response.baseFeePerGas) && + Array.isArray(response.reward), + "response should be an object with baseFeePerGas and reward properties", + ); + + let maxPriorityFeePerGas = hexStringToBigInt(response.reward[0][0]); + + if (maxPriorityFeePerGas === 0n) { + try { + const suggestedMaxPriorityFeePerGas = await this.#provider.request({ + method: "eth_maxPriorityFeePerGas", + params: [], + }); + + assertHardhatInvariant( + typeof suggestedMaxPriorityFeePerGas === "string", + "suggestedMaxPriorityFeePerGas should be a string", + ); + + maxPriorityFeePerGas = hexStringToBigInt( + suggestedMaxPriorityFeePerGas, + ); + } catch { + // if eth_maxPriorityFeePerGas does not exist, use 1 wei + maxPriorityFeePerGas = 1n; + } + } + + // If after all of these we still have a 0 wei maxPriorityFeePerGas, we + // use 1 wei. This is to improve the UX of the automatic gas price + // on chains that are very empty (i.e local testnets). This will be very + // unlikely to trigger on a live chain. + if (maxPriorityFeePerGas === 0n) { + maxPriorityFeePerGas = 1n; + } + + return { + // Each block increases the base fee by 1/8 at most, when full. + // We have the next block's base fee, so we compute a cap for the + // next N blocks here. + + maxFeePerGas: + (hexStringToBigInt(response.baseFeePerGas[1]) * + 9n ** + (AutomaticGasPriceHandler.EIP1559_BASE_FEE_MAX_FULL_BLOCKS_PREFERENCE - + 1n)) / + 8n ** + (AutomaticGasPriceHandler.EIP1559_BASE_FEE_MAX_FULL_BLOCKS_PREFERENCE - + 1n), + + maxPriorityFeePerGas, + }; + } catch { + this.#nodeHasFeeHistory = false; + + return undefined; + } + } +} diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/fixed-gas-price-handler.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/fixed-gas-price-handler.ts new file mode 100644 index 0000000000..9ff1b43083 --- /dev/null +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/fixed-gas-price-handler.ts @@ -0,0 +1,45 @@ +import type { + JsonRpcRequest, + JsonRpcResponse, +} from "../../../../../../types/providers.js"; +import type { RequestHandler } from "../../types.js"; +import type { PrefixedHexString } from "@ignored/hardhat-vnext-utils/hex"; + +import { getRequestParams } from "../../../json-rpc.js"; + +/** + * This class ensures that a fixed gas price is applied to transaction requests. + * For `eth_sendTransaction` requests, it sets the gasPrice field with the value provided via the class constructor, if it hasn't been specified already. + */ +export class FixedGasPriceHandler implements RequestHandler { + readonly #gasPrice: PrefixedHexString; + + constructor(gasPrice: PrefixedHexString) { + this.#gasPrice = gasPrice; + } + + public async handle( + jsonRpcRequest: JsonRpcRequest, + ): Promise { + if (jsonRpcRequest.method !== "eth_sendTransaction") { + return jsonRpcRequest; + } + + const params = getRequestParams(jsonRpcRequest); + + // TODO: from V2 - Should we validate this type? + const tx = params[0]; + + // Temporary change to ignore EIP-1559 + if ( + tx !== undefined && + tx.gasPrice === undefined && + tx.maxFeePerGas === undefined && + tx.maxPriorityFeePerGas === undefined + ) { + tx.gasPrice = this.#gasPrice; + } + + return jsonRpcRequest; + } +} diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/e2e.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/e2e.ts index 16bb61995d..2d925d4309 100644 --- a/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/e2e.ts +++ b/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/e2e.ts @@ -10,15 +10,17 @@ import { createMockedNetworkHre } from "./hooks-mock.js"; // These tests simulate a real scenario where the user calls "await connection.provider.request(jsonRpcRequest)". describe("request-handlers - e2e", () => { it("should successfully executes all the handlers setting fixed values", async () => { - // should use the handlers: ChainIdValidatorHandler, FixedGasHandler + // should use the handlers in this order: ChainIdValidatorHandler, FixedGasHandler, FixedGasPriceHandler const FIXED_GAS_LIMIT = 1231n; + const FIXED_GAS_PRICE = 1232n; const hre = await createMockedNetworkHre( { networks: { localhost: { gas: FIXED_GAS_LIMIT, + gasPrice: FIXED_GAS_PRICE, type: "http", url: "http://localhost:8545", chainId: 1, @@ -47,13 +49,15 @@ describe("request-handlers - e2e", () => { assert.ok(Array.isArray(res), "res should be an array"); assert.equal(res[0].gas, numberToHexString(FIXED_GAS_LIMIT)); + assert.equal(res[0].gasPrice, numberToHexString(FIXED_GAS_PRICE)); }); it("should successfully executes all the handlers setting automatic values", async () => { - // should use the handlers: ChainIdValidatorHandler, AutomaticGasHandler + // should use the handlers in this order: ChainIdValidatorHandler, AutomaticGasHandler, AutomaticGasPriceHandler const FIXED_GAS_LIMIT = 1231; const GAS_MULTIPLIER = 1.337; + const LATEST_BASE_FEE_IN_MOCKED_PROVIDER = 80; const hre = await createMockedNetworkHre( { @@ -70,9 +74,19 @@ describe("request-handlers - e2e", () => { { eth_chainId: "0x1", eth_getBlockByNumber: { + baseFeePerGas: "0x1", gasLimit: numberToHexString(FIXED_GAS_LIMIT * 1000), }, eth_estimateGas: numberToHexString(FIXED_GAS_LIMIT), + eth_feeHistory: { + baseFeePerGas: [ + numberToHexString(LATEST_BASE_FEE_IN_MOCKED_PROVIDER), + numberToHexString( + Math.floor((LATEST_BASE_FEE_IN_MOCKED_PROVIDER * 9) / 8), + ), + ], + reward: [["0x4"]], + }, }, ); @@ -86,6 +100,7 @@ describe("request-handlers - e2e", () => { { from: "0x0000000000000000000000000000000000000011", to: "0x0000000000000000000000000000000000000012", + maxFeePerGas: "0x99", }, ], }); @@ -96,5 +111,8 @@ describe("request-handlers - e2e", () => { res[0].gas, numberToHexString(Math.floor(FIXED_GAS_LIMIT * GAS_MULTIPLIER)), ); + + assert.equal(res[0].maxPriorityFeePerGas, "0x4"); + assert.equal(res[0].maxFeePerGas, "0x99"); }); }); diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/automatic-gas-price-handler.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/automatic-gas-price-handler.ts new file mode 100644 index 0000000000..01b8448039 --- /dev/null +++ b/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/automatic-gas-price-handler.ts @@ -0,0 +1,362 @@ +import assert from "node:assert/strict"; +import { beforeEach, describe, it } from "node:test"; + +import { numberToHexString } from "@ignored/hardhat-vnext-utils/hex"; + +import { + getJsonRpcRequest, + getRequestParams, +} from "../../../../../../../src/internal/builtin-plugins/network-manager/json-rpc.js"; +import { AutomaticGasPriceHandler } from "../../../../../../../src/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/automatic-gas-price-handler.js"; +import { EthereumMockedProvider } from "../../ethereum-mocked-provider.js"; + +describe("AutomaticGasPriceHandler", () => { + let automaticGasPriceHandler: AutomaticGasPriceHandler; + let mockedProvider: EthereumMockedProvider; + + const FIXED_GAS_PRICE = 1232; + + beforeEach(async () => { + mockedProvider = new EthereumMockedProvider(); + + mockedProvider.setReturnValue( + "eth_gasPrice", + numberToHexString(FIXED_GAS_PRICE), + ); + + automaticGasPriceHandler = new AutomaticGasPriceHandler(mockedProvider); + }); + + describe("when the fee price values are provided", () => { + it("shouldn't replace the provided gasPrice", async () => { + const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ + { + from: "0x0000000000000000000000000000000000000011", + to: "0x0000000000000000000000000000000000000011", + value: 1, + gasPrice: 456, + }, + ]); + + automaticGasPriceHandler.handle(jsonRpcRequest); + + assert.equal(getRequestParams(jsonRpcRequest)[0].gasPrice, 456); + }); + + it("shouldn't replace the provided maxFeePerGas and maxPriorityFeePerGas values", async () => { + const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ + { + from: "0x0000000000000000000000000000000000000011", + to: "0x0000000000000000000000000000000000000011", + value: 1, + maxFeePerGas: 456, + maxPriorityFeePerGas: 789, + }, + ]); + + automaticGasPriceHandler.handle(jsonRpcRequest); + + assert.equal(getRequestParams(jsonRpcRequest)[0].maxFeePerGas, 456); + assert.equal( + getRequestParams(jsonRpcRequest)[0].maxPriorityFeePerGas, + 789, + ); + }); + }); + + describe("Default fee price values", () => { + describe("When eth_feeHistory is available and EIP1559 is supported", () => { + const latestBaseFeeInMockedProvider = 80; + + beforeEach(() => { + mockedProvider.setReturnValue("eth_feeHistory", { + baseFeePerGas: [ + numberToHexString(latestBaseFeeInMockedProvider), + numberToHexString( + Math.floor((latestBaseFeeInMockedProvider * 9) / 8), + ), + ], + reward: [["0x4"]], + }); + + mockedProvider.setReturnValue("eth_getBlockByNumber", { + baseFeePerGas: "0x1", + }); + }); + + it("should use the reward return value as default maxPriorityFeePerGas", async () => { + const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ + { + from: "0x0000000000000000000000000000000000000011", + to: "0x0000000000000000000000000000000000000011", + value: 1, + maxFeePerGas: "0x99", + }, + ]); + + await automaticGasPriceHandler.handle(jsonRpcRequest); + + assert.equal( + getRequestParams(jsonRpcRequest)[0].maxPriorityFeePerGas, + "0x4", + ); + assert.equal(getRequestParams(jsonRpcRequest)[0].maxFeePerGas, "0x99"); + }); + + it("should add the reward to the maxFeePerGas if not big enough", async () => { + const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ + { + from: "0x0000000000000000000000000000000000000011", + to: "0x0000000000000000000000000000000000000011", + value: 1, + maxFeePerGas: "0x1", + }, + ]); + + await automaticGasPriceHandler.handle(jsonRpcRequest); + + assert.equal( + getRequestParams(jsonRpcRequest)[0].maxPriorityFeePerGas, + "0x4", + ); + assert.equal(getRequestParams(jsonRpcRequest)[0].maxFeePerGas, "0x5"); + }); + + it("should use the expected max base fee of N blocks in the future if maxFeePerGas is missing", async () => { + const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ + { + from: "0x0000000000000000000000000000000000000011", + to: "0x0000000000000000000000000000000000000011", + value: 1, + maxPriorityFeePerGas: "0x1", + }, + ]); + + const expectedBaseFee = Math.floor( + latestBaseFeeInMockedProvider * + (9 / 8) ** + Number( + AutomaticGasPriceHandler.EIP1559_BASE_FEE_MAX_FULL_BLOCKS_PREFERENCE, + ), + ); + + await automaticGasPriceHandler.handle(jsonRpcRequest); + + assert.equal( + getRequestParams(jsonRpcRequest)[0].maxPriorityFeePerGas, + "0x1", + ); + assert.equal( + getRequestParams(jsonRpcRequest)[0].maxFeePerGas, + numberToHexString(expectedBaseFee), + ); + }); + }); + + describe("when the eth_feeHistory result causes maxPriorityFeePerGas to be 0 and eth_maxPriorityFeePerGas doesn't exist", () => { + const latestBaseFeeInMockedProvider = 80; + + beforeEach(() => { + mockedProvider.setReturnValue("eth_feeHistory", { + baseFeePerGas: [ + numberToHexString(latestBaseFeeInMockedProvider), + numberToHexString( + Math.floor((latestBaseFeeInMockedProvider * 9) / 8), + ), + ], + reward: [["0x0"]], + }); + + mockedProvider.setReturnValue("eth_getBlockByNumber", { + baseFeePerGas: "0x1", + }); + }); + + it("should use a non-zero maxPriorityFeePerGas", async () => { + const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ + { + from: "0x0000000000000000000000000000000000000011", + to: "0x0000000000000000000000000000000000000011", + value: 1, + }, + ]); + + const expectedBaseFee = Math.floor( + latestBaseFeeInMockedProvider * + (9 / 8) ** + Number( + AutomaticGasPriceHandler.EIP1559_BASE_FEE_MAX_FULL_BLOCKS_PREFERENCE, + ), + ); + + await automaticGasPriceHandler.handle(jsonRpcRequest); + + assert.equal( + getRequestParams(jsonRpcRequest)[0].maxPriorityFeePerGas, + "0x1", + ); + assert.equal( + getRequestParams(jsonRpcRequest)[0].maxFeePerGas, + numberToHexString(expectedBaseFee), + ); + }); + }); + + describe("when the eth_feeHistory result causes maxPriorityFeePerGas to be 0 and eth_maxPriorityFeePerGas exists", () => { + const latestBaseFeeInMockedProvider = 80; + + beforeEach(() => { + mockedProvider.setReturnValue("eth_feeHistory", { + baseFeePerGas: [ + numberToHexString(latestBaseFeeInMockedProvider), + numberToHexString( + Math.floor((latestBaseFeeInMockedProvider * 9) / 8), + ), + ], + reward: [["0x0"]], + }); + + mockedProvider.setReturnValue("eth_getBlockByNumber", { + baseFeePerGas: "0x1", + }); + + mockedProvider.setReturnValue("eth_maxPriorityFeePerGas", "0x12"); + }); + + it("should use the result of eth_maxPriorityFeePerGas as maxPriorityFeePerGas", async () => { + const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ + { + from: "0x0000000000000000000000000000000000000011", + to: "0x0000000000000000000000000000000000000011", + value: 1, + }, + ]); + + const expectedBaseFee = Math.floor( + latestBaseFeeInMockedProvider * + (9 / 8) ** + Number( + AutomaticGasPriceHandler.EIP1559_BASE_FEE_MAX_FULL_BLOCKS_PREFERENCE, + ), + ); + + await automaticGasPriceHandler.handle(jsonRpcRequest); + + assert.equal( + getRequestParams(jsonRpcRequest)[0].maxPriorityFeePerGas, + "0x12", + ); + assert.equal( + getRequestParams(jsonRpcRequest)[0].maxFeePerGas, + numberToHexString(expectedBaseFee), + ); + }); + }); + + describe("when eth_feeHistory is available and EIP1559 is not supported", () => { + const latestBaseFeeInMockedProvider = 80; + + beforeEach(() => { + mockedProvider.setReturnValue("eth_feeHistory", { + baseFeePerGas: [ + numberToHexString(latestBaseFeeInMockedProvider), + numberToHexString( + Math.floor((latestBaseFeeInMockedProvider * 9) / 8), + ), + ], + reward: [["0x4"]], + }); + + mockedProvider.setReturnValue("eth_getBlockByNumber", {}); + }); + + runTestUseLegacyGasPrice(); + }); + + describe("when eth_feeHistory is not available", () => { + beforeEach(() => { + mockedProvider.setReturnValue("eth_getBlockByNumber", {}); + }); + + runTestUseLegacyGasPrice(); + }); + + /** + * Group of tests that expect gasPrice to be used instead of EIP1559 fields + */ + function runTestUseLegacyGasPrice() { + it("should use gasPrice when nothing is provided", async () => { + const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ + { + from: "0x0000000000000000000000000000000000000011", + to: "0x0000000000000000000000000000000000000011", + value: 1, + }, + ]); + + await automaticGasPriceHandler.handle(jsonRpcRequest); + + assert.equal( + getRequestParams(jsonRpcRequest)[0].gasPrice, + numberToHexString(FIXED_GAS_PRICE), + ); + }); + + it("should use gasPrice as default maxPriorityFeePerGas, adding it to maxFeePerGas if necessary", async () => { + const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ + { + from: "0x0000000000000000000000000000000000000011", + to: "0x0000000000000000000000000000000000000011", + value: 1, + maxFeePerGas: "0x1", + }, + ]); + + await automaticGasPriceHandler.handle(jsonRpcRequest); + + assert.equal( + getRequestParams(jsonRpcRequest)[0].maxPriorityFeePerGas, + numberToHexString(FIXED_GAS_PRICE), + ); + assert.equal( + getRequestParams(jsonRpcRequest)[0].maxFeePerGas, + numberToHexString(FIXED_GAS_PRICE + 1), + ); + }); + + it("should use gasPrice as default maxFeePerGas, fixing maxPriorityFee to it if necessary", async () => { + const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ + { + from: "0x0000000000000000000000000000000000000011", + to: "0x0000000000000000000000000000000000000011", + value: 1, + maxPriorityFeePerGas: numberToHexString(FIXED_GAS_PRICE + 2), + }, + ]); + + await automaticGasPriceHandler.handle(jsonRpcRequest); + + assert.equal( + getRequestParams(jsonRpcRequest)[0].maxPriorityFeePerGas, + numberToHexString(FIXED_GAS_PRICE + 2), + ); + assert.equal( + getRequestParams(jsonRpcRequest)[0].maxFeePerGas, + numberToHexString(FIXED_GAS_PRICE * 2 + 2), + ); + }); + } + }); + + it("should forward the other calls", async () => { + const jsonRpcRequest = getJsonRpcRequest( + 1, + "eth_getBlockByNumber", + [1, 2, 3, 4], + ); + + await automaticGasPriceHandler.handle(jsonRpcRequest); + + assert.deepEqual(jsonRpcRequest.params, [1, 2, 3, 4]); + }); +}); diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/fixed-gas-price-handler.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/fixed-gas-price-handler.ts new file mode 100644 index 0000000000..354c43d141 --- /dev/null +++ b/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/fixed-gas-price-handler.ts @@ -0,0 +1,68 @@ +import assert from "node:assert/strict"; +import { beforeEach, describe, it } from "node:test"; + +import { numberToHexString } from "@ignored/hardhat-vnext-utils/hex"; + +import { + getJsonRpcRequest, + getRequestParams, +} from "../../../../../../../src/internal/builtin-plugins/network-manager/json-rpc.js"; +import { FixedGasPriceHandler } from "../../../../../../../src/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/fixed-gas-price-handler.js"; + +describe("FixedGasPriceHandler", () => { + let fixedGasPriceHandler: FixedGasPriceHandler; + + const FIXED_GAS_PRICE = 1234n; + + beforeEach(() => { + fixedGasPriceHandler = new FixedGasPriceHandler( + numberToHexString(FIXED_GAS_PRICE), + ); + }); + + it("should set the fixed gasPrice if not present", async () => { + const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ + { + from: "0x0000000000000000000000000000000000000011", + to: "0x0000000000000000000000000000000000000011", + value: 1, + }, + ]); + + fixedGasPriceHandler.handle(jsonRpcRequest); + + assert.equal( + getRequestParams(jsonRpcRequest)[0].gasPrice, + numberToHexString(FIXED_GAS_PRICE), + ); + }); + + it("shouldn't replace the provided gasPrice", async () => { + const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ + { + from: "0x0000000000000000000000000000000000000011", + to: "0x0000000000000000000000000000000000000011", + value: 1, + gasPrice: 14567, + }, + ]); + + fixedGasPriceHandler.handle(jsonRpcRequest); + + assert.equal(getRequestParams(jsonRpcRequest)[0].gasPrice, 14567); + }); + + it("should forward the other calls and not modify the gasPrice", async () => { + const jsonRpcRequest = getJsonRpcRequest(1, "eth_gasPrice", [ + { + from: "0x0000000000000000000000000000000000000011", + to: "0x0000000000000000000000000000000000000011", + value: 1, + }, + ]); + + fixedGasPriceHandler.handle(jsonRpcRequest); + + assert.equal(getRequestParams(jsonRpcRequest)[0].gas, undefined); + }); +}); From ef072a8d9af53accf3c5b47c8c9f024910058302 Mon Sep 17 00:00:00 2001 From: ChrisD <18092467+ChristopherDedominici@users.noreply.github.com> Date: Wed, 27 Nov 2024 17:19:32 +0100 Subject: [PATCH 06/17] add automatic and fixed sender --- .../accounts/automatic-sender-handler.ts | 32 +++++++++ .../handlers/accounts/fixed-sender-handler.ts | 21 ++++++ .../handlers/accounts/sender.ts | 57 +++++++++++++++ .../handlers/accounts/types.ts | 9 +++ .../network-manager/request-handlers/e2e.ts | 23 ++++-- .../accounts/automatic-sender-handler.ts | 71 +++++++++++++++++++ .../handlers/accounts/fixed-sender-handler.ts | 60 ++++++++++++++++ 7 files changed, 266 insertions(+), 7 deletions(-) create mode 100644 v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/automatic-sender-handler.ts create mode 100644 v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/fixed-sender-handler.ts create mode 100644 v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/sender.ts create mode 100644 v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/types.ts create mode 100644 v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/automatic-sender-handler.ts create mode 100644 v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/fixed-sender-handler.ts diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/automatic-sender-handler.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/automatic-sender-handler.ts new file mode 100644 index 0000000000..121d11fabe --- /dev/null +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/automatic-sender-handler.ts @@ -0,0 +1,32 @@ +import { assertHardhatInvariant } from "@ignored/hardhat-vnext-errors"; + +import { SenderHandler } from "./sender.js"; + +/** + * This class automatically retrieves and caches the first available account from the connected provider. + * It overrides the getSender method of the base class to request the list of accounts if the first account has not been fetched yet, + * ensuring dynamic selection of the sender for all JSON-RPC requests without requiring manual input. + */ +export class AutomaticSenderHandler extends SenderHandler { + #alreadyFetchedAccounts = false; + #firstAccount: string | undefined; + + protected async getSender(): Promise { + if (this.#alreadyFetchedAccounts === false) { + const accounts = await this.provider.request({ + method: "eth_accounts", + }); + + // TODO: This shouldn't be an exception but a failed JSON response! + assertHardhatInvariant( + Array.isArray(accounts), + "eth_accounts response should be an array", + ); + + this.#firstAccount = accounts[0]; + this.#alreadyFetchedAccounts = true; + } + + return this.#firstAccount; + } +} diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/fixed-sender-handler.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/fixed-sender-handler.ts new file mode 100644 index 0000000000..e50f5cf583 --- /dev/null +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/fixed-sender-handler.ts @@ -0,0 +1,21 @@ +import type { EthereumProvider } from "../../../../../../types/providers.js"; + +import { SenderHandler } from "./sender.js"; + +/** + * This class provides a fixed sender address for transactions. + * It overrides the getSender method of the base class to always return the sender address specified during instantiation, + * ensuring that all JSON-RPC requests use this fixed sender. + */ +export class FixedSenderHandler extends SenderHandler { + readonly #sender: string; + + constructor(provider: EthereumProvider, sender: string) { + super(provider); + this.#sender = sender; + } + + protected async getSender(): Promise { + return this.#sender; + } +} diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/sender.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/sender.ts new file mode 100644 index 0000000000..c47b98bd72 --- /dev/null +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/sender.ts @@ -0,0 +1,57 @@ +import type { JsonRpcTransactionData } from "./types.js"; +import type { + EthereumProvider, + JsonRpcRequest, + JsonRpcResponse, +} from "../../../../../../types/providers.js"; +import type { RequestHandler } from "../../types.js"; + +import { HardhatError } from "@ignored/hardhat-vnext-errors"; + +import { getRequestParams } from "../../../json-rpc.js"; + +/** + * This class modifies JSON-RPC requests. + * It checks if the request is related to transactions and ensures that the "from" field is populated with a sender account if it's missing. + * If no account is available for sending transactions, it throws an error. + * The class also provides a mechanism to retrieve the sender account, which must be implemented by subclasses. + */ +export abstract class SenderHandler implements RequestHandler { + protected readonly provider: EthereumProvider; + + constructor(provider: EthereumProvider) { + this.provider = provider; + } + + public async handle( + jsonRpcRequest: JsonRpcRequest, + ): Promise { + const method = jsonRpcRequest.method; + const params = getRequestParams(jsonRpcRequest); + + if ( + method === "eth_sendTransaction" || + method === "eth_call" || + method === "eth_estimateGas" + ) { + // TODO: from V2 - Should we validate this type? + const tx: JsonRpcTransactionData = params[0]; + + if (tx !== undefined && tx.from === undefined) { + const senderAccount = await this.getSender(); + + if (senderAccount !== undefined) { + tx.from = senderAccount; + } else if (method === "eth_sendTransaction") { + throw new HardhatError( + HardhatError.ERRORS.NETWORK.NO_REMOTE_ACCOUNT_AVAILABLE, + ); + } + } + } + + return jsonRpcRequest; + } + + protected abstract getSender(): Promise; +} diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/types.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/types.ts new file mode 100644 index 0000000000..565d514fc6 --- /dev/null +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/types.ts @@ -0,0 +1,9 @@ +export interface JsonRpcTransactionData { + from?: string; + to?: string; + gas?: string | number; + gasPrice?: string | number; + value?: string | number; + data?: string; + nonce?: string | number; +} diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/e2e.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/e2e.ts index 2d925d4309..c0987ed790 100644 --- a/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/e2e.ts +++ b/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/e2e.ts @@ -9,8 +9,8 @@ import { createMockedNetworkHre } from "./hooks-mock.js"; // are correctly modified in the "onRequest" hook handler. // These tests simulate a real scenario where the user calls "await connection.provider.request(jsonRpcRequest)". describe("request-handlers - e2e", () => { - it("should successfully executes all the handlers setting fixed values", async () => { - // should use the handlers in this order: ChainIdValidatorHandler, FixedGasHandler, FixedGasPriceHandler + it("should successfully executes all the handlers that set fixed values", async () => { + // should use the handlers in this order: ChainIdValidatorHandler, FixedGasHandler, FixedGasPriceHandler, FixedSenderHandler const FIXED_GAS_LIMIT = 1231n; const FIXED_GAS_PRICE = 1232n; @@ -21,12 +21,14 @@ describe("request-handlers - e2e", () => { localhost: { gas: FIXED_GAS_LIMIT, gasPrice: FIXED_GAS_PRICE, + from: "0x2a97a65d5673a2c61e95ce33cecadf24f654f96d", type: "http", url: "http://localhost:8545", chainId: 1, }, }, }, + // List of methods that the handlers will call; we mock the responses { eth_chainId: "0x1", }, @@ -40,7 +42,6 @@ describe("request-handlers - e2e", () => { method: "eth_sendTransaction", params: [ { - from: "0x0000000000000000000000000000000000000011", to: "0x0000000000000000000000000000000000000012", }, ], @@ -48,12 +49,16 @@ describe("request-handlers - e2e", () => { assert.ok(Array.isArray(res), "res should be an array"); + // gas assert.equal(res[0].gas, numberToHexString(FIXED_GAS_LIMIT)); + // gasPrice assert.equal(res[0].gasPrice, numberToHexString(FIXED_GAS_PRICE)); + // sender + assert.equal(res[0].from, "0x2a97a65d5673a2c61e95ce33cecadf24f654f96d"); }); - it("should successfully executes all the handlers setting automatic values", async () => { - // should use the handlers in this order: ChainIdValidatorHandler, AutomaticGasHandler, AutomaticGasPriceHandler + it("should successfully executes all the handlers that set automatic values", async () => { + // should use the handlers in this order: ChainIdValidatorHandler, AutomaticGasHandler, AutomaticGasPriceHandler, AutomaticSenderHandler const FIXED_GAS_LIMIT = 1231; const GAS_MULTIPLIER = 1.337; @@ -72,6 +77,7 @@ describe("request-handlers - e2e", () => { }, }, { + // List of methods that the handlers will call; we mock the responses eth_chainId: "0x1", eth_getBlockByNumber: { baseFeePerGas: "0x1", @@ -87,6 +93,7 @@ describe("request-handlers - e2e", () => { ], reward: [["0x4"]], }, + eth_accounts: ["0x123006d4548a3ac17d72b372ae1e416bf65b8eaf"], }, ); @@ -98,7 +105,6 @@ describe("request-handlers - e2e", () => { method: "eth_sendTransaction", params: [ { - from: "0x0000000000000000000000000000000000000011", to: "0x0000000000000000000000000000000000000012", maxFeePerGas: "0x99", }, @@ -107,12 +113,15 @@ describe("request-handlers - e2e", () => { assert.ok(Array.isArray(res), "res should be an array"); + // gas assert.equal( res[0].gas, numberToHexString(Math.floor(FIXED_GAS_LIMIT * GAS_MULTIPLIER)), ); - + // gas price assert.equal(res[0].maxPriorityFeePerGas, "0x4"); assert.equal(res[0].maxFeePerGas, "0x99"); + // sender + assert.equal(res[0].from, "0x123006d4548a3ac17d72b372ae1e416bf65b8eaf"); }); }); diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/automatic-sender-handler.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/automatic-sender-handler.ts new file mode 100644 index 0000000000..c891371761 --- /dev/null +++ b/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/automatic-sender-handler.ts @@ -0,0 +1,71 @@ +import type { JsonRpcTransactionData } from "../../../../../../../src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/types.js"; + +import assert from "node:assert/strict"; +import { before, describe, it } from "node:test"; + +import { numberToHexString } from "@ignored/hardhat-vnext-utils/hex"; + +import { + getJsonRpcRequest, + getRequestParams, +} from "../../../../../../../src/internal/builtin-plugins/network-manager/json-rpc.js"; +import { AutomaticSenderHandler } from "../../../../../../../src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/automatic-sender-handler.js"; +import { EthereumMockedProvider } from "../../ethereum-mocked-provider.js"; + +describe("AutomaticSenderHandler", function () { + let automaticSenderHandler: AutomaticSenderHandler; + let mockedProvider: EthereumMockedProvider; + let tx: JsonRpcTransactionData; + + before(() => { + mockedProvider = new EthereumMockedProvider(); + + mockedProvider.setReturnValue("eth_accounts", [ + "0x123006d4548a3ac17d72b372ae1e416bf65b8eaf", + ]); + + automaticSenderHandler = new AutomaticSenderHandler(mockedProvider); + + tx = { + to: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", + gas: numberToHexString(21000), + gasPrice: numberToHexString(678912), + nonce: numberToHexString(0), + value: numberToHexString(1), + }; + }); + + it("should set the from value into the transaction", async () => { + const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [tx]); + + await automaticSenderHandler.handle(jsonRpcRequest); + + assert.equal( + getRequestParams(jsonRpcRequest)[0].from, + "0x123006d4548a3ac17d72b372ae1e416bf65b8eaf", + ); + }); + + it("should not replace transaction's from", async () => { + tx.from = "0x000006d4548a3ac17d72b372ae1e416bf65b8ead"; + + const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [tx]); + + assert.equal( + getRequestParams(jsonRpcRequest)[0].from, + "0x000006d4548a3ac17d72b372ae1e416bf65b8ead", + ); + }); + + it("should not fail on eth_calls if provider doesn't have any accounts", async () => { + mockedProvider.setReturnValue("eth_accounts", []); + + tx.value = "asd"; + + const jsonRpcRequest = getJsonRpcRequest(1, "eth_call", [tx]); + + await automaticSenderHandler.handle(jsonRpcRequest); + + assert.equal(getRequestParams(jsonRpcRequest)[0].value, "asd"); + }); +}); diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/fixed-sender-handler.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/fixed-sender-handler.ts new file mode 100644 index 0000000000..08e171d987 --- /dev/null +++ b/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/fixed-sender-handler.ts @@ -0,0 +1,60 @@ +import type { JsonRpcTransactionData } from "../../../../../../../src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/types.js"; + +import assert from "node:assert/strict"; +import { before, describe, it } from "node:test"; + +import { numberToHexString } from "@ignored/hardhat-vnext-utils/hex"; + +import { + getJsonRpcRequest, + getRequestParams, +} from "../../../../../../../src/internal/builtin-plugins/network-manager/json-rpc.js"; +import { FixedSenderHandler } from "../../../../../../../src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/fixed-sender-handler.js"; +import { EthereumMockedProvider } from "../../ethereum-mocked-provider.js"; + +describe("FixedSenderHandler", function () { + let fixedSenderHandler: FixedSenderHandler; + let mockedProvider: EthereumMockedProvider; + let tx: JsonRpcTransactionData; + + before(() => { + mockedProvider = new EthereumMockedProvider(); + + fixedSenderHandler = new FixedSenderHandler( + mockedProvider, + "0x2a97a65d5673a2c61e95ce33cecadf24f654f96d", + ); + + tx = { + to: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", + gas: numberToHexString(21000), + gasPrice: numberToHexString(678912), + nonce: numberToHexString(0), + value: numberToHexString(1), + }; + }); + + it("should set the from value into the transaction", async () => { + const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [tx]); + + await fixedSenderHandler.handle(jsonRpcRequest); + + assert.equal( + getRequestParams(jsonRpcRequest)[0].from, + "0x2a97a65d5673a2c61e95ce33cecadf24f654f96d", + ); + }); + + it("should not replace transaction's from", async () => { + tx.from = "0x000006d4548a3ac17d72b372ae1e416bf65b8ead"; + + const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [tx]); + + await fixedSenderHandler.handle(jsonRpcRequest); + + assert.equal( + getRequestParams(jsonRpcRequest)[0].from, + "0x000006d4548a3ac17d72b372ae1e416bf65b8ead", + ); + }); +}); From f8089cdbf5647f4334d5d583e92439e9fb870055 Mon Sep 17 00:00:00 2001 From: ChrisD <18092467+ChristopherDedominici@users.noreply.github.com> Date: Thu, 28 Nov 2024 15:54:29 +0100 Subject: [PATCH 07/17] add LocalAccountsHandler and HDAccountsHandler --- .../network-manager/hook-handlers/network.ts | 2 +- .../handlers/accounts/derive-private-keys.ts | 63 ++ .../handlers/accounts/hd-wallet-handler.ts | 33 + .../handlers/accounts/local-accounts.ts | 344 ++++++++ .../handlers/chain-id/chain-id.ts | 8 +- .../network-manager/request-handlers/e2e.ts | 164 +++- .../handlers/accounts/hd-wallet.ts | 167 ++++ .../handlers/accounts/local-accounts.ts | 754 ++++++++++++++++++ 8 files changed, 1520 insertions(+), 15 deletions(-) create mode 100644 v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/derive-private-keys.ts create mode 100644 v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/hd-wallet-handler.ts create mode 100644 v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/local-accounts.ts create mode 100644 v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/hd-wallet.ts create mode 100644 v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/local-accounts.ts diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/hook-handlers/network.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/hook-handlers/network.ts index 5c7f807b1a..7e76795a94 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/hook-handlers/network.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/hook-handlers/network.ts @@ -41,7 +41,7 @@ export default async (): Promise> => { ); if (requestHandlers === undefined) { - requestHandlers = createHandlersArray(networkConnection); + requestHandlers = await createHandlersArray(networkConnection); requestHandlersPerConnection.set(networkConnection.id, requestHandlers); } diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/derive-private-keys.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/derive-private-keys.ts new file mode 100644 index 0000000000..9e9b1b5928 --- /dev/null +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/derive-private-keys.ts @@ -0,0 +1,63 @@ +import { HardhatError } from "@ignored/hardhat-vnext-errors"; +import { mnemonicToSeedSync } from "ethereum-cryptography/bip39"; +import { HDKey } from "ethereum-cryptography/hdkey"; + +const HD_PATH_REGEX = /^m(:?\/\d+'?)+\/?$/; + +export function derivePrivateKeys( + mnemonic: string, + hdpath: string, + initialIndex: number, + count: number, + passphrase: string, +): Buffer[] { + if (hdpath.match(HD_PATH_REGEX) === null) { + throw new HardhatError(HardhatError.ERRORS.NETWORK.INVALID_HD_PATH, { + path: hdpath, + }); + } + + if (!hdpath.endsWith("/")) { + hdpath += "/"; + } + + const privateKeys: Buffer[] = []; + + for (let i = initialIndex; i < initialIndex + count; i++) { + const privateKey = deriveKeyFromMnemonicAndPath( + mnemonic, + hdpath + i.toString(), + passphrase, + ); + + if (privateKey === undefined) { + throw new HardhatError(HardhatError.ERRORS.NETWORK.CANT_DERIVE_KEY, { + mnemonic, + path: hdpath, + }); + } + + privateKeys.push(privateKey); + } + + return privateKeys; +} + +function deriveKeyFromMnemonicAndPath( + mnemonic: string, + hdPath: string, + passphrase: string, +): Buffer | undefined { + // NOTE: If mnemonic has space or newline at the beginning or end, it will be trimmed. + // This is because mnemonic containing them may generate different private keys. + const trimmedMnemonic = mnemonic.trim(); + + const seed = mnemonicToSeedSync(trimmedMnemonic, passphrase); + + const masterKey = HDKey.fromMasterSeed(seed); + const derived = masterKey.derive(hdPath); + + return derived.privateKey === null + ? undefined + : Buffer.from(derived.privateKey); +} diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/hd-wallet-handler.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/hd-wallet-handler.ts new file mode 100644 index 0000000000..61a8631d23 --- /dev/null +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/hd-wallet-handler.ts @@ -0,0 +1,33 @@ +import type { EthereumProvider } from "../../../../../../types/providers.js"; + +import { bytesToHexString } from "@ignored/hardhat-vnext-utils/hex"; + +import { derivePrivateKeys } from "./derive-private-keys.js"; +import { LocalAccountsHandler } from "./local-accounts.js"; + +export class HDWalletHandler extends LocalAccountsHandler { + constructor( + provider: EthereumProvider, + mnemonic: string, + hdpath: string = "m/44'/60'/0'/0/", + initialIndex: number = 0, + count: number = 10, + passphrase: string = "", + ) { + // NOTE: If mnemonic has space or newline at the beginning or end, it will be trimmed. + // This is because mnemonic containing them may generate different private keys. + const trimmedMnemonic = mnemonic.trim(); + + const privateKeys = derivePrivateKeys( + trimmedMnemonic, + hdpath, + initialIndex, + count, + passphrase, + ); + + const privateKeysAsHex = privateKeys.map((pk) => bytesToHexString(pk)); + + super(provider, privateKeysAsHex); + } +} diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/local-accounts.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/local-accounts.ts new file mode 100644 index 0000000000..20c2e698ce --- /dev/null +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/local-accounts.ts @@ -0,0 +1,344 @@ +import type { + EthereumProvider, + JsonRpcRequest, + JsonRpcResponse, +} from "../../../../../../types/providers.js"; +import type { RequestHandler } from "../../types.js"; + +import { + assertHardhatInvariant, + HardhatError, +} from "@ignored/hardhat-vnext-errors"; +import { toBigInt } from "@ignored/hardhat-vnext-utils/bigint"; +import { + bytesToHexString, + hexStringToBigInt, + hexStringToBytes, +} from "@ignored/hardhat-vnext-utils/hex"; +import { addr, Transaction } from "micro-eth-signer"; +import * as typed from "micro-eth-signer/typed-data"; +import { signTyped } from "micro-eth-signer/typed-data"; + +import { getRequestParams } from "../../../json-rpc.js"; +import { rpcAddress } from "../../../rpc/types/address.js"; +import { rpcAny } from "../../../rpc/types/any.js"; +import { rpcData } from "../../../rpc/types/data.js"; +import { + rpcTransactionRequest, + type RpcTransactionRequest, +} from "../../../rpc/types/tx-request.js"; +import { validateParams } from "../../../rpc/validate-params.js"; +import { ChainId } from "../chain-id/chain-id.js"; + +export class LocalAccountsHandler extends ChainId implements RequestHandler { + readonly #addressToPrivateKey: Map = new Map(); + + constructor( + provider: EthereumProvider, + localAccountsHexPrivateKeys: string[], + ) { + super(provider); + + this.#initializePrivateKeys(localAccountsHexPrivateKeys); + } + + public async handle( + jsonRpcRequest: JsonRpcRequest, + ): Promise { + const response = await this.#resolveRequest(jsonRpcRequest); + if (response !== null) { + return response; + } + + await this.#modifyRequest(jsonRpcRequest); + + return jsonRpcRequest; + } + + async #resolveRequest( + jsonRpcRequest: JsonRpcRequest, + ): Promise { + if ( + jsonRpcRequest.method === "eth_accounts" || + jsonRpcRequest.method === "eth_requestAccounts" + ) { + return this.#createJsonRpcResponse(jsonRpcRequest.id, [ + ...this.#addressToPrivateKey.keys(), + ]); + } + + const params = getRequestParams(jsonRpcRequest); + + if (jsonRpcRequest.method === "eth_sign") { + if (params.length > 0) { + const [address, data] = validateParams(params, rpcAddress, rpcData); + + if (address !== undefined) { + if (data === undefined) { + throw new HardhatError( + HardhatError.ERRORS.NETWORK.ETHSIGN_MISSING_DATA_PARAM, + ); + } + + const privateKey = this.#getPrivateKeyForAddress(address); + return this.#createJsonRpcResponse( + jsonRpcRequest.id, + typed.personal.sign(data, privateKey), + ); + } + } + } + + if (jsonRpcRequest.method === "personal_sign") { + if (params.length > 0) { + const [data, address] = validateParams(params, rpcData, rpcAddress); + + if (data !== undefined) { + if (address === undefined) { + throw new HardhatError( + HardhatError.ERRORS.NETWORK.PERSONALSIGN_MISSING_ADDRESS_PARAM, + ); + } + + const privateKey = this.#getPrivateKeyForAddress(address); + return this.#createJsonRpcResponse( + jsonRpcRequest.id, + typed.personal.sign(data, privateKey), + ); + } + } + } + + if (jsonRpcRequest.method === "eth_signTypedData_v4") { + const [address, data] = validateParams(params, rpcAddress, rpcAny); + + if (data === undefined) { + throw new HardhatError( + HardhatError.ERRORS.NETWORK.ETHSIGN_MISSING_DATA_PARAM, + ); + } + + let typedMessage = data; + if (typeof data === "string") { + try { + typedMessage = JSON.parse(data); + } catch { + throw new HardhatError( + HardhatError.ERRORS.NETWORK.ETHSIGN_TYPED_DATA_V4_INVALID_DATA_PARAM, + ); + } + } + + // if we don't manage the address, the method is forwarded + const privateKey = this.#getPrivateKeyForAddressOrNull(address); + if (privateKey !== null) { + return this.#createJsonRpcResponse( + jsonRpcRequest.id, + signTyped(typedMessage, privateKey), + ); + } + } + + return null; + } + + async #modifyRequest(jsonRpcRequest: JsonRpcRequest): Promise { + const params = getRequestParams(jsonRpcRequest); + + if (jsonRpcRequest.method === "eth_sendTransaction" && params.length > 0) { + const [txRequest] = validateParams(params, rpcTransactionRequest); + + if (txRequest.gas === undefined) { + throw new HardhatError( + HardhatError.ERRORS.NETWORK.MISSING_TX_PARAM_TO_SIGN_LOCALLY, + { param: "gas" }, + ); + } + + if (txRequest.from === undefined) { + throw new HardhatError( + HardhatError.ERRORS.NETWORK.MISSING_TX_PARAM_TO_SIGN_LOCALLY, + { param: "from" }, + ); + } + + const hasGasPrice = txRequest.gasPrice !== undefined; + const hasEip1559Fields = + txRequest.maxFeePerGas !== undefined || + txRequest.maxPriorityFeePerGas !== undefined; + + if (!hasGasPrice && !hasEip1559Fields) { + throw new HardhatError( + HardhatError.ERRORS.NETWORK.MISSING_FEE_PRICE_FIELDS, + ); + } + + if (hasGasPrice && hasEip1559Fields) { + throw new HardhatError( + HardhatError.ERRORS.NETWORK.INCOMPATIBLE_FEE_PRICE_FIELDS, + ); + } + + if (hasEip1559Fields && txRequest.maxFeePerGas === undefined) { + throw new HardhatError( + HardhatError.ERRORS.NETWORK.MISSING_TX_PARAM_TO_SIGN_LOCALLY, + { param: "maxFeePerGas" }, + ); + } + + if (hasEip1559Fields && txRequest.maxPriorityFeePerGas === undefined) { + throw new HardhatError( + HardhatError.ERRORS.NETWORK.MISSING_TX_PARAM_TO_SIGN_LOCALLY, + { param: "maxPriorityFeePerGas" }, + ); + } + + if (txRequest.nonce === undefined) { + txRequest.nonce = await this.#getNonce(txRequest.from); + } + + const privateKey = this.#getPrivateKeyForAddress(txRequest.from); + + const chainId = await this.getChainId(); + + const rawTransaction = await this.#getSignedTransaction( + txRequest, + chainId, + privateKey, + ); + + jsonRpcRequest.method = "eth_sendRawTransaction"; + jsonRpcRequest.params = [bytesToHexString(rawTransaction)]; + } + } + + #initializePrivateKeys(localAccountsHexPrivateKeys: string[]) { + const privateKeys: Uint8Array[] = localAccountsHexPrivateKeys.map((h) => + hexStringToBytes(h), + ); + + for (const pk of privateKeys) { + const address = addr.fromPrivateKey(pk).toLowerCase(); + this.#addressToPrivateKey.set(address, pk); + } + } + + #getPrivateKeyForAddress(address: Uint8Array): Uint8Array { + const pk = this.#addressToPrivateKey.get(bytesToHexString(address)); + if (pk === undefined) { + throw new HardhatError(HardhatError.ERRORS.NETWORK.NOT_LOCAL_ACCOUNT, { + account: bytesToHexString(address), + }); + } + + return pk; + } + + #getPrivateKeyForAddressOrNull(address: Uint8Array): Uint8Array | null { + try { + return this.#getPrivateKeyForAddress(address); + } catch { + return null; + } + } + + async #getNonce(address: Uint8Array): Promise { + const response = await this.provider.request({ + method: "eth_getTransactionCount", + params: [bytesToHexString(address), "pending"], + }); + + assertHardhatInvariant( + typeof response === "string", + "response should be a string", + ); + + return hexStringToBigInt(response); + } + + async #getSignedTransaction( + transactionRequest: RpcTransactionRequest, + chainId: number, + privateKey: Uint8Array, + ): Promise { + const txData = { + ...transactionRequest, + gasLimit: transactionRequest.gas, + }; + + const accessList = txData.accessList?.map(({ address, storageKeys }) => { + return { + address: addr.addChecksum(bytesToHexString(address)), + storageKeys: + storageKeys !== null + ? storageKeys.map((k) => bytesToHexString(k)) + : [], + }; + }); + + const checksummedAddress = addr.addChecksum( + txData.to !== undefined + ? bytesToHexString(txData.to).toLowerCase() + : "0x0", + ); + + assertHardhatInvariant( + txData.nonce !== undefined, + "nonce should be defined", + ); + + let transaction; + if (txData.maxFeePerGas !== undefined) { + transaction = Transaction.prepare({ + type: "eip1559", + to: checksummedAddress, + nonce: txData.nonce, + chainId: txData.chainId ?? toBigInt(chainId), + value: txData.value !== undefined ? txData.value : 0n, + data: txData.data !== undefined ? bytesToHexString(txData.data) : "", + gasLimit: txData.gasLimit, + maxFeePerGas: txData.maxFeePerGas, + maxPriorityFeePerGas: txData.maxPriorityFeePerGas, + accessList: accessList ?? [], + }); + } else if (accessList !== undefined) { + transaction = Transaction.prepare({ + type: "eip2930", + to: checksummedAddress, + nonce: txData.nonce, + chainId: txData.chainId ?? toBigInt(chainId), + value: txData.value !== undefined ? txData.value : 0n, + data: txData.data !== undefined ? bytesToHexString(txData.data) : "", + gasPrice: txData.gasPrice ?? 0n, + gasLimit: txData.gasLimit, + accessList, + }); + } else { + transaction = Transaction.prepare({ + type: "legacy", + to: checksummedAddress, + nonce: txData.nonce, + chainId: txData.chainId ?? toBigInt(chainId), + value: txData.value !== undefined ? txData.value : 0n, + data: txData.data !== undefined ? bytesToHexString(txData.data) : "", + gasPrice: txData.gasPrice ?? 0n, + gasLimit: txData.gasLimit, + }); + } + + const signedTransaction = transaction.signBy(privateKey); + + return signedTransaction.toRawBytes(); + } + + #createJsonRpcResponse( + id: number | string, + result: unknown, + ): JsonRpcResponse { + return { + jsonrpc: "2.0", + id, + result, + }; + } +} diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/chain-id/chain-id.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/chain-id/chain-id.ts index f90506fd35..0998cf6d10 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/chain-id/chain-id.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/chain-id/chain-id.ts @@ -13,12 +13,12 @@ import { * after being retrieved to avoid redundant requests. */ export class ChainId { - readonly #provider: EthereumProvider; + protected readonly provider: EthereumProvider; #chainId: number | undefined; constructor(provider: EthereumProvider) { - this.#provider = provider; + this.provider = provider; } public async getChainId(): Promise { @@ -35,7 +35,7 @@ export class ChainId { } async #getChainIdFromEthChainId(): Promise { - const id = await this.#provider.request({ + const id = await this.provider.request({ method: "eth_chainId", }); @@ -45,7 +45,7 @@ export class ChainId { } async #getChainIdFromEthNetVersion(): Promise { - const id = await this.#provider.request({ + const id = await this.provider.request({ method: "net_version", }); diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/e2e.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/e2e.ts index c0987ed790..5d4efc81c5 100644 --- a/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/e2e.ts +++ b/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/e2e.ts @@ -1,7 +1,14 @@ +import type { HttpNetworkHDAccountsConfig } from "../../../../../src/types/config.js"; + import assert from "node:assert/strict"; import { describe, it } from "node:test"; -import { numberToHexString } from "@ignored/hardhat-vnext-utils/hex"; +import { + hexStringToBytes, + numberToHexString, +} from "@ignored/hardhat-vnext-utils/hex"; + +import { getJsonRpcRequest } from "../../../../../src/internal/builtin-plugins/network-manager/json-rpc.js"; import { createMockedNetworkHre } from "./hooks-mock.js"; @@ -19,12 +26,12 @@ describe("request-handlers - e2e", () => { { networks: { localhost: { - gas: FIXED_GAS_LIMIT, - gasPrice: FIXED_GAS_PRICE, - from: "0x2a97a65d5673a2c61e95ce33cecadf24f654f96d", type: "http", url: "http://localhost:8545", chainId: 1, + gas: FIXED_GAS_LIMIT, + gasPrice: FIXED_GAS_PRICE, + from: "0x2a97a65d5673a2c61e95ce33cecadf24f654f96d", }, }, }, @@ -60,7 +67,7 @@ describe("request-handlers - e2e", () => { it("should successfully executes all the handlers that set automatic values", async () => { // should use the handlers in this order: ChainIdValidatorHandler, AutomaticGasHandler, AutomaticGasPriceHandler, AutomaticSenderHandler - const FIXED_GAS_LIMIT = 1231; + const MOCKED_GAS_LIMIT = 21000; const GAS_MULTIPLIER = 1.337; const LATEST_BASE_FEE_IN_MOCKED_PROVIDER = 80; @@ -68,11 +75,11 @@ describe("request-handlers - e2e", () => { { networks: { localhost: { - gas: "auto", - gasMultiplier: GAS_MULTIPLIER, type: "http", url: "http://localhost:8545", chainId: 1, + gas: "auto", + gasMultiplier: GAS_MULTIPLIER, }, }, }, @@ -81,9 +88,9 @@ describe("request-handlers - e2e", () => { eth_chainId: "0x1", eth_getBlockByNumber: { baseFeePerGas: "0x1", - gasLimit: numberToHexString(FIXED_GAS_LIMIT * 1000), + gasLimit: numberToHexString(MOCKED_GAS_LIMIT * 1000), }, - eth_estimateGas: numberToHexString(FIXED_GAS_LIMIT), + eth_estimateGas: numberToHexString(MOCKED_GAS_LIMIT), eth_feeHistory: { baseFeePerGas: [ numberToHexString(LATEST_BASE_FEE_IN_MOCKED_PROVIDER), @@ -116,7 +123,7 @@ describe("request-handlers - e2e", () => { // gas assert.equal( res[0].gas, - numberToHexString(Math.floor(FIXED_GAS_LIMIT * GAS_MULTIPLIER)), + numberToHexString(Math.floor(MOCKED_GAS_LIMIT * GAS_MULTIPLIER)), ); // gas price assert.equal(res[0].maxPriorityFeePerGas, "0x4"); @@ -124,4 +131,141 @@ describe("request-handlers - e2e", () => { // sender assert.equal(res[0].from, "0x123006d4548a3ac17d72b372ae1e416bf65b8eaf"); }); + + describe("local account and HD account", () => { + it("should successfully executes all the handlers that set automatic values using the local account", async () => { + // should use the handlers in this order: ChainIdValidatorHandler, AutomaticGasHandler, AutomaticGasPriceHandler, LocalAccountsHandler + + const MOCKED_GAS_LIMIT = 21000; + const GAS_MULTIPLIER = 1.337; + const LATEST_BASE_FEE_IN_MOCKED_PROVIDER = 80; + const accounts = [ + "0xd78629ec714c4c72e04e294bb21615ddcb4d15dbb63db0bd84a8e084c7134c13", + ]; + + const hre = await createMockedNetworkHre( + { + networks: { + localhost: { + type: "http", + url: "http://localhost:8545", + chainId: 1, + gas: "auto", + gasMultiplier: GAS_MULTIPLIER, + accounts, + }, + }, + }, + { + // List of methods that the handlers will call; we mock the responses + eth_chainId: "0x1", + eth_getBlockByNumber: { + baseFeePerGas: "0x1", + gasLimit: numberToHexString(MOCKED_GAS_LIMIT * 1000), + }, + eth_estimateGas: numberToHexString(MOCKED_GAS_LIMIT), + eth_feeHistory: { + baseFeePerGas: [ + numberToHexString(LATEST_BASE_FEE_IN_MOCKED_PROVIDER), + numberToHexString( + Math.floor((LATEST_BASE_FEE_IN_MOCKED_PROVIDER * 9) / 8), + ), + ], + reward: [["0x4"]], + }, + eth_getTransactionCount: numberToHexString(0x8), + }, + ); + + // Use the localhost network for these tests because the modifier is only + // applicable to HTTP networks. EDR networks do not require this modifier. + const connection = await hre.network.connect("localhost"); + + const tx = { + from: "0x4F3e91d2CaCd82FffD1f33A0d26d4078401986e9", + to: "0x4F3e91d2CaCd82FffD1f33A0d26d4078401986e9", + }; + + const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [tx]); + + const res = await connection.provider.request(jsonRpcRequest); + + assert.ok(Array.isArray(res), "params should be an array"); + + const rawTransaction = hexStringToBytes(res[0]); + + // The tx type is encoded in the first byte, and it must be the EIP-1559 one + assert.equal(rawTransaction[0], 2); + }); + + it("should successfully executes all the handlers that set automatic values using the HD account", async () => { + // should use the handlers in this order: ChainIdValidatorHandler, AutomaticGasHandler, AutomaticGasPriceHandler, HDAccountsHandler + + const MOCKED_GAS_LIMIT = 21000; + const GAS_MULTIPLIER = 1.337; + const LATEST_BASE_FEE_IN_MOCKED_PROVIDER = 80; + const HD_ACCOUNT: HttpNetworkHDAccountsConfig = { + mnemonic: + "couch hunt wisdom giant regret supreme issue sing enroll ankle type husband", + path: "m/44'/60'/0'/0/", + initialIndex: 0, + count: 2, + passphrase: "", + }; + + const hre = await createMockedNetworkHre( + { + networks: { + localhost: { + type: "http", + url: "http://localhost:8545", + chainId: 1, + gas: "auto", + gasMultiplier: GAS_MULTIPLIER, + accounts: HD_ACCOUNT, + }, + }, + }, + { + // List of methods that the handlers will call; we mock the responses + eth_chainId: "0x1", + eth_getBlockByNumber: { + baseFeePerGas: "0x1", + gasLimit: numberToHexString(MOCKED_GAS_LIMIT * 1000), + }, + eth_estimateGas: numberToHexString(MOCKED_GAS_LIMIT), + eth_feeHistory: { + baseFeePerGas: [ + numberToHexString(LATEST_BASE_FEE_IN_MOCKED_PROVIDER), + numberToHexString( + Math.floor((LATEST_BASE_FEE_IN_MOCKED_PROVIDER * 9) / 8), + ), + ], + reward: [["0x4"]], + }, + eth_getTransactionCount: numberToHexString(0x8), + }, + ); + + // Use the localhost network for these tests because the modifier is only + // applicable to HTTP networks. EDR networks do not require this modifier. + const connection = await hre.network.connect("localhost"); + + const tx = { + from: "0x4F3e91d2CaCd82FffD1f33A0d26d4078401986e9", + to: "0x4F3e91d2CaCd82FffD1f33A0d26d4078401986e9", + }; + + const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [tx]); + + const res = await connection.provider.request(jsonRpcRequest); + + assert.ok(Array.isArray(res), "params should be an array"); + + const rawTransaction = hexStringToBytes(res[0]); + + // The tx type is encoded in the first byte, and it must be the EIP-1559 one + assert.equal(rawTransaction[0], 2); + }); + }); }); diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/hd-wallet.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/hd-wallet.ts new file mode 100644 index 0000000000..44b58de361 --- /dev/null +++ b/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/hd-wallet.ts @@ -0,0 +1,167 @@ +import type { JsonRpcResponse } from "../../../../../../../src/types/providers.js"; + +import assert from "node:assert/strict"; +import { beforeEach, describe, it } from "node:test"; + +import { HardhatError } from "@ignored/hardhat-vnext-errors"; +import { assertThrowsHardhatError } from "@nomicfoundation/hardhat-test-utils"; + +import { + getJsonRpcRequest, + isJsonRpcResponse, +} from "../../../../../../../src/internal/builtin-plugins/network-manager/json-rpc.js"; +import { HDWalletHandler } from "../../../../../../../src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/hd-wallet-handler.js"; +import { EthereumMockedProvider } from "../../ethereum-mocked-provider.js"; + +function getResult(res: JsonRpcResponse | null): string[] { + assert.ok(res !== null, "res should not be null"); + assert.ok("result" in res, "res should have the property 'result'"); + assert.ok(Array.isArray(res.result), "res.result should be an array"); + + return res.result; +} + +describe("HDWalletHandler", () => { + let hdWalletHandler: HDWalletHandler; + let mockedProvider: EthereumMockedProvider; + + const mnemonic = + "couch hunt wisdom giant regret supreme issue sing enroll ankle type husband"; + const hdpath = "m/44'/60'/0'/0/"; + + beforeEach(() => { + mockedProvider = new EthereumMockedProvider(); + + hdWalletHandler = new HDWalletHandler(mockedProvider, mnemonic, hdpath); + }); + + it("should generate a valid address", async () => { + const jsonRpcRequest = getJsonRpcRequest(1, "eth_accounts"); + + const res = await hdWalletHandler.handle(jsonRpcRequest); + + assert.ok(isJsonRpcResponse(res), "Expected a JSON-RPC response"); + + const result = getResult(res); + + assert.equal(result[0], "0x4f3e91d2cacd82fffd1f33a0d26d4078401986e9"); + }); + + it("should generate a valid address with passphrase", async () => { + const passphrase = "this is a secret"; + + hdWalletHandler = new HDWalletHandler( + mockedProvider, + mnemonic, + hdpath, + 0, + 10, + passphrase, + ); + + const jsonRpcRequest = getJsonRpcRequest(1, "eth_accounts"); + + const res = await hdWalletHandler.handle(jsonRpcRequest); + + assert.ok(isJsonRpcResponse(res), "Expected a JSON-RPC response"); + + const result = getResult(res); + + assert.equal(result[0], "0x6955b833d195e49c07fc56fbf0ec387325facb87"); + }); + + it("should generate a valid address when given a different index", async () => { + hdWalletHandler = new HDWalletHandler(mockedProvider, mnemonic, hdpath, 1); + + const jsonRpcRequest = getJsonRpcRequest(1, "eth_accounts"); + + const res = await hdWalletHandler.handle(jsonRpcRequest); + + assert.ok(isJsonRpcResponse(res), "Expected a JSON-RPC response"); + + const result = getResult(res); + + assert.equal(result[0], "0x2a97a65d5673a2c61e95ce33cecadf24f654f96d"); + }); + + it("should generate 2 accounts", async () => { + hdWalletHandler = new HDWalletHandler( + mockedProvider, + mnemonic, + hdpath, + 0, + 2, + ); + + const jsonRpcRequest = getJsonRpcRequest(1, "eth_accounts"); + + const res = await hdWalletHandler.handle(jsonRpcRequest); + + assert.ok(isJsonRpcResponse(res), "Expected a JSON-RPC response"); + + const result = getResult(res); + + assert.deepEqual(result, [ + "0x4f3e91d2cacd82fffd1f33a0d26d4078401986e9", + "0x2a97a65d5673a2c61e95ce33cecadf24f654f96d", + ]); + }); + + describe("HDPath formatting", () => { + it("should work if it doesn't end in a /", async () => { + hdWalletHandler = new HDWalletHandler( + mockedProvider, + mnemonic, + "m/44'/60'/0'/0", + ); + + const jsonRpcRequest = getJsonRpcRequest(1, "eth_accounts"); + + const res = await hdWalletHandler.handle(jsonRpcRequest); + + assert.ok(isJsonRpcResponse(res), "Expected a JSON-RPC response"); + + const result = getResult(res); + + assert.equal(result[0], "0x4f3e91d2cacd82fffd1f33a0d26d4078401986e9"); + }); + + it("should throw if the path is invalid", () => { + assertThrowsHardhatError( + () => new HDWalletHandler(mockedProvider, mnemonic, ""), + HardhatError.ERRORS.NETWORK.INVALID_HD_PATH, + { path: "" }, + ); + + assertThrowsHardhatError( + () => new HDWalletHandler(mockedProvider, mnemonic, "m/"), + HardhatError.ERRORS.NETWORK.INVALID_HD_PATH, + { path: "m/" }, + ); + + assertThrowsHardhatError( + () => new HDWalletHandler(mockedProvider, mnemonic, "m//"), + HardhatError.ERRORS.NETWORK.INVALID_HD_PATH, + { path: "m//" }, + ); + + assertThrowsHardhatError( + () => new HDWalletHandler(mockedProvider, mnemonic, "m/'"), + HardhatError.ERRORS.NETWORK.INVALID_HD_PATH, + { path: "m/'" }, + ); + + assertThrowsHardhatError( + () => new HDWalletHandler(mockedProvider, mnemonic, "m/0''"), + HardhatError.ERRORS.NETWORK.INVALID_HD_PATH, + { path: "m/0''" }, + ); + + assertThrowsHardhatError( + () => new HDWalletHandler(mockedProvider, mnemonic, "ghj"), + HardhatError.ERRORS.NETWORK.INVALID_HD_PATH, + { path: "ghj" }, + ); + }); + }); +}); diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/local-accounts.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/local-accounts.ts new file mode 100644 index 0000000000..2dc4f3600e --- /dev/null +++ b/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/local-accounts.ts @@ -0,0 +1,754 @@ +import assert from "node:assert/strict"; +import { beforeEach, describe, it } from "node:test"; + +import { HardhatError } from "@ignored/hardhat-vnext-errors"; +import { + hexStringToBytes, + numberToHexString, +} from "@ignored/hardhat-vnext-utils/hex"; +import { assertRejectsWithHardhatError } from "@nomicfoundation/hardhat-test-utils"; +import { addr } from "micro-eth-signer"; + +import { getJsonRpcRequest } from "../../../../../../../src/internal/builtin-plugins/network-manager/json-rpc.js"; +import { LocalAccountsHandler } from "../../../../../../../src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/local-accounts.js"; +import { EthereumMockedProvider } from "../../ethereum-mocked-provider.js"; + +// This is a valid raw EIP_2930 tx checked in a local hardhat node, +// where the sender account had funds and the chain id was 123. +const EXPECTED_RAW_TX = + "0x01f89a7b800182753094b5bc06d4548a3ac17d72b372ae1e416bf65b8e" + + "ad0180f838f79457d7ad4d3f0c74e3766874cf06fa1dc23c21f7e8e1a0a5" + + "0e92910457911e0e22d6dd1672f440a37b590b231d8309101255290f5394" + + "ec80a02b2fca5e2cf3569d29693e965f045529efa6a54bf0ab11104dd4ea" + + "8b2ca3daf7a06025c30f36a179a09b9952e025632a65f220ec385eccd23a" + + "1fb952976eace481"; + +const EXPECTED_RAW_TX_VALUES = { + from: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", + to: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", + gas: "0x7530", + gasLimit: "0x7530", + gasPrice: "0x1", + nonce: "0x0", + value: "0x1", + accessList: [ + { + address: "0x57d7ad4d3f0c74e3766874cf06fa1dc23c21f7e8", + storageKeys: [ + "0xa50e92910457911e0e22d6dd1672f440a37b590b231d8309101255290f5394ec", + ], + }, + ], +}; + +/** + * Validate that `rawTx` is an EIP-2930 transaction that has + * the same values as `tx` + */ +function validateRawEIP2930Transaction(tx: any) { + assert.equal(EXPECTED_RAW_TX_VALUES.from, tx.from); + assert.equal(EXPECTED_RAW_TX_VALUES.to, tx.to); + + assert.equal(EXPECTED_RAW_TX_VALUES.gas, tx.gas); + assert.equal(EXPECTED_RAW_TX_VALUES.gasPrice, tx.gasPrice); + assert.equal(EXPECTED_RAW_TX_VALUES.nonce, tx.nonce); + assert.equal(EXPECTED_RAW_TX_VALUES.value, tx.value); + assert.deepEqual(EXPECTED_RAW_TX_VALUES.accessList, tx.accessList); +} + +const MOCK_PROVIDER_CHAIN_ID = 123; + +describe("LocalAccountsHandler", () => { + let localAccountsHandler: LocalAccountsHandler; + + let mockedProvider: EthereumMockedProvider; + + const accounts = [ + "0xb2e31025a2474b37e4c2d2931929a00b5752b98a3af45e3fd9a62ddc3cdf370e", + "0x6d7229c1db5892730b84b4bc10543733b72cabf4cd3130d910faa8e459bb8eca", + "0x6d4ec871d9b5469119bbfc891e958b6220d076a6849006098c370c8af5fc7776", + "0xec02c2b7019e75378a05018adc30a0252ba705670acb383a1d332e57b0b792d2", + ]; + + beforeEach(() => { + mockedProvider = new EthereumMockedProvider(); + + mockedProvider.setReturnValue( + "net_version", + numberToHexString(MOCK_PROVIDER_CHAIN_ID), + ); + mockedProvider.setReturnValue( + "eth_getTransactionCount", + numberToHexString(0x8), + ); + mockedProvider.setReturnValue("eth_accounts", []); + + localAccountsHandler = new LocalAccountsHandler(mockedProvider, accounts); + }); + + describe("resolveRequest", () => { + it("should return the account addresses in eth_accounts", async () => { + const jsonRpcRequest = getJsonRpcRequest(1, "eth_accounts"); + + const res = await localAccountsHandler.handle(jsonRpcRequest); + + assert.ok(res !== null, "res should not be null"); + assert.ok("result" in res, "res should have the property 'result'"); + assert.ok(Array.isArray(res.result), "res.result should be an array"); + + assert.equal( + res.result[0], + addr.fromPrivateKey(accounts[0]).toLowerCase(), + ); + assert.equal( + res.result[1], + addr.fromPrivateKey(accounts[1]).toLowerCase(), + ); + }); + + it("should return the account addresses in eth_requestAccounts", async () => { + const jsonRpcRequest = getJsonRpcRequest(1, "eth_requestAccounts"); + + const res = await localAccountsHandler.handle(jsonRpcRequest); + + assert.ok(res !== null, "res should not be null"); + assert.ok("result" in res, "res should have the property 'result'"); + assert.ok(Array.isArray(res.result), "res.result should be an array"); + + assert.equal( + res.result[0], + addr.fromPrivateKey(accounts[0]).toLowerCase(), + ); + assert.equal( + res.result[1], + addr.fromPrivateKey(accounts[1]).toLowerCase(), + ); + }); + + it("should forward other methods", async () => { + const input = [1, 2]; + const jsonRpcRequest = getJsonRpcRequest(1, "eth_sarasa", input); + + const res = await localAccountsHandler.handle(jsonRpcRequest); + + assert.deepEqual(res, jsonRpcRequest); + }); + + describe("eth_sign", () => { + it("should be compatible with parity's implementation", async () => { + // This test was created by using Parity Ethereum + // v2.2.5-beta-7fbcdfeed-20181213 and calling eth_sign + localAccountsHandler = new LocalAccountsHandler(mockedProvider, [ + "0x6e59a6617c48d76d3b21d722eaba867e16ecf54ab3da7a93724f51812bc6d1aa", + ]); + + const jsonRpcRequest = getJsonRpcRequest(1, "eth_sign", [ + "0x24f1a362780503D762060C1683864C4066A74b05", + "0x41206d657373616765", + ]); + + const res = await localAccountsHandler.handle(jsonRpcRequest); + + assert.ok(res !== null, "res should not be null"); + assert.ok("result" in res, "res should have the property 'result'"); + + assert.equal( + res.result, + "0x25c349f668c90a890c84aa79a78cf6c74e96483b43ec3ed06aa8aec835477c034aa096e883cc9871aa4ffdffd9f21f6ee4aa4b70f478ad56a18971e4ec2c753e1b", + ); + }); + + it("should be compatible with ganache-cli's implementation", async () => { + // This test was created by using Ganache CLI v6.1.6 (ganache-core: 2.1.5) + localAccountsHandler = new LocalAccountsHandler(mockedProvider, [ + "0xf159c85082f4dd4ee472583a37a1b5683c727ec99708f3d94ff05faa7a7a70ce", + ]); + + const jsonRpcRequest = getJsonRpcRequest(1, "eth_sign", [ + "0x0a929c90dd22f0fb09ec38983780530ee30a29a3", + "0x41206d657373616765", + ]); + + const res = await localAccountsHandler.handle(jsonRpcRequest); + + assert.ok(res !== null, "res should not be null"); + assert.ok("result" in res, "res should have the property 'result'"); + assert.ok( + typeof res.result === "string", + "res.result should be a string", + ); + + // This test is weird because ganache encodes the v param of the signature + // differently than the rest. It subtracts 27 from it before serializing. + assert.equal( + res.result.slice(0, -2), + "0x84d993fc1b54926db1b6b81544aada29f0f36850a83dc979e8bacfa87e7c7cb11689b2f4ca64697842c42bb7e0cb02dff1851b42e25e62858f27f57bd00ff74b00".slice( + 0, + -2, + ), + ); + }); + + it("should be compatible with geth's implementation", async () => { + // This test was created by using Geth 1.8.20-stable + localAccountsHandler = new LocalAccountsHandler(mockedProvider, [ + "0xf2d19e944851ea0faa9440e24a22ddab850210cae46b306a3fde4c98b22a0dcb", + ]); + + const jsonRpcRequest = getJsonRpcRequest(1, "eth_sign", [ + "0x5Fd8509eABccFFec1d2530e48F55545B49Bd5B5e", + "0x41206d657373616765", + ]); + + const res = await localAccountsHandler.handle(jsonRpcRequest); + + assert.ok(res !== null, "res should not be null"); + assert.ok("result" in res, "res should have the property 'result'"); + + assert.equal( + res.result, + "0x88c6ac158d40e84f519fbb48b6a1355a31202b684163f637fe5c92cc1109acbe5c79a2dd95a8aecff45756c6fc3b4fc8aef345179605bcead2916dd533fb22651b", + ); + }); + + it("should throw if no data is given", async () => { + const jsonRpcRequest = getJsonRpcRequest(1, "eth_sign", [ + addr.fromPrivateKey(accounts[0]), + ]); + + // eslint-disable-next-line no-restricted-syntax -- not a Hardhat error + await assert.rejects(localAccountsHandler.handle(jsonRpcRequest)); + }); + + it("should throw if the address isn't one of the local ones", async () => { + const jsonRpcRequest = getJsonRpcRequest(1, "eth_sign", [ + "0x000006d4548a3ac17d72b372ae1e416bf65b8ead", + "0x00", + ]); + + await assertRejectsWithHardhatError( + () => localAccountsHandler.handle(jsonRpcRequest), + HardhatError.ERRORS.NETWORK.NOT_LOCAL_ACCOUNT, + { + account: "0x000006d4548a3ac17d72b372ae1e416bf65b8ead", + }, + ); + }); + + it("should just forward if no address is given", async () => { + const jsonRpcRequest = getJsonRpcRequest(1, "eth_sign"); + + const res = await localAccountsHandler.handle(jsonRpcRequest); + + assert.deepEqual(res, jsonRpcRequest); + }); + }); + + describe("eth_signTypedData_v4", () => { + it("should be compatible with EIP-712 example", async () => { + // This test was taken from the `eth_signTypedData` example from the + // EIP-712 specification. + // + localAccountsHandler = new LocalAccountsHandler(mockedProvider, [ + // keccak256("cow") + "0xc85ef7d79691fe79573b1a7064c19c1a9819ebdbd1faaab1a8ec92344438aaf4", + ]); + + const jsonRpcRequest = getJsonRpcRequest(1, "eth_signTypedData_v4", [ + "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", + { + types: { + EIP712Domain: [ + { name: "name", type: "string" }, + { name: "version", type: "string" }, + { name: "chainId", type: "uint256" }, + { name: "verifyingContract", type: "address" }, + ], + Person: [ + { name: "name", type: "string" }, + { name: "wallet", type: "address" }, + ], + Mail: [ + { name: "from", type: "Person" }, + { name: "to", type: "Person" }, + { name: "contents", type: "string" }, + ], + }, + primaryType: "Mail", + domain: { + name: "Ether Mail", + version: "1", + chainId: 1, + verifyingContract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC", + }, + message: { + from: { + name: "Cow", + wallet: "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", + }, + to: { + name: "Bob", + wallet: "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB", + }, + contents: "Hello, Bob!", + }, + }, + ]); + + const res = await localAccountsHandler.handle(jsonRpcRequest); + + assert.ok(res !== null, "res should not be null"); + assert.ok("result" in res, "res should have the property 'result'"); + + assert.equal( + res.result, + "0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b915621c", + ); + }); + + it("should be compatible with stringified JSON input", async () => { + localAccountsHandler = new LocalAccountsHandler(mockedProvider, [ + // keccak256("cow") + "0xc85ef7d79691fe79573b1a7064c19c1a9819ebdbd1faaab1a8ec92344438aaf4", + ]); + + const jsonRpcRequest = getJsonRpcRequest(1, "eth_signTypedData_v4", [ + "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", + JSON.stringify({ + types: { + EIP712Domain: [ + { name: "name", type: "string" }, + { name: "version", type: "string" }, + { name: "chainId", type: "uint256" }, + { name: "verifyingContract", type: "address" }, + ], + Person: [ + { name: "name", type: "string" }, + { name: "wallet", type: "address" }, + ], + Mail: [ + { name: "from", type: "Person" }, + { name: "to", type: "Person" }, + { name: "contents", type: "string" }, + ], + }, + primaryType: "Mail", + domain: { + name: "Ether Mail", + version: "1", + chainId: 1, + verifyingContract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC", + }, + message: { + from: { + name: "Cow", + wallet: "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", + }, + to: { + name: "Bob", + wallet: "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB", + }, + contents: "Hello, Bob!", + }, + }), + ]); + + const res = await localAccountsHandler.handle(jsonRpcRequest); + + assert.ok(res !== null, "res should not be null"); + assert.ok("result" in res, "res should have the property 'result'"); + + assert.equal( + res.result, + "0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b915621c", + ); + }); + + it("should throw if data string input is not JSON", async () => { + const jsonRpcRequest = getJsonRpcRequest(1, "eth_signTypedData_v4", [ + "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", + "}thisisnotvalidjson{", + ]); + + await assertRejectsWithHardhatError( + () => localAccountsHandler.handle(jsonRpcRequest), + HardhatError.ERRORS.NETWORK.ETHSIGN_TYPED_DATA_V4_INVALID_DATA_PARAM, + {}, + ); + }); + + it("should throw if no data is given", async () => { + const jsonRpcRequest = getJsonRpcRequest(1, "eth_signTypedData_v4", [ + addr.fromPrivateKey(accounts[0]), + ]); + + await assertRejectsWithHardhatError( + () => localAccountsHandler.handle(jsonRpcRequest), + HardhatError.ERRORS.NETWORK.ETHSIGN_MISSING_DATA_PARAM, + {}, + ); + }); + + it("should just forward if the address isn't one of the local ones", async () => { + const jsonRpcRequest = getJsonRpcRequest(1, "eth_signTypedData_v4", [ + "0x000006d4548a3ac17d72b372ae1e416bf65b8ead", + {}, + ]); + + const res = await localAccountsHandler.handle(jsonRpcRequest); + + assert.deepEqual(res, jsonRpcRequest); + }); + }); + + describe("personal_sign", () => { + it("should be compatible with geth's implementation", async () => { + // This test was created by using Geth 1.10.12-unstable and calling personal_sign + localAccountsHandler = new LocalAccountsHandler(mockedProvider, [ + "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", + ]); + + const jsonRpcRequest = getJsonRpcRequest(1, "personal_sign", [ + "0x5417aa2a18a44da0675524453ff108c545382f0d7e26605c56bba47c21b5e979", + "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + ]); + + const res = await localAccountsHandler.handle(jsonRpcRequest); + + assert.ok(res !== null, "res should not be null"); + assert.ok("result" in res, "res should have the property 'result'"); + + assert.equal( + res.result, + "0x9c73dd4937a37eecab3abb54b74b6ec8e500080431d36afedb1726624587ee6710296e10c1194dded7376f13ff03ef6c9e797eb86bae16c20c57776fc69344271c", + ); + }); + + it("should be compatible with metamask's implementation", async () => { + // This test was created by using Metamask 10.3.0 + localAccountsHandler = new LocalAccountsHandler(mockedProvider, [ + "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", + ]); + + const jsonRpcRequest = getJsonRpcRequest(1, "personal_sign", [ + "0x7699f568ecd7753e6ddf75a42fa4c2cc86cbbdc704c9eb1a6b6d4b9d8b8d1519", + "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + ]); + + const res = await localAccountsHandler.handle(jsonRpcRequest); + + assert.ok(res !== null, "res should not be null"); + assert.ok("result" in res, "res should have the property 'result'"); + + assert.equal( + res.result, + "0x2875e4206c9fe3b229291c81f95cc4f421e2f4d3e023f5b4041daa56ab4000977010b47a3c01036ec8a6a0872aec2ab285150f003d01b0d8da60c1cceb9154181c", + ); + }); + }); + }); + + describe("modifyRequest", () => { + it("should, given two identical tx, return the same raw transaction", async () => { + const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ + { + from: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", + to: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", + gas: numberToHexString(21000), + gasPrice: numberToHexString(678912), + nonce: numberToHexString(0), + value: numberToHexString(1), + }, + ]); + + await localAccountsHandler.handle(jsonRpcRequest); + + // This transaction was submitted to a blockchain and accepted, so the signature must be valid + const expectedRaw = + "0xf86480830a5c0082520894b5bc06d4548a3ac17d72b372ae1" + + "e416bf65b8ead018082011aa0614471b82c6ffedd4722ca5faa7f9b309a923661a4b2" + + "adc1a53a3ebe8c4d1f0aa06aebf2fbbe82703e5075965c65c776a9caeeff4b637f203" + + "d65383e1ed2e22654"; + + assert.deepEqual(jsonRpcRequest, { + jsonrpc: "2.0", + id: 1, + method: "eth_sendRawTransaction", + params: [expectedRaw], + }); + }); + + it("should send eip1559 txs if the eip1559 fields are present", async () => { + const tx = { + from: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", + to: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", + gas: numberToHexString(30000), + nonce: numberToHexString(0), + value: numberToHexString(1), + chainId: numberToHexString(MOCK_PROVIDER_CHAIN_ID), + maxFeePerGas: numberToHexString(12), + maxPriorityFeePerGas: numberToHexString(2), + }; + + const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [tx]); + + await localAccountsHandler.handle(jsonRpcRequest); + + assert.ok( + Array.isArray(jsonRpcRequest.params), + "params should be an array", + ); + + const rawTransaction = hexStringToBytes(jsonRpcRequest.params[0]); + + // The tx type is encoded in the first byte, and it must be the EIP-1559 one + assert.equal(rawTransaction[0], 2); + }); + + it("should send access list transactions", async () => { + const tx = { + from: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", + to: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", + gas: numberToHexString(30000), + gasPrice: numberToHexString(1), + nonce: numberToHexString(0), + value: numberToHexString(1), + chainId: numberToHexString(MOCK_PROVIDER_CHAIN_ID), + accessList: [ + { + address: "0x57d7ad4d3f0c74e3766874cf06fa1dc23c21f7e8", + storageKeys: [ + "0xa50e92910457911e0e22d6dd1672f440a37b590b231d8309101255290f5394ec", + ], + }, + ], + }; + + const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [tx]); + + await localAccountsHandler.handle(jsonRpcRequest); + + assert.ok( + Array.isArray(jsonRpcRequest.params), + "params should be an array", + ); + + const rawTransaction = jsonRpcRequest.params[0]; + + assert.equal(rawTransaction, EXPECTED_RAW_TX); + + validateRawEIP2930Transaction(tx); + }); + + it("should add the chainId value if it's missing", async () => { + const tx = { + from: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", + to: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", + gas: numberToHexString(30000), + gasPrice: numberToHexString(1), + nonce: numberToHexString(0), + value: numberToHexString(1), + accessList: [ + { + address: "0x57d7ad4d3f0c74e3766874cf06fa1dc23c21f7e8", + storageKeys: [ + "0xa50e92910457911e0e22d6dd1672f440a37b590b231d8309101255290f5394ec", + ], + }, + ], + }; + + const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [tx]); + + await localAccountsHandler.handle(jsonRpcRequest); + + assert.ok( + Array.isArray(jsonRpcRequest.params), + "params should be an array", + ); + + const rawTransaction = jsonRpcRequest.params[0]; + + assert.equal(rawTransaction, EXPECTED_RAW_TX); + + validateRawEIP2930Transaction(tx); + }); + + it("should get the nonce if not provided", async () => { + const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ + { + from: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", + to: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", + gas: numberToHexString(21000), + gasPrice: numberToHexString(678912), + value: numberToHexString(1), + }, + ]); + + await localAccountsHandler.handle(jsonRpcRequest); + + assert.equal( + mockedProvider.getNumberOfCalls("eth_getTransactionCount"), + 1, + ); + }); + + it("should throw when calling sendTransaction without gas", async () => { + const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ + { + from: addr.fromPrivateKey(accounts[0]), + to: "0x2a97a65d5673a2c61e95ce33cecadf24f654f96d", + gasPrice: numberToHexString(0x3b9aca00), + nonce: numberToHexString(0x8), + }, + ]); + + await assertRejectsWithHardhatError( + () => localAccountsHandler.handle(jsonRpcRequest), + HardhatError.ERRORS.NETWORK.MISSING_TX_PARAM_TO_SIGN_LOCALLY, + { param: "gas" }, + ); + }); + + it("should throw when calling sendTransaction without gasPrice, maxFeePerGas and maxPriorityFeePerGas", async () => { + const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ + { + from: addr.fromPrivateKey(accounts[0]), + to: "0x2a97a65d5673a2c61e95ce33cecadf24f654f96d", + nonce: numberToHexString(0x8), + gas: numberToHexString(123), + }, + ]); + + await assertRejectsWithHardhatError( + () => localAccountsHandler.handle(jsonRpcRequest), + HardhatError.ERRORS.NETWORK.MISSING_FEE_PRICE_FIELDS, + {}, + ); + }); + + it("should throw when calling sendTransaction with gasPrice and EIP1559 fields", async () => { + const jsonRpcRequest1 = getJsonRpcRequest(1, "eth_sendTransaction", [ + { + from: addr.fromPrivateKey(accounts[0]), + to: "0x2a97a65d5673a2c61e95ce33cecadf24f654f96d", + nonce: numberToHexString(0x8), + gas: numberToHexString(123), + gasPrice: numberToHexString(1), + maxFeePerGas: numberToHexString(1), + }, + ]); + + const jsonRpcRequest2 = getJsonRpcRequest(1, "eth_sendTransaction", [ + { + from: addr.fromPrivateKey(accounts[0]), + to: "0x2a97a65d5673a2c61e95ce33cecadf24f654f96d", + nonce: numberToHexString(0x8), + gas: numberToHexString(123), + gasPrice: numberToHexString(1), + maxPriorityFeePerGas: numberToHexString(1), + }, + ]); + + await assertRejectsWithHardhatError( + () => localAccountsHandler.handle(jsonRpcRequest1), + HardhatError.ERRORS.NETWORK.INCOMPATIBLE_FEE_PRICE_FIELDS, + {}, + ); + + await assertRejectsWithHardhatError( + () => localAccountsHandler.handle(jsonRpcRequest2), + HardhatError.ERRORS.NETWORK.INCOMPATIBLE_FEE_PRICE_FIELDS, + {}, + ); + }); + + it("should throw when only one EIP1559 field is provided", async () => { + const jsonRpcRequest1 = getJsonRpcRequest(1, "eth_sendTransaction", [ + { + from: addr.fromPrivateKey(accounts[0]), + to: "0x2a97a65d5673a2c61e95ce33cecadf24f654f96d", + nonce: numberToHexString(0x8), + gas: numberToHexString(123), + maxFeePerGas: numberToHexString(1), + }, + ]); + + const jsonRpcRequest2 = getJsonRpcRequest(1, "eth_sendTransaction", [ + { + from: addr.fromPrivateKey(accounts[0]), + to: "0x2a97a65d5673a2c61e95ce33cecadf24f654f96d", + nonce: numberToHexString(0x8), + gas: numberToHexString(123), + maxPriorityFeePerGas: numberToHexString(1), + }, + ]); + + await assertRejectsWithHardhatError( + () => localAccountsHandler.handle(jsonRpcRequest1), + HardhatError.ERRORS.NETWORK.MISSING_TX_PARAM_TO_SIGN_LOCALLY, + { param: "maxPriorityFeePerGas" }, + ); + + await assertRejectsWithHardhatError( + () => localAccountsHandler.handle(jsonRpcRequest2), + HardhatError.ERRORS.NETWORK.MISSING_TX_PARAM_TO_SIGN_LOCALLY, + { param: "maxFeePerGas" }, + ); + }); + + it("should throw if trying to send from an account that isn't local", async () => { + const jsonRpcRequest = getJsonRpcRequest(1, "eth_sendTransaction", [ + { + from: "0x000006d4548a3ac17d72b372ae1e416bf65b8ead", + to: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", + gas: numberToHexString(21000), + gasPrice: numberToHexString(678912), + nonce: numberToHexString(0), + value: numberToHexString(1), + }, + ]); + + await assertRejectsWithHardhatError( + () => localAccountsHandler.handle(jsonRpcRequest), + HardhatError.ERRORS.NETWORK.NOT_LOCAL_ACCOUNT, + { account: "0x000006d4548a3ac17d72b372ae1e416bf65b8ead" }, + ); + }); + + it("should not modify the json rpc request for other methods", async () => { + const input = [1, 2]; + const originalJsonRpcRequest = getJsonRpcRequest(1, "eth_sarasa", input); + + const jsonRpcRequest = { ...originalJsonRpcRequest }; + + await localAccountsHandler.handle(jsonRpcRequest); + + assert.deepEqual(jsonRpcRequest, originalJsonRpcRequest); + }); + + it("should not modify the json rpc request if no address is given", async () => { + const originalJsonRpcRequest = getJsonRpcRequest(1, "eth_sign"); + + const jsonRpcRequest = { ...originalJsonRpcRequest }; + + await localAccountsHandler.handle(jsonRpcRequest); + + assert.deepEqual(jsonRpcRequest, originalJsonRpcRequest); + }); + + it("should not modify the json rpc request if the address isn't one of the local ones", async () => { + const originalJsonRpcRequest = getJsonRpcRequest( + 1, + "eth_signTypedData_v4", + ["0x000006d4548a3ac17d72b372ae1e416bf65b8ead", {}], + ); + + const jsonRpcRequest = { ...originalJsonRpcRequest }; + + await localAccountsHandler.handle(jsonRpcRequest); + + assert.deepEqual(jsonRpcRequest, originalJsonRpcRequest); + }); + }); +}); From 7d5f6ebee4e8252068acc02930c996e811c44bf5 Mon Sep 17 00:00:00 2001 From: ChrisD <18092467+ChristopherDedominici@users.noreply.github.com> Date: Thu, 28 Nov 2024 15:54:43 +0100 Subject: [PATCH 08/17] add logic to create handler-array --- .../request-handlers/hanlders-array.ts | 111 ++++++++++++++++-- 1 file changed, 103 insertions(+), 8 deletions(-) diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/hanlders-array.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/hanlders-array.ts index 07c20a4380..6451f0d143 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/hanlders-array.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/hanlders-array.ts @@ -1,29 +1,118 @@ import type { RequestHandler } from "./types.js"; +import type { + HDAccountsUserConfig, + HttpNetworkAccountsUserConfig, +} from "../../../../types/config.js"; import type { ChainType, NetworkConnection, } from "../../../../types/network.js"; -import { ChainIdValidatorHandler } from "./handlers/chain-id/handler.js"; +import { numberToHexString } from "@ignored/hardhat-vnext-utils/hex"; + +import { AutomaticSenderHandler } from "./handlers/accounts/automatic-sender-handler.js"; +import { FixedSenderHandler } from "./handlers/accounts/fixed-sender-handler.js"; +import { HDWalletHandler } from "./handlers/accounts/hd-wallet-handler.js"; +import { LocalAccountsHandler } from "./handlers/accounts/local-accounts.js"; +import { ChainIdValidatorHandler } from "./handlers/chain-id/chain-id-handler.js"; +import { AutomaticGasHandler } from "./handlers/gas/automatic-gas-handler.js"; +import { AutomaticGasPriceHandler } from "./handlers/gas/automatic-gas-price-handler.js"; +import { FixedGasHandler } from "./handlers/gas/fixed-gas-handler.js"; +import { FixedGasPriceHandler } from "./handlers/gas/fixed-gas-price-handler.js"; // TODO: finish docs /** * * This function returns an handlers array based on the values in the networkConfig and.... - * The order of the handlers, if all are present, is: chain handler, gas handler and accounts handler. + * The order of the handlers, if all are present, is: chain handler, gas handlers (gasPrice first, then gas), sender handler and accounts handler. * The order is important to get a correct result when the handler are executed */ -export function createHandlersArray( - networkConnection: NetworkConnection, -): RequestHandler[] { +export async function createHandlersArray< + ChainTypeT extends ChainType | string, +>(networkConnection: NetworkConnection): Promise { const requestHandlers = []; - if (networkConnection.networkConfig.type === "http") { - if (networkConnection.networkConfig.chainId !== undefined) { + const networkConfig = networkConnection.networkConfig; + + if (networkConfig.type === "http") { + if (networkConfig.chainId !== undefined) { requestHandlers.push( new ChainIdValidatorHandler( networkConnection.provider, - networkConnection.networkConfig.chainId, + networkConfig.chainId, + ), + ); + } + } + + if ( + networkConfig.gasPrice === undefined || + networkConfig.gasPrice === "auto" + ) { + // If you use a hook handler that signs locally, you are required to + // have all the transaction fields available, including the + // gasPrice / maxFeePerGas & maxPriorityFeePerGas. + // + // We never use those when using EDR Network, as we sign within the + // EDR Network itself. This means that we don't need to provide all the + // fields, as the missing ones will be resolved there. + // + // EDR Network handles this in a more performant way, so we don't use + // the AutomaticGasPrice for it. + if (networkConfig.type === "http") { + requestHandlers.push( + new AutomaticGasPriceHandler(networkConnection.provider), + ); + } + } else { + requestHandlers.push( + new FixedGasPriceHandler(numberToHexString(networkConfig.gasPrice)), + ); + } + + if (networkConfig.gas === undefined || networkConfig.gas === "auto") { + requestHandlers.push( + new AutomaticGasHandler( + networkConnection.provider, + networkConfig.gasMultiplier, + ), + ); + } else { + requestHandlers.push( + new FixedGasHandler(numberToHexString(networkConfig.gas)), + ); + } + + if (networkConfig.from === undefined) { + requestHandlers.push( + new AutomaticSenderHandler(networkConnection.provider), + ); + } else { + requestHandlers.push( + new FixedSenderHandler(networkConnection.provider, networkConfig.from), + ); + } + + if (networkConfig.type === "http") { + const accounts = networkConfig.accounts; + + if (Array.isArray(accounts)) { + const resolvedAccounts = await Promise.all( + accounts.map((acc) => acc.getHexString()), + ); + + requestHandlers.push( + new LocalAccountsHandler(networkConnection.provider, resolvedAccounts), + ); + } else if (isHDAccountsConfig(accounts)) { + requestHandlers.push( + new HDWalletHandler( + networkConnection.provider, + accounts.mnemonic, + accounts.path, + accounts.initialIndex, + accounts.count, + accounts.passphrase, ), ); } @@ -31,3 +120,9 @@ export function createHandlersArray( return requestHandlers; } + +function isHDAccountsConfig( + accounts?: HttpNetworkAccountsUserConfig, +): accounts is HDAccountsUserConfig { + return accounts !== undefined && Object.keys(accounts).includes("mnemonic"); +} From ccdff2a380ea6edb59863916dcf9913c08421ba4 Mon Sep 17 00:00:00 2001 From: ChrisD <18092467+ChristopherDedominici@users.noreply.github.com> Date: Thu, 28 Nov 2024 16:18:16 +0100 Subject: [PATCH 09/17] revert change in local-account-handler --- .../handlers/accounts/local-accounts.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/local-accounts.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/local-accounts.ts index 20c2e698ce..3c210d708e 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/local-accounts.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/local-accounts.ts @@ -276,10 +276,14 @@ export class LocalAccountsHandler extends ChainId implements RequestHandler { }; }); + // TODO: Fix after the alpha release + assertHardhatInvariant( + txData.to !== undefined, + "The alpha version doesn't support deploying contracts with local accounts yet", + ); + const checksummedAddress = addr.addChecksum( - txData.to !== undefined - ? bytesToHexString(txData.to).toLowerCase() - : "0x0", + bytesToHexString(txData.to).toLowerCase(), ); assertHardhatInvariant( From 64634a30fe84551a54ad7dedd44b3a8b33eaa7a5 Mon Sep 17 00:00:00 2001 From: ChrisD <18092467+ChristopherDedominici@users.noreply.github.com> Date: Thu, 28 Nov 2024 16:23:41 +0100 Subject: [PATCH 10/17] modify chain-id handlers --- .../handlers/chain-id/chain-id-handler.ts | 8 ++++---- .../request-handlers/handlers/chain-id/chain-id.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/chain-id/chain-id-handler.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/chain-id/chain-id-handler.ts index 6db2351324..7b99600922 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/chain-id/chain-id-handler.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/chain-id/chain-id-handler.ts @@ -15,13 +15,13 @@ import { ChainId } from "./chain-id.js"; * HardhatError to signal a network configuration mismatch. Once validated, further checks * are skipped to avoid redundant validations. */ -export class ChainIdValidatorHandler implements RequestHandler { - readonly #chainId: ChainId; +export class ChainIdValidatorHandler extends ChainId implements RequestHandler { readonly #expectedChainId: number; #alreadyValidated = false; constructor(provider: EthereumProvider, expectedChainId: number) { - this.#chainId = new ChainId(provider); + super(provider); + this.#expectedChainId = expectedChainId; } @@ -39,7 +39,7 @@ export class ChainIdValidatorHandler implements RequestHandler { return jsonRpcRequest; } - const actualChainId = await this.#chainId.getChainId(); + const actualChainId = await this.getChainId(); if (actualChainId !== this.#expectedChainId) { throw new HardhatError( diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/chain-id/chain-id.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/chain-id/chain-id.ts index 0998cf6d10..38eaf89e69 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/chain-id/chain-id.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/handlers/chain-id/chain-id.ts @@ -21,7 +21,7 @@ export class ChainId { this.provider = provider; } - public async getChainId(): Promise { + protected async getChainId(): Promise { if (this.#chainId === undefined) { try { this.#chainId = await this.#getChainIdFromEthChainId(); From ce212d42c0ef339ca60bfc0b4bde8300a355519a Mon Sep 17 00:00:00 2001 From: ChrisD <18092467+ChristopherDedominici@users.noreply.github.com> Date: Thu, 28 Nov 2024 16:28:17 +0100 Subject: [PATCH 11/17] update docs for the function createHandlersArray --- .../network-manager/request-handlers/hanlders-array.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/hanlders-array.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/hanlders-array.ts index 6451f0d143..4a5f5ea0d4 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/hanlders-array.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/hanlders-array.ts @@ -20,12 +20,10 @@ import { AutomaticGasPriceHandler } from "./handlers/gas/automatic-gas-price-han import { FixedGasHandler } from "./handlers/gas/fixed-gas-handler.js"; import { FixedGasPriceHandler } from "./handlers/gas/fixed-gas-price-handler.js"; -// TODO: finish docs /** - * - * This function returns an handlers array based on the values in the networkConfig and.... + * This function returns an handlers array based on the values in the NetworkConnection and NetworkConfig. * The order of the handlers, if all are present, is: chain handler, gas handlers (gasPrice first, then gas), sender handler and accounts handler. - * The order is important to get a correct result when the handler are executed + * The order is important to get a correct result when the handlers are executed. */ export async function createHandlersArray< ChainTypeT extends ChainType | string, From df725ab4fef0dd586ab8fdc9b4028722595e222d Mon Sep 17 00:00:00 2001 From: ChrisD <18092467+ChristopherDedominici@users.noreply.github.com> Date: Thu, 28 Nov 2024 16:35:14 +0100 Subject: [PATCH 12/17] improve if condition in createHandlersArray --- .../request-handlers/hanlders-array.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/hanlders-array.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/hanlders-array.ts index 4a5f5ea0d4..6ad80b4eb9 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/hanlders-array.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/hanlders-array.ts @@ -32,15 +32,13 @@ export async function createHandlersArray< const networkConfig = networkConnection.networkConfig; - if (networkConfig.type === "http") { - if (networkConfig.chainId !== undefined) { - requestHandlers.push( - new ChainIdValidatorHandler( - networkConnection.provider, - networkConfig.chainId, - ), - ); - } + if (networkConfig.type === "http" && networkConfig.chainId !== undefined) { + requestHandlers.push( + new ChainIdValidatorHandler( + networkConnection.provider, + networkConfig.chainId, + ), + ); } if ( From 647044d0cd7c44d6d5c757fbcaaebcd462e7ff80 Mon Sep 17 00:00:00 2001 From: ChrisD <18092467+ChristopherDedominici@users.noreply.github.com> Date: Thu, 28 Nov 2024 16:49:01 +0100 Subject: [PATCH 13/17] remove "assert.rejects" --- .../request-handlers/handlers/accounts/local-accounts.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/local-accounts.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/local-accounts.ts index 2dc4f3600e..8d1b10fb0b 100644 --- a/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/local-accounts.ts +++ b/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/local-accounts.ts @@ -6,7 +6,10 @@ import { hexStringToBytes, numberToHexString, } from "@ignored/hardhat-vnext-utils/hex"; -import { assertRejectsWithHardhatError } from "@nomicfoundation/hardhat-test-utils"; +import { + assertRejects, + assertRejectsWithHardhatError, +} from "@nomicfoundation/hardhat-test-utils"; import { addr } from "micro-eth-signer"; import { getJsonRpcRequest } from "../../../../../../../src/internal/builtin-plugins/network-manager/json-rpc.js"; @@ -216,8 +219,7 @@ describe("LocalAccountsHandler", () => { addr.fromPrivateKey(accounts[0]), ]); - // eslint-disable-next-line no-restricted-syntax -- not a Hardhat error - await assert.rejects(localAccountsHandler.handle(jsonRpcRequest)); + assertRejects(localAccountsHandler.handle(jsonRpcRequest)); }); it("should throw if the address isn't one of the local ones", async () => { From 2fa55f6cc21d79ec78f11dca321ff12057613355 Mon Sep 17 00:00:00 2001 From: ChrisD <18092467+ChristopherDedominici@users.noreply.github.com> Date: Fri, 6 Dec 2024 12:00:15 +0100 Subject: [PATCH 14/17] removed "if http condition" for the AutomaticGasPriceHandler --- .../network-manager/request-handlers/hanlders-array.ts | 8 +++----- .../network-manager/request-handlers/e2e.ts | 8 -------- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/hanlders-array.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/hanlders-array.ts index 6ad80b4eb9..8645b7cd1f 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/hanlders-array.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/request-handlers/hanlders-array.ts @@ -55,11 +55,9 @@ export async function createHandlersArray< // // EDR Network handles this in a more performant way, so we don't use // the AutomaticGasPrice for it. - if (networkConfig.type === "http") { - requestHandlers.push( - new AutomaticGasPriceHandler(networkConnection.provider), - ); - } + requestHandlers.push( + new AutomaticGasPriceHandler(networkConnection.provider), + ); } else { requestHandlers.push( new FixedGasPriceHandler(numberToHexString(networkConfig.gasPrice)), diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/e2e.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/e2e.ts index 5d4efc81c5..0d27798901 100644 --- a/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/e2e.ts +++ b/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/e2e.ts @@ -41,8 +41,6 @@ describe("request-handlers - e2e", () => { }, ); - // Use the localhost network for these tests because the modifier is only - // applicable to HTTP networks. EDR networks do not require this modifier. const connection = await hre.network.connect("localhost"); const res = await connection.provider.request({ @@ -104,8 +102,6 @@ describe("request-handlers - e2e", () => { }, ); - // Use the localhost network for these tests because the modifier is only - // applicable to HTTP networks. EDR networks do not require this modifier. const connection = await hre.network.connect("localhost"); const res = await connection.provider.request({ @@ -177,8 +173,6 @@ describe("request-handlers - e2e", () => { }, ); - // Use the localhost network for these tests because the modifier is only - // applicable to HTTP networks. EDR networks do not require this modifier. const connection = await hre.network.connect("localhost"); const tx = { @@ -247,8 +241,6 @@ describe("request-handlers - e2e", () => { }, ); - // Use the localhost network for these tests because the modifier is only - // applicable to HTTP networks. EDR networks do not require this modifier. const connection = await hre.network.connect("localhost"); const tx = { From 71d8935de87bebcd7b06c7d6ec70a25b517dc415 Mon Sep 17 00:00:00 2001 From: ChrisD <18092467+ChristopherDedominici@users.noreply.github.com> Date: Fri, 6 Dec 2024 12:06:55 +0100 Subject: [PATCH 15/17] remove unused "maxFeePerGas" in e2e test --- .../builtin-plugins/network-manager/request-handlers/e2e.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/e2e.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/e2e.ts index 0d27798901..d401462fe4 100644 --- a/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/e2e.ts +++ b/v-next/hardhat/test/internal/builtin-plugins/network-manager/request-handlers/e2e.ts @@ -109,7 +109,6 @@ describe("request-handlers - e2e", () => { params: [ { to: "0x0000000000000000000000000000000000000012", - maxFeePerGas: "0x99", }, ], }); @@ -123,7 +122,6 @@ describe("request-handlers - e2e", () => { ); // gas price assert.equal(res[0].maxPriorityFeePerGas, "0x4"); - assert.equal(res[0].maxFeePerGas, "0x99"); // sender assert.equal(res[0].from, "0x123006d4548a3ac17d72b372ae1e416bf65b8eaf"); }); From 406593b1f4d51e84e24c17b869a77318dc27327a Mon Sep 17 00:00:00 2001 From: Christopher Dedominici <18092467+ChristopherDedominici@users.noreply.github.com> Date: Fri, 6 Dec 2024 12:07:31 +0100 Subject: [PATCH 16/17] Update v-next/hardhat/src/internal/builtin-plugins/network-manager/hook-handlers/network.ts Co-authored-by: Patricio Palladino --- .../builtin-plugins/network-manager/hook-handlers/network.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/hook-handlers/network.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/hook-handlers/network.ts index 7e76795a94..ae11dda538 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/hook-handlers/network.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/hook-handlers/network.ts @@ -23,7 +23,7 @@ export default async (): Promise> => { // and associate it with the corresponding handlers array. // When a connection is closed, its associated handlers array is removed from the map. // See the "closeConnection" function at the end of the file for more details. - const requestHandlersPerConnection: Map = new Map(); + const requestHandlersPerConnection: WeakMap = new Map(); const handlers: Partial = { async onRequest( From 64ed26493777e4c8bbae73acff5110e919bc4440 Mon Sep 17 00:00:00 2001 From: ChrisD <18092467+ChristopherDedominici@users.noreply.github.com> Date: Fri, 6 Dec 2024 12:26:47 +0100 Subject: [PATCH 17/17] fix WeakMap errors --- .../network-manager/hook-handlers/network.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/hook-handlers/network.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/hook-handlers/network.ts index ae11dda538..d8aaaa590c 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/hook-handlers/network.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/hook-handlers/network.ts @@ -23,7 +23,10 @@ export default async (): Promise> => { // and associate it with the corresponding handlers array. // When a connection is closed, its associated handlers array is removed from the map. // See the "closeConnection" function at the end of the file for more details. - const requestHandlersPerConnection: WeakMap = new Map(); + const requestHandlersPerConnection: WeakMap< + NetworkConnection, + RequestHandler[] + > = new Map(); const handlers: Partial = { async onRequest( @@ -36,13 +39,11 @@ export default async (): Promise> => { nextJsonRpcRequest: JsonRpcRequest, ) => Promise, ) { - let requestHandlers = requestHandlersPerConnection.get( - networkConnection.id, - ); + let requestHandlers = requestHandlersPerConnection.get(networkConnection); if (requestHandlers === undefined) { requestHandlers = await createHandlersArray(networkConnection); - requestHandlersPerConnection.set(networkConnection.id, requestHandlers); + requestHandlersPerConnection.set(networkConnection, requestHandlers); } // We clone the request to avoid interfering with other hook handlers that @@ -70,8 +71,8 @@ export default async (): Promise> => { nextNetworkConnection: NetworkConnection, ) => Promise, ): Promise { - if (requestHandlersPerConnection.has(networkConnection.id) === true) { - requestHandlersPerConnection.delete(networkConnection.id); + if (requestHandlersPerConnection.has(networkConnection) === true) { + requestHandlersPerConnection.delete(networkConnection); } return next(context, networkConnection);