diff --git a/config/eslint/eslintrc.js b/config/eslint/eslintrc.js index 0d563e5981..f33ff32f2c 100644 --- a/config/eslint/eslintrc.js +++ b/config/eslint/eslintrc.js @@ -191,7 +191,7 @@ module.exports = { "no-cond-assign": "error", "no-debugger": "error", "no-duplicate-case": "error", - "no-duplicate-imports": "error", + "@typescript-eslint/no-duplicate-imports": "error", "no-eval": "error", "no-extra-bind": "error", "no-new-func": "error", diff --git a/packages/hardhat-chai-matchers/.eslintrc.js b/packages/hardhat-chai-matchers/.eslintrc.js index 44ed8ed6d5..7f3a838a47 100644 --- a/packages/hardhat-chai-matchers/.eslintrc.js +++ b/packages/hardhat-chai-matchers/.eslintrc.js @@ -4,4 +4,7 @@ module.exports = { project: `${__dirname}/src/tsconfig.json`, sourceType: "module", }, + rules: { + "@typescript-eslint/no-non-null-assertion": "error" + } }; diff --git a/packages/hardhat-chai-matchers/package.json b/packages/hardhat-chai-matchers/package.json index a3732936d3..6427bd7ddc 100644 --- a/packages/hardhat-chai-matchers/package.json +++ b/packages/hardhat-chai-matchers/package.json @@ -1,6 +1,6 @@ { "name": "@nomicfoundation/hardhat-chai-matchers", - "version": "1.0.6", + "version": "2.0.0", "description": "Hardhat utils for testing", "homepage": "https://github.com/nomicfoundation/hardhat/tree/main/packages/hardhat-chai-matchers", "repository": "github:nomicfoundation/hardhat", @@ -19,7 +19,7 @@ "lint:fix": "yarn prettier --write && yarn eslint --fix", "eslint": "eslint 'src/**/*.ts' 'test/**/*.ts'", "prettier": "prettier \"**/*.{js,md,json}\"", - "test": "mocha --recursive \"test/**/*.ts\" --exit --reporter dot", + "test": "mocha --recursive \"test/**/*.ts\" --exit", "test:ci": "yarn test && node scripts/check-subpath-exports.js", "build": "tsc --build .", "prepublishOnly": "yarn build", @@ -37,7 +37,7 @@ "README.md" ], "devDependencies": { - "@nomiclabs/hardhat-ethers": "^2.0.0", + "@nomiclabs/hardhat-ethers": "^3.0.0", "@types/bn.js": "^5.1.0", "@types/chai": "^4.2.0", "@types/mocha": ">=9.1.0", @@ -52,7 +52,7 @@ "eslint-plugin-import": "2.24.1", "eslint-plugin-no-only-tests": "3.0.0", "eslint-plugin-prettier": "3.4.0", - "ethers": "^5.0.0", + "ethers": "^6.1.0", "get-port": "^5.1.1", "hardhat": "^2.9.4", "mocha": "^10.0.0", @@ -62,9 +62,9 @@ "typescript": "~4.7.4" }, "peerDependencies": { - "@nomiclabs/hardhat-ethers": "^2.0.0", + "@nomiclabs/hardhat-ethers": "^3.0.0", "chai": "^4.2.0", - "ethers": "^5.0.0", + "ethers": "^6.1.0", "hardhat": "^2.9.4" }, "dependencies": { diff --git a/packages/hardhat-chai-matchers/src/internal/changeEtherBalance.ts b/packages/hardhat-chai-matchers/src/internal/changeEtherBalance.ts index 50def56b7f..e0383d29ec 100644 --- a/packages/hardhat-chai-matchers/src/internal/changeEtherBalance.ts +++ b/packages/hardhat-chai-matchers/src/internal/changeEtherBalance.ts @@ -1,33 +1,40 @@ -import type { BigNumberish, providers } from "ethers"; +import type { + Addressable, + BigNumberish, + TransactionResponse, + default as EthersT, +} from "ethers"; import { buildAssert } from "../utils"; import { ensure } from "./calledOnContract/utils"; -import { Account, getAddressOf } from "./misc/account"; +import { getAddressOf } from "./misc/account"; import { BalanceChangeOptions } from "./misc/balance"; +import { assertIsNotNull } from "./utils"; export function supportChangeEtherBalance(Assertion: Chai.AssertionStatic) { Assertion.addMethod( "changeEtherBalance", function ( this: any, - account: Account | string, + account: Addressable | string, balanceChange: BigNumberish, options?: BalanceChangeOptions ) { - const { BigNumber } = require("ethers"); - + const { toBigInt } = require("ethers") as typeof EthersT; // capture negated flag before async code executes; see buildAssert's jsdoc const negated = this.__flags.negate; const subject = this._obj; const checkBalanceChange = ([actualChange, address]: [ - typeof BigNumber, + bigint, string ]) => { const assert = buildAssert(negated, checkBalanceChange); + const expectedChange = toBigInt(balanceChange); + assert( - actualChange.eq(BigNumber.from(balanceChange)), + actualChange === expectedChange, `Expected the ether balance of "${address}" to change by ${balanceChange.toString()} wei, but it changed by ${actualChange.toString()} wei`, `Expected the ether balance of "${address}" NOT to change by ${balanceChange.toString()} wei, but it did` ); @@ -47,19 +54,16 @@ export function supportChangeEtherBalance(Assertion: Chai.AssertionStatic) { export async function getBalanceChange( transaction: - | providers.TransactionResponse - | Promise - | (() => - | Promise - | providers.TransactionResponse), - account: Account | string, + | TransactionResponse + | Promise + | (() => Promise | TransactionResponse), + account: Addressable | string, options?: BalanceChangeOptions -) { - const { BigNumber } = await import("ethers"); +): Promise { const hre = await import("hardhat"); const provider = hre.network.provider; - let txResponse: providers.TransactionResponse; + let txResponse: TransactionResponse; if (typeof transaction === "function") { txResponse = await transaction(); @@ -68,6 +72,7 @@ export async function getBalanceChange( } const txReceipt = await txResponse.wait(); + assertIsNotNull(txReceipt, "txReceipt"); const txBlockNumber = txReceipt.blockNumber; const block = await provider.send("eth_getBlockByHash", [ @@ -83,23 +88,26 @@ export async function getBalanceChange( const address = await getAddressOf(account); - const balanceAfter = await provider.send("eth_getBalance", [ + const balanceAfterHex = await provider.send("eth_getBalance", [ address, `0x${txBlockNumber.toString(16)}`, ]); - const balanceBefore = await provider.send("eth_getBalance", [ + const balanceBeforeHex = await provider.send("eth_getBalance", [ address, `0x${(txBlockNumber - 1).toString(16)}`, ]); + const balanceAfter = BigInt(balanceAfterHex); + const balanceBefore = BigInt(balanceBeforeHex); + if (options?.includeFee !== true && address === txResponse.from) { - const gasPrice = txReceipt.effectiveGasPrice ?? txResponse.gasPrice; + const gasPrice = txReceipt.gasPrice; const gasUsed = txReceipt.gasUsed; - const txFee = gasPrice.mul(gasUsed); + const txFee = gasPrice * gasUsed; - return BigNumber.from(balanceAfter).add(txFee).sub(balanceBefore); + return balanceAfter + txFee - balanceBefore; } else { - return BigNumber.from(balanceAfter).sub(balanceBefore); + return balanceAfter - balanceBefore; } } diff --git a/packages/hardhat-chai-matchers/src/internal/changeEtherBalances.ts b/packages/hardhat-chai-matchers/src/internal/changeEtherBalances.ts index e2be028cfd..2c972591f5 100644 --- a/packages/hardhat-chai-matchers/src/internal/changeEtherBalances.ts +++ b/packages/hardhat-chai-matchers/src/internal/changeEtherBalances.ts @@ -1,25 +1,26 @@ -import type { BigNumber, BigNumberish, providers } from "ethers"; +import type EthersT from "ethers"; +import type { Addressable, BigNumberish, TransactionResponse } from "ethers"; import ordinal from "ordinal"; import { buildAssert } from "../utils"; -import { getAddressOf, Account } from "./misc/account"; +import { getAddressOf } from "./misc/account"; import { BalanceChangeOptions, getAddresses, getBalances, } from "./misc/balance"; +import { assertIsNotNull } from "./utils"; export function supportChangeEtherBalances(Assertion: Chai.AssertionStatic) { Assertion.addMethod( "changeEtherBalances", function ( this: any, - accounts: Array, + accounts: Array, balanceChanges: BigNumberish[], options?: BalanceChangeOptions ) { - const { BigNumber } = require("ethers"); - + const { toBigInt } = require("ethers") as typeof EthersT; // capture negated flag before async code executes; see buildAssert's jsdoc const negated = this.__flags.negate; @@ -29,19 +30,19 @@ export function supportChangeEtherBalances(Assertion: Chai.AssertionStatic) { } const checkBalanceChanges = ([actualChanges, accountAddresses]: [ - Array, + bigint[], string[] ]) => { const assert = buildAssert(negated, checkBalanceChanges); assert( - actualChanges.every((change, ind) => - change.eq(BigNumber.from(balanceChanges[ind])) + actualChanges.every( + (change, ind) => change === toBigInt(balanceChanges[ind]) ), () => { const lines: string[] = []; - actualChanges.forEach((change: BigNumber, i) => { - if (!change.eq(BigNumber.from(balanceChanges[i]))) { + actualChanges.forEach((change: bigint, i) => { + if (change !== toBigInt(balanceChanges[i])) { lines.push( `Expected the ether balance of ${ accountAddresses[i] @@ -57,8 +58,8 @@ export function supportChangeEtherBalances(Assertion: Chai.AssertionStatic) { }, () => { const lines: string[] = []; - actualChanges.forEach((change: BigNumber, i) => { - if (change.eq(BigNumber.from(balanceChanges[i]))) { + actualChanges.forEach((change: bigint, i) => { + if (change === toBigInt(balanceChanges[i])) { lines.push( `Expected the ether balance of ${ accountAddresses[i] @@ -88,15 +89,14 @@ export function supportChangeEtherBalances(Assertion: Chai.AssertionStatic) { } export async function getBalanceChanges( - transaction: - | providers.TransactionResponse - | Promise, - accounts: Array, + transaction: TransactionResponse | Promise, + accounts: Array, options?: BalanceChangeOptions -) { +): Promise { const txResponse = await transaction; const txReceipt = await txResponse.wait(); + assertIsNotNull(txReceipt, "txReceipt"); const txBlockNumber = txReceipt.blockNumber; const balancesAfter = await getBalances(accounts, txBlockNumber); @@ -104,16 +104,16 @@ export async function getBalanceChanges( const txFees = await getTxFees(accounts, txResponse, options); - return balancesAfter.map((balance, ind) => - balance.add(txFees[ind]).sub(balancesBefore[ind]) + return balancesAfter.map( + (balance, ind) => balance + txFees[ind] - balancesBefore[ind] ); } async function getTxFees( - accounts: Array, - txResponse: providers.TransactionResponse, + accounts: Array, + txResponse: TransactionResponse, options?: BalanceChangeOptions -) { +): Promise { return Promise.all( accounts.map(async (account) => { if ( @@ -121,14 +121,15 @@ async function getTxFees( (await getAddressOf(account)) === txResponse.from ) { const txReceipt = await txResponse.wait(); - const gasPrice = txReceipt.effectiveGasPrice ?? txResponse.gasPrice; + assertIsNotNull(txReceipt, "txReceipt"); + const gasPrice = txReceipt.gasPrice ?? txResponse.gasPrice; const gasUsed = txReceipt.gasUsed; - const txFee = gasPrice.mul(gasUsed); + const txFee = gasPrice * gasUsed; return txFee; } - return 0; + return 0n; }) ); } diff --git a/packages/hardhat-chai-matchers/src/internal/changeTokenBalance.ts b/packages/hardhat-chai-matchers/src/internal/changeTokenBalance.ts index 7def041206..17a006ffba 100644 --- a/packages/hardhat-chai-matchers/src/internal/changeTokenBalance.ts +++ b/packages/hardhat-chai-matchers/src/internal/changeTokenBalance.ts @@ -1,14 +1,29 @@ import type EthersT from "ethers"; +import type { + Addressable, + BaseContract, + BaseContractMethod, + BigNumberish, + ContractTransactionResponse, +} from "ethers"; import { buildAssert } from "../utils"; import { ensure } from "./calledOnContract/utils"; -import { Account, getAddressOf } from "./misc/account"; - -type TransactionResponse = EthersT.providers.TransactionResponse; - -interface Token extends EthersT.Contract { - balanceOf(address: string, overrides?: any): Promise; -} +import { getAddressOf } from "./misc/account"; +import { assertIsNotNull } from "./utils"; + +type TransactionResponse = EthersT.TransactionResponse; + +export type Token = BaseContract & { + balanceOf: BaseContractMethod<[string], bigint, bigint>; + name: BaseContractMethod<[], string, string>; + transfer: BaseContractMethod< + [string, BigNumberish], + boolean, + ContractTransactionResponse + >; + symbol: BaseContractMethod<[], string, string>; +}; export function supportChangeTokenBalance(Assertion: Chai.AssertionStatic) { Assertion.addMethod( @@ -16,7 +31,7 @@ export function supportChangeTokenBalance(Assertion: Chai.AssertionStatic) { function ( this: any, token: Token, - account: Account | string, + account: Addressable | string, balanceChange: EthersT.BigNumberish ) { const ethers = require("ethers") as typeof EthersT; @@ -32,14 +47,14 @@ export function supportChangeTokenBalance(Assertion: Chai.AssertionStatic) { checkToken(token, "changeTokenBalance"); const checkBalanceChange = ([actualChange, address, tokenDescription]: [ - EthersT.BigNumber, + bigint, string, string ]) => { const assert = buildAssert(negated, checkBalanceChange); assert( - actualChange.eq(ethers.BigNumber.from(balanceChange)), + actualChange === ethers.toBigInt(balanceChange), `Expected the balance of ${tokenDescription} tokens for "${address}" to change by ${balanceChange.toString()}, but it changed by ${actualChange.toString()}`, `Expected the balance of ${tokenDescription} tokens for "${address}" NOT to change by ${balanceChange.toString()}, but it did` ); @@ -63,7 +78,7 @@ export function supportChangeTokenBalance(Assertion: Chai.AssertionStatic) { function ( this: any, token: Token, - accounts: Array, + accounts: Array, balanceChanges: EthersT.BigNumberish[] ) { const ethers = require("ethers") as typeof EthersT; @@ -76,13 +91,7 @@ export function supportChangeTokenBalance(Assertion: Chai.AssertionStatic) { subject = subject(); } - checkToken(token, "changeTokenBalances"); - - if (accounts.length !== balanceChanges.length) { - throw new Error( - `The number of accounts (${accounts.length}) is different than the number of expected balance changes (${balanceChanges.length})` - ); - } + validateInput(this._obj, token, accounts, balanceChanges); const balanceChangesPromise = Promise.all( accounts.map((account) => getBalanceChange(subject, token, account)) @@ -93,12 +102,12 @@ export function supportChangeTokenBalance(Assertion: Chai.AssertionStatic) { actualChanges, addresses, tokenDescription, - ]: [EthersT.BigNumber[], string[], string]) => { + ]: [bigint[], string[], string]) => { const assert = buildAssert(negated, checkBalanceChanges); assert( - actualChanges.every((change, ind) => - change.eq(ethers.BigNumber.from(balanceChanges[ind])) + actualChanges.every( + (change, ind) => change === ethers.toBigInt(balanceChanges[ind]) ), `Expected the balances of ${tokenDescription} tokens for ${ addresses as any @@ -127,12 +136,34 @@ export function supportChangeTokenBalance(Assertion: Chai.AssertionStatic) { ); } +function validateInput( + obj: any, + token: Token, + accounts: Array, + balanceChanges: EthersT.BigNumberish[] +) { + try { + checkToken(token, "changeTokenBalances"); + + if (accounts.length !== balanceChanges.length) { + throw new Error( + `The number of accounts (${accounts.length}) is different than the number of expected balance changes (${balanceChanges.length})` + ); + } + } catch (e) { + // if the input validation fails, we discard the subject since it could + // potentially be a rejected promise + Promise.resolve(obj).catch(() => {}); + throw e; + } +} + function checkToken(token: unknown, method: string) { - if (typeof token !== "object" || token === null || !("functions" in token)) { + if (typeof token !== "object" || token === null || !("interface" in token)) { throw new Error( `The first argument of ${method} must be the contract instance of the token` ); - } else if ((token as any).functions.balanceOf === undefined) { + } else if ((token as any).interface.getFunction("balanceOf") === null) { throw new Error("The given contract instance is not an ERC20 token"); } } @@ -140,7 +171,7 @@ function checkToken(token: unknown, method: string) { export async function getBalanceChange( transaction: TransactionResponse | Promise, token: Token, - account: Account | string + account: Addressable | string ) { const ethers = require("ethers") as typeof EthersT; const hre = await import("hardhat"); @@ -149,6 +180,7 @@ export async function getBalanceChange( const txResponse = await transaction; const txReceipt = await txResponse.wait(); + assertIsNotNull(txReceipt, "txReceipt"); const txBlockNumber = txReceipt.blockNumber; const block = await provider.send("eth_getBlockByHash", [ @@ -172,7 +204,7 @@ export async function getBalanceChange( blockTag: txBlockNumber - 1, }); - return ethers.BigNumber.from(balanceAfter).sub(balanceBefore); + return ethers.toBigInt(balanceAfter) - balanceBefore; } let tokenDescriptionsCache: Record = {}; @@ -182,8 +214,9 @@ let tokenDescriptionsCache: Record = {}; * exist, the address of the token is used. */ async function getTokenDescription(token: Token): Promise { - if (tokenDescriptionsCache[token.address] === undefined) { - let tokenDescription = ``; + const tokenAddress = await token.getAddress(); + if (tokenDescriptionsCache[tokenAddress] === undefined) { + let tokenDescription = ``; try { tokenDescription = await token.symbol(); } catch (e) { @@ -192,10 +225,10 @@ async function getTokenDescription(token: Token): Promise { } catch (e2) {} } - tokenDescriptionsCache[token.address] = tokenDescription; + tokenDescriptionsCache[tokenAddress] = tokenDescription; } - return tokenDescriptionsCache[token.address]; + return tokenDescriptionsCache[tokenAddress]; } // only used by tests diff --git a/packages/hardhat-chai-matchers/src/internal/constants.ts b/packages/hardhat-chai-matchers/src/internal/constants.ts new file mode 100644 index 0000000000..da752e9b4e --- /dev/null +++ b/packages/hardhat-chai-matchers/src/internal/constants.ts @@ -0,0 +1 @@ +export const ASSERTION_ABORTED = "hh-chai-matchers-assertion-aborted"; diff --git a/packages/hardhat-chai-matchers/src/internal/emit.ts b/packages/hardhat-chai-matchers/src/internal/emit.ts index d6ea26791c..3dbd68842b 100644 --- a/packages/hardhat-chai-matchers/src/internal/emit.ts +++ b/packages/hardhat-chai-matchers/src/internal/emit.ts @@ -1,18 +1,17 @@ -import type { - providers, - utils as EthersUtils, - Contract, - Transaction, -} from "ethers"; +import type EthersT from "ethers"; +import type { Contract, Transaction } from "ethers"; import { AssertionError } from "chai"; import util from "util"; import ordinal from "ordinal"; import { AssertWithSsfi, buildAssert, Ssfi } from "../utils"; +import { ASSERTION_ABORTED } from "./constants"; +import { assertIsNotNull } from "./utils"; +import { HardhatChaiMatchersAssertionError } from "./errors"; -type EventFragment = EthersUtils.EventFragment; -type Interface = EthersUtils.Interface; -type Provider = providers.Provider; +type EventFragment = EthersT.EventFragment; +type Interface = EthersT.Interface; +type Provider = EthersT.Provider; export const EMIT_CALLED = "emitAssertionCalled"; @@ -20,7 +19,7 @@ async function waitForPendingTransaction( tx: Promise | Transaction | string, provider: Provider ) { - let hash: string | undefined; + let hash: string | null; if (tx instanceof Promise) { ({ hash } = await tx); } else if (typeof tx === "string") { @@ -28,10 +27,10 @@ async function waitForPendingTransaction( } else { ({ hash } = tx); } - if (hash === undefined) { + if (hash === null) { throw new Error(`${JSON.stringify(tx)} is not a valid transaction`); } - return provider.waitForTransaction(hash); + return provider.getTransactionReceipt(hash); } export function supportEmit( @@ -47,28 +46,39 @@ export function supportEmit( const promise = this.then === undefined ? Promise.resolve() : this; - const onSuccess = (receipt: providers.TransactionReceipt) => { + const onSuccess = (receipt: EthersT.TransactionReceipt) => { + // abort if the assertion chain was aborted, for example because + // a `.not` was combined with a `.withArgs` + if (chaiUtils.flag(this, ASSERTION_ABORTED) === true) { + return; + } + const assert = buildAssert(negated, onSuccess); - let eventFragment: EventFragment | undefined; + let eventFragment: EventFragment | null = null; try { eventFragment = contract.interface.getEvent(eventName); } catch (e) { // ignore error } - if (eventFragment === undefined) { + if (eventFragment === null) { throw new AssertionError( `Event "${eventName}" doesn't exist in the contract` ); } - const topic = contract.interface.getEventTopic(eventFragment); + const topic = eventFragment.topicHash; + const contractAddress = contract.target; + if (typeof contractAddress !== "string") { + throw new HardhatChaiMatchersAssertionError( + `The contract target should be a string` + ); + } this.logs = receipt.logs .filter((log) => log.topics.includes(topic)) .filter( - (log) => - log.address.toLowerCase() === contract.address.toLowerCase() + (log) => log.address.toLowerCase() === contractAddress.toLowerCase() ); assert( @@ -80,9 +90,26 @@ export function supportEmit( chaiUtils.flag(this, "contract", contract); }; - const derivedPromise = promise - .then(() => waitForPendingTransaction(tx, contract.provider)) - .then(onSuccess); + const derivedPromise = promise.then(() => { + // abort if the assertion chain was aborted, for example because + // a `.not` was combined with a `.withArgs` + if (chaiUtils.flag(this, ASSERTION_ABORTED) === true) { + return; + } + + if (contract.runner === null || contract.runner.provider === null) { + throw new HardhatChaiMatchersAssertionError( + "contract.runner.provider shouldn't be null" + ); + } + + return waitForPendingTransaction(tx, contract.runner.provider).then( + (receipt) => { + assertIsNotNull(receipt, "receipt"); + return onSuccess(receipt); + } + ); + }); chaiUtils.flag(this, EMIT_CALLED, true); @@ -124,11 +151,12 @@ function assertArgsArraysEqual( assert: AssertWithSsfi, ssfi: Ssfi ) { - const { utils } = require("ethers") as { utils: typeof EthersUtils }; - - const actualArgs = ( + const ethers = require("ethers") as typeof EthersT; + const parsedLog = ( chaiUtils.flag(context, "contract").interface as Interface - ).parseLog(log).args; + ).parseLog(log); + assertIsNotNull(parsedLog, "parsedLog"); + const actualArgs = parsedLog.args; const eventName = chaiUtils.flag(context, "eventName"); assert( actualArgs.length === expectedArgs.length, @@ -159,7 +187,7 @@ function assertArgsArraysEqual( } } else if (expectedArgs[index] instanceof Uint8Array) { new Assertion(actualArgs[index], undefined, ssfi, true).equal( - utils.hexlify(expectedArgs[index]) + ethers.hexlify(expectedArgs[index]) ); } else if ( expectedArgs[index]?.length !== undefined && @@ -195,10 +223,10 @@ function assertArgsArraysEqual( expectedArgs[index], "The actual value was an indexed and hashed value of the event argument. The expected value provided to the assertion should be the actual event argument (the pre-image of the hash). You provided the hash itself. Please supply the the actual event argument (the pre-image of the hash) instead." ); - const expectedArgBytes = utils.isHexString(expectedArgs[index]) - ? utils.arrayify(expectedArgs[index]) - : utils.toUtf8Bytes(expectedArgs[index]); - const expectedHash = utils.keccak256(expectedArgBytes); + const expectedArgBytes = ethers.isHexString(expectedArgs[index]) + ? ethers.getBytes(expectedArgs[index]) + : ethers.toUtf8Bytes(expectedArgs[index]); + const expectedHash = ethers.keccak256(expectedArgBytes); new Assertion(actualArgs[index].hash, undefined, ssfi, true).to.equal( expectedHash, `The actual value was an indexed and hashed value of the event argument. The expected value provided to the assertion was hashed to produce ${expectedHash}. The actual hash and the expected hash did not match` diff --git a/packages/hardhat-chai-matchers/src/internal/errors.ts b/packages/hardhat-chai-matchers/src/internal/errors.ts index c6f141cb69..87665ce845 100644 --- a/packages/hardhat-chai-matchers/src/internal/errors.ts +++ b/packages/hardhat-chai-matchers/src/internal/errors.ts @@ -1,9 +1,25 @@ -import { CustomError } from "hardhat/common"; +import { NomicLabsHardhatPluginError } from "hardhat/plugins"; -export class HardhatChaiMatchersDecodingError extends CustomError { +export class HardhatChaiMatchersError extends NomicLabsHardhatPluginError { + constructor(message: string, parent?: Error) { + super("@nomicfoundation/hardhat-chai-matchers", message, parent); + } +} + +export class HardhatChaiMatchersDecodingError extends HardhatChaiMatchersError { constructor(encodedData: string, type: string, parent: Error) { const message = `There was an error decoding '${encodedData}' as a ${type}`; super(message, parent); } } + +/** + * This class is used to assert assumptions in our implementation. Chai's + * AssertionError should be used for user assertions. + */ +export class HardhatChaiMatchersAssertionError extends HardhatChaiMatchersError { + constructor(message: string) { + super(`Assertion error: ${message}`); + } +} diff --git a/packages/hardhat-chai-matchers/src/internal/misc/account.ts b/packages/hardhat-chai-matchers/src/internal/misc/account.ts index ca77d3f87b..c4d7e536ce 100644 --- a/packages/hardhat-chai-matchers/src/internal/misc/account.ts +++ b/packages/hardhat-chai-matchers/src/internal/misc/account.ts @@ -1,21 +1,24 @@ -import type { Contract, Signer, Wallet } from "ethers"; +import type { Addressable } from "ethers"; import assert from "assert"; -export type Account = Signer | Contract; +import { HardhatChaiMatchersAssertionError } from "../errors"; -export function isAccount(account: Account): account is Contract | Wallet { - const ethers = require("ethers"); - return account instanceof ethers.Contract || account instanceof ethers.Wallet; -} +export async function getAddressOf( + account: Addressable | string +): Promise { + const { isAddressable } = await import("ethers"); -export async function getAddressOf(account: Account | string) { if (typeof account === "string") { assert(/^0x[0-9a-fA-F]{40}$/.test(account), `Invalid address ${account}`); return account; - } else if (isAccount(account)) { - return account.address; - } else { + } + + if (isAddressable(account)) { return account.getAddress(); } + + throw new HardhatChaiMatchersAssertionError( + `Expected string or addressable, got ${account as any}` + ); } diff --git a/packages/hardhat-chai-matchers/src/internal/misc/balance.ts b/packages/hardhat-chai-matchers/src/internal/misc/balance.ts index b70f6460e3..9bd93f6827 100644 --- a/packages/hardhat-chai-matchers/src/internal/misc/balance.ts +++ b/packages/hardhat-chai-matchers/src/internal/misc/balance.ts @@ -1,18 +1,20 @@ -import { Account, getAddressOf } from "./account"; +import type { Addressable } from "ethers"; + +import { getAddressOf } from "./account"; export interface BalanceChangeOptions { includeFee?: boolean; } -export function getAddresses(accounts: Array) { +export function getAddresses(accounts: Array) { return Promise.all(accounts.map((account) => getAddressOf(account))); } export async function getBalances( - accounts: Array, + accounts: Array, blockNumber?: number -) { - const { BigNumber } = await import("ethers"); +): Promise { + const { toBigInt } = await import("ethers"); const hre = await import("hardhat"); const provider = hre.ethers.provider; @@ -23,7 +25,7 @@ export async function getBalances( address, `0x${blockNumber?.toString(16) ?? 0}`, ]); - return BigNumber.from(result); + return toBigInt(result); }) ); } diff --git a/packages/hardhat-chai-matchers/src/internal/reverted/panic.ts b/packages/hardhat-chai-matchers/src/internal/reverted/panic.ts index e94c5508fd..d64b7074ba 100644 --- a/packages/hardhat-chai-matchers/src/internal/reverted/panic.ts +++ b/packages/hardhat-chai-matchers/src/internal/reverted/panic.ts @@ -1,5 +1,3 @@ -import type { BigNumber } from "ethers"; - export const PANIC_CODES = { ASSERTION_ERROR: 0x1, ARITHMETIC_UNDER_OR_OVERFLOW: 0x11, @@ -13,27 +11,25 @@ export const PANIC_CODES = { }; // copied from hardhat-core -export function panicErrorCodeToReason( - errorCode: BigNumber -): string | undefined { - switch (errorCode.toNumber()) { - case 0x1: +export function panicErrorCodeToReason(errorCode: bigint): string | undefined { + switch (errorCode) { + case 0x1n: return "Assertion error"; - case 0x11: + case 0x11n: return "Arithmetic operation underflowed or overflowed outside of an unchecked block"; - case 0x12: + case 0x12n: return "Division or modulo division by zero"; - case 0x21: + case 0x21n: return "Tried to convert a value into an enum, but the value was too big or negative"; - case 0x22: + case 0x22n: return "Incorrectly encoded storage byte array"; - case 0x31: + case 0x31n: return ".pop() was called on an empty array"; - case 0x32: + case 0x32n: return "Array accessed at an out-of-bounds or negative index"; - case 0x41: + case 0x41n: return "Too much memory was allocated, or an array was created that is too large"; - case 0x51: + case 0x51n: return "Called a zero-initialized variable of internal function type"; } } diff --git a/packages/hardhat-chai-matchers/src/internal/reverted/reverted.ts b/packages/hardhat-chai-matchers/src/internal/reverted/reverted.ts index 105dcdb445..57ba59340a 100644 --- a/packages/hardhat-chai-matchers/src/internal/reverted/reverted.ts +++ b/packages/hardhat-chai-matchers/src/internal/reverted/reverted.ts @@ -1,4 +1,7 @@ +import type EthersT from "ethers"; + import { buildAssert } from "../../utils"; +import { assertIsNotNull } from "../utils"; import { decodeReturnData, getReturnDataFromError } from "./utils"; export function supportReverted(Assertion: Chai.AssertionStatic) { @@ -27,6 +30,7 @@ export function supportReverted(Assertion: Chai.AssertionStatic) { const receipt = await getTransactionReceipt(hash); + assertIsNotNull(receipt, "receipt"); assert( receipt.status === 0, "Expected transaction to be reverted", @@ -52,6 +56,7 @@ export function supportReverted(Assertion: Chai.AssertionStatic) { }; const onError = (error: any) => { + const { toBeHex } = require("ethers") as typeof EthersT; const assert = buildAssert(negated, onError); const returnData = getReturnDataFromError(error); const decodedReturnData = decodeReturnData(returnData); @@ -73,9 +78,9 @@ export function supportReverted(Assertion: Chai.AssertionStatic) { assert( true, undefined, - `Expected transaction NOT to be reverted, but it reverted with panic code ${decodedReturnData.code.toHexString()} (${ - decodedReturnData.description - })` + `Expected transaction NOT to be reverted, but it reverted with panic code ${toBeHex( + decodedReturnData.code + )} (${decodedReturnData.description})` ); } else { const _exhaustiveCheck: never = decodedReturnData; diff --git a/packages/hardhat-chai-matchers/src/internal/reverted/revertedWith.ts b/packages/hardhat-chai-matchers/src/internal/reverted/revertedWith.ts index 4b8d903405..4bd50ea865 100644 --- a/packages/hardhat-chai-matchers/src/internal/reverted/revertedWith.ts +++ b/packages/hardhat-chai-matchers/src/internal/reverted/revertedWith.ts @@ -1,3 +1,5 @@ +import type EthersT from "ethers"; + import { buildAssert } from "../../utils"; import { decodeReturnData, getReturnDataFromError } from "./utils"; @@ -13,6 +15,9 @@ export function supportRevertedWith(Assertion: Chai.AssertionStatic) { !(expectedReason instanceof RegExp) && typeof expectedReason !== "string" ) { + // if the input validation fails, we discard the subject since it could + // potentially be a rejected promise + Promise.resolve(this._obj).catch(() => {}); throw new TypeError( "Expected the revert reason to be a string or a regular expression" ); @@ -33,6 +38,7 @@ export function supportRevertedWith(Assertion: Chai.AssertionStatic) { }; const onError = (error: any) => { + const { toBeHex } = require("ethers") as typeof EthersT; const assert = buildAssert(negated, onError); const returnData = getReturnDataFromError(error); @@ -57,9 +63,9 @@ export function supportRevertedWith(Assertion: Chai.AssertionStatic) { } else if (decodedReturnData.kind === "Panic") { assert( false, - `Expected transaction to be reverted with reason '${expectedReasonString}', but it reverted with panic code ${decodedReturnData.code.toHexString()} (${ - decodedReturnData.description - })` + `Expected transaction to be reverted with reason '${expectedReasonString}', but it reverted with panic code ${toBeHex( + decodedReturnData.code + )} (${decodedReturnData.description})` ); } else if (decodedReturnData.kind === "Custom") { assert( diff --git a/packages/hardhat-chai-matchers/src/internal/reverted/revertedWithCustomError.ts b/packages/hardhat-chai-matchers/src/internal/reverted/revertedWithCustomError.ts index f5e0ea26fd..afe5c66aa9 100644 --- a/packages/hardhat-chai-matchers/src/internal/reverted/revertedWithCustomError.ts +++ b/packages/hardhat-chai-matchers/src/internal/reverted/revertedWithCustomError.ts @@ -1,15 +1,23 @@ +import type EthersT from "ethers"; + import { AssertionError } from "chai"; import ordinal from "ordinal"; +import { ASSERTION_ABORTED } from "../constants"; +import { assertIsNotNull } from "../utils"; import { buildAssert, Ssfi } from "../../utils"; -import { decodeReturnData, getReturnDataFromError } from "./utils"; +import { + decodeReturnData, + getReturnDataFromError, + resultToArray, +} from "./utils"; export const REVERTED_WITH_CUSTOM_ERROR_CALLED = "customErrorAssertionCalled"; interface CustomErrorAssertionData { - contractInterface: any; + contractInterface: EthersT.Interface; returnData: string; - customError: CustomError; + customError: EthersT.ErrorFragment; } export function supportRevertedWithCustomError( @@ -18,38 +26,25 @@ export function supportRevertedWithCustomError( ) { Assertion.addMethod( "revertedWithCustomError", - function (this: any, contract: any, expectedCustomErrorName: string) { + function ( + this: any, + contract: EthersT.BaseContract, + expectedCustomErrorName: string + ) { // capture negated flag before async code executes; see buildAssert's jsdoc const negated = this.__flags.negate; - // check the case where users forget to pass the contract as the first - // argument - if (typeof contract === "string" || contract?.interface === undefined) { - throw new TypeError( - "The first argument of .revertedWithCustomError must be the contract that defines the custom error" - ); - } - - // validate custom error name - if (typeof expectedCustomErrorName !== "string") { - throw new TypeError("Expected the custom error name to be a string"); - } - - const iface: any = contract.interface; - - const expectedCustomError = findCustomErrorByName( - iface, + const { iface, expectedCustomError } = validateInput( + this._obj, + contract, expectedCustomErrorName ); - // check that interface contains the given custom error - if (expectedCustomError === undefined) { - throw new Error( - `The given contract doesn't have a custom error named '${expectedCustomErrorName}'` - ); - } - const onSuccess = () => { + if (utils.flag(this, ASSERTION_ABORTED) === true) { + return; + } + const assert = buildAssert(negated, onSuccess); assert( @@ -59,6 +54,12 @@ export function supportRevertedWithCustomError( }; const onError = (error: any) => { + if (utils.flag(this, ASSERTION_ABORTED) === true) { + return; + } + + const { toBeHex } = require("ethers") as typeof EthersT; + const assert = buildAssert(negated, onError); const returnData = getReturnDataFromError(error); @@ -77,12 +78,12 @@ export function supportRevertedWithCustomError( } else if (decodedReturnData.kind === "Panic") { assert( false, - `Expected transaction to be reverted with custom error '${expectedCustomErrorName}', but it reverted with panic code ${decodedReturnData.code.toHexString()} (${ - decodedReturnData.description - })` + `Expected transaction to be reverted with custom error '${expectedCustomErrorName}', but it reverted with panic code ${toBeHex( + decodedReturnData.code + )} (${decodedReturnData.description})` ); } else if (decodedReturnData.kind === "Custom") { - if (decodedReturnData.id === expectedCustomError.id) { + if (decodedReturnData.id === expectedCustomError.selector) { // add flag with the data needed for .withArgs const customErrorAssertionData: CustomErrorAssertionData = { contractInterface: iface, @@ -99,12 +100,9 @@ export function supportRevertedWithCustomError( } else { // try to decode the actual custom error // this will only work when the error comes from the given contract - const actualCustomError = findCustomErrorById( - iface, - decodedReturnData.id - ); + const actualCustomError = iface.getError(decodedReturnData.id); - if (actualCustomError === undefined) { + if (actualCustomError === null) { assert( false, `Expected transaction to be reverted with custom error '${expectedCustomErrorName}', but it reverted with a different custom error` @@ -138,10 +136,49 @@ export function supportRevertedWithCustomError( ); } +function validateInput( + obj: any, + contract: EthersT.BaseContract, + expectedCustomErrorName: string +): { iface: EthersT.Interface; expectedCustomError: EthersT.ErrorFragment } { + try { + // check the case where users forget to pass the contract as the first + // argument + if (typeof contract === "string" || contract?.interface === undefined) { + // discard subject since it could potentially be a rejected promise + throw new TypeError( + "The first argument of .revertedWithCustomError must be the contract that defines the custom error" + ); + } + + // validate custom error name + if (typeof expectedCustomErrorName !== "string") { + throw new TypeError("Expected the custom error name to be a string"); + } + + const iface = contract.interface; + const expectedCustomError = iface.getError(expectedCustomErrorName); + + // check that interface contains the given custom error + if (expectedCustomError === null) { + throw new Error( + `The given contract doesn't have a custom error named '${expectedCustomErrorName}'` + ); + } + + return { iface, expectedCustomError }; + } catch (e) { + // if the input validation fails, we discard the subject since it could + // potentially be a rejected promise + Promise.resolve(obj).catch(() => {}); + throw e; + } +} + export async function revertedWithCustomErrorWithArgs( context: any, Assertion: Chai.AssertionStatic, - utils: Chai.ChaiUtils, + _utils: Chai.ChaiUtils, expectedArgs: any[], ssfi: Ssfi ) { @@ -160,9 +197,10 @@ export async function revertedWithCustomErrorWithArgs( const { contractInterface, customError, returnData } = customErrorAssertionData; - const errorFragment = contractInterface.errors[customError.signature]; + const errorFragment = contractInterface.getError(customError.name); + assertIsNotNull(errorFragment, "errorFragment"); // We transform ether's Array-like object into an actual array as it's safer - const actualArgs = Array.from( + const actualArgs = resultToArray( contractInterface.decodeErrorResult(errorFragment, returnData) ); @@ -210,51 +248,3 @@ export async function revertedWithCustomErrorWithArgs( } } } - -interface CustomError { - name: string; - id: string; - signature: string; -} - -function findCustomErrorByName( - iface: any, - name: string -): CustomError | undefined { - const ethers = require("ethers"); - - const customErrorEntry = Object.entries(iface.errors).find( - ([, fragment]: any) => fragment.name === name - ); - - if (customErrorEntry === undefined) { - return undefined; - } - - const [customErrorSignature] = customErrorEntry; - const customErrorId = ethers.utils.id(customErrorSignature).slice(0, 10); - - return { - id: customErrorId, - name, - signature: customErrorSignature, - }; -} - -function findCustomErrorById(iface: any, id: string): CustomError | undefined { - const ethers = require("ethers"); - - const customErrorEntry: any = Object.entries(iface.errors).find( - ([signature]: any) => ethers.utils.id(signature).slice(0, 10) === id - ); - - if (customErrorEntry === undefined) { - return undefined; - } - - return { - id, - name: customErrorEntry[1].name, - signature: customErrorEntry[0], - }; -} diff --git a/packages/hardhat-chai-matchers/src/internal/reverted/revertedWithPanic.ts b/packages/hardhat-chai-matchers/src/internal/reverted/revertedWithPanic.ts index 9ec6050d3d..022e3ab561 100644 --- a/packages/hardhat-chai-matchers/src/internal/reverted/revertedWithPanic.ts +++ b/packages/hardhat-chai-matchers/src/internal/reverted/revertedWithPanic.ts @@ -1,4 +1,4 @@ -import type { BigNumber } from "ethers"; +import type EthersT from "ethers"; import { normalizeToBigInt } from "hardhat/common"; @@ -10,33 +10,37 @@ export function supportRevertedWithPanic(Assertion: Chai.AssertionStatic) { Assertion.addMethod( "revertedWithPanic", function (this: any, expectedCodeArg: any) { - const ethers = require("ethers"); + const ethers = require("ethers") as typeof EthersT; // capture negated flag before async code executes; see buildAssert's jsdoc const negated = this.__flags.negate; - let expectedCode: BigNumber | undefined; + let expectedCode: bigint | undefined; try { if (expectedCodeArg !== undefined) { - const normalizedCode = normalizeToBigInt(expectedCodeArg); - expectedCode = ethers.BigNumber.from(normalizedCode); + expectedCode = normalizeToBigInt(expectedCodeArg); } } catch { + // if the input validation fails, we discard the subject since it could + // potentially be a rejected promise + Promise.resolve(this._obj).catch(() => {}); throw new TypeError( `Expected the given panic code to be a number-like value, but got '${expectedCodeArg}'` ); } - const code: number | undefined = expectedCode as any; + const code: bigint | undefined = expectedCode; let description: string | undefined; let formattedPanicCode: string; if (code === undefined) { formattedPanicCode = "some panic code"; } else { - const codeBN = ethers.BigNumber.from(code); + const codeBN = ethers.toBigInt(code); description = panicErrorCodeToReason(codeBN) ?? "unknown panic code"; - formattedPanicCode = `panic code ${codeBN.toHexString()} (${description})`; + formattedPanicCode = `panic code ${ethers.toBeHex( + codeBN + )} (${description})`; } const onSuccess = () => { @@ -67,19 +71,19 @@ export function supportRevertedWithPanic(Assertion: Chai.AssertionStatic) { } else if (decodedReturnData.kind === "Panic") { if (code !== undefined) { assert( - decodedReturnData.code.eq(code), - `Expected transaction to be reverted with ${formattedPanicCode}, but it reverted with panic code ${decodedReturnData.code.toHexString()} (${ - decodedReturnData.description - })`, + decodedReturnData.code === code, + `Expected transaction to be reverted with ${formattedPanicCode}, but it reverted with panic code ${ethers.toBeHex( + decodedReturnData.code + )} (${decodedReturnData.description})`, `Expected transaction NOT to be reverted with ${formattedPanicCode}, but it was` ); } else { assert( true, undefined, - `Expected transaction NOT to be reverted with ${formattedPanicCode}, but it reverted with panic code ${decodedReturnData.code.toHexString()} (${ - decodedReturnData.description - })` + `Expected transaction NOT to be reverted with ${formattedPanicCode}, but it reverted with panic code ${ethers.toBeHex( + decodedReturnData.code + )} (${decodedReturnData.description})` ); } } else if (decodedReturnData.kind === "Custom") { diff --git a/packages/hardhat-chai-matchers/src/internal/reverted/revertedWithoutReason.ts b/packages/hardhat-chai-matchers/src/internal/reverted/revertedWithoutReason.ts index f9a5286377..0a7da1a859 100644 --- a/packages/hardhat-chai-matchers/src/internal/reverted/revertedWithoutReason.ts +++ b/packages/hardhat-chai-matchers/src/internal/reverted/revertedWithoutReason.ts @@ -1,3 +1,5 @@ +import type EthersT from "ethers"; + import { buildAssert } from "../../utils"; import { decodeReturnData, getReturnDataFromError } from "./utils"; @@ -16,6 +18,7 @@ export function supportRevertedWithoutReason(Assertion: Chai.AssertionStatic) { }; const onError = (error: any) => { + const { toBeHex } = require("ethers") as typeof EthersT; const assert = buildAssert(negated, onError); const returnData = getReturnDataFromError(error); @@ -35,9 +38,9 @@ export function supportRevertedWithoutReason(Assertion: Chai.AssertionStatic) { } else if (decodedReturnData.kind === "Panic") { assert( false, - `Expected transaction to be reverted without a reason, but it reverted with panic code ${decodedReturnData.code.toHexString()} (${ - decodedReturnData.description - })` + `Expected transaction to be reverted without a reason, but it reverted with panic code ${toBeHex( + decodedReturnData.code + )} (${decodedReturnData.description})` ); } else if (decodedReturnData.kind === "Custom") { assert( diff --git a/packages/hardhat-chai-matchers/src/internal/reverted/utils.ts b/packages/hardhat-chai-matchers/src/internal/reverted/utils.ts index 84d537ff7b..253ed71562 100644 --- a/packages/hardhat-chai-matchers/src/internal/reverted/utils.ts +++ b/packages/hardhat-chai-matchers/src/internal/reverted/utils.ts @@ -1,4 +1,4 @@ -import type { BigNumber } from "ethers"; +import type EthersT from "ethers"; import { AssertionError } from "chai"; @@ -51,7 +51,7 @@ type DecodedReturnData = } | { kind: "Panic"; - code: BigNumber; + code: bigint; description: string; } | { @@ -61,7 +61,9 @@ type DecodedReturnData = }; export function decodeReturnData(returnData: string): DecodedReturnData { - const { defaultAbiCoder: abi } = require("@ethersproject/abi"); + const { AbiCoder } = require("ethers") as typeof EthersT; + const abi = new AbiCoder(); + if (returnData === "0x") { return { kind: "Empty" }; } else if (returnData.startsWith(ERROR_STRING_PREFIX)) { @@ -79,7 +81,7 @@ export function decodeReturnData(returnData: string): DecodedReturnData { }; } else if (returnData.startsWith(PANIC_CODE_PREFIX)) { const encodedReason = returnData.slice(PANIC_CODE_PREFIX.length); - let code: BigNumber; + let code: bigint; try { code = abi.decode(["uint256"], `0x${encodedReason}`)[0]; } catch (e: any) { @@ -101,3 +103,25 @@ export function decodeReturnData(returnData: string): DecodedReturnData { data: `0x${returnData.slice(10)}`, }; } + +/** + * Takes an ethers result object and converts it into a (potentially nested) array. + * + * For example, given this error: + * + * struct Point(uint x, uint y) + * error MyError(string, Point) + * + * revert MyError("foo", Point(1, 2)) + * + * The resulting array will be: ["foo", [1n, 2n]] + */ +export function resultToArray(result: EthersT.Result): any[] { + return result + .toArray() + .map((x) => + typeof x === "object" && x !== null && "toArray" in x + ? resultToArray(x) + : x + ); +} diff --git a/packages/hardhat-chai-matchers/src/internal/utils.ts b/packages/hardhat-chai-matchers/src/internal/utils.ts new file mode 100644 index 0000000000..b3b672c2e4 --- /dev/null +++ b/packages/hardhat-chai-matchers/src/internal/utils.ts @@ -0,0 +1,12 @@ +import { HardhatChaiMatchersAssertionError } from "./errors"; + +export function assertIsNotNull( + value: T, + valueName: string +): asserts value is Exclude { + if (value === null) { + throw new HardhatChaiMatchersAssertionError( + `${valueName} should not be null` + ); + } +} diff --git a/packages/hardhat-chai-matchers/src/internal/withArgs.ts b/packages/hardhat-chai-matchers/src/internal/withArgs.ts index 856c055e59..29c3e3eda6 100644 --- a/packages/hardhat-chai-matchers/src/internal/withArgs.ts +++ b/packages/hardhat-chai-matchers/src/internal/withArgs.ts @@ -1,6 +1,7 @@ import { AssertionError } from "chai"; import { isBigNumber, normalizeToBigInt } from "hardhat/common"; +import { ASSERTION_ABORTED } from "./constants"; import { emitWithArgs, EMIT_CALLED } from "./emit"; import { @@ -51,24 +52,7 @@ export function supportWithArgs( utils: Chai.ChaiUtils ) { Assertion.addMethod("withArgs", function (this: any, ...expectedArgs: any[]) { - if (Boolean(this.__flags.negate)) { - throw new Error("Do not combine .not. with .withArgs()"); - } - - const emitCalled = utils.flag(this, EMIT_CALLED) === true; - const revertedWithCustomErrorCalled = - utils.flag(this, REVERTED_WITH_CUSTOM_ERROR_CALLED) === true; - - if (!emitCalled && !revertedWithCustomErrorCalled) { - throw new Error( - "withArgs can only be used in combination with a previous .emit or .revertedWithCustomError assertion" - ); - } - if (emitCalled && revertedWithCustomErrorCalled) { - throw new Error( - "withArgs called with both .emit and .revertedWithCustomError, but these assertions cannot be combined" - ); - } + const { emitCalled } = validateInput.call(this, utils); const promise = this.then === undefined ? Promise.resolve() : this; @@ -93,3 +77,39 @@ export function supportWithArgs( return this; }); } + +function validateInput( + this: any, + utils: Chai.ChaiUtils +): { emitCalled: boolean } { + try { + if (Boolean(this.__flags.negate)) { + throw new Error("Do not combine .not. with .withArgs()"); + } + + const emitCalled = utils.flag(this, EMIT_CALLED) === true; + const revertedWithCustomErrorCalled = + utils.flag(this, REVERTED_WITH_CUSTOM_ERROR_CALLED) === true; + + if (!emitCalled && !revertedWithCustomErrorCalled) { + throw new Error( + "withArgs can only be used in combination with a previous .emit or .revertedWithCustomError assertion" + ); + } + if (emitCalled && revertedWithCustomErrorCalled) { + throw new Error( + "withArgs called with both .emit and .revertedWithCustomError, but these assertions cannot be combined" + ); + } + + return { emitCalled }; + } catch (e) { + // signal that validation failed to allow the matchers to finish early + utils.flag(this, ASSERTION_ABORTED, true); + + // discard subject since it could potentially be a rejected promise + Promise.resolve(this._obj).catch(() => {}); + + throw e; + } +} diff --git a/packages/hardhat-chai-matchers/test/bigNumber.ts b/packages/hardhat-chai-matchers/test/bigNumber.ts index 2bfcff1967..7adb08c353 100644 --- a/packages/hardhat-chai-matchers/test/bigNumber.ts +++ b/packages/hardhat-chai-matchers/test/bigNumber.ts @@ -1,5 +1,4 @@ import { expect, AssertionError } from "chai"; -import { BigNumber as BigNumberEthers } from "ethers"; import { BigNumber as BigNumberJs } from "bignumber.js"; import BN from "bn.js"; @@ -7,11 +6,10 @@ import { HardhatError } from "hardhat/internal/core/errors"; import "../src/internal/add-chai-matchers"; -type SupportedNumber = number | bigint | BN | BigNumberEthers | BigNumberJs; +type SupportedNumber = number | bigint | BN | BigNumberJs; const numberToBigNumberConversions = [ (n: number) => BigInt(n), - (n: number) => BigNumberEthers.from(n), (n: number) => new BN(n), (n: number) => new BigNumberJs(n), ]; @@ -21,8 +19,6 @@ describe("BigNumber matchers", function () { if (typeof n === "object") { if (n instanceof BN) { return "BN"; - } else if (n instanceof BigNumberEthers) { - return "ethers.BigNumber"; } else if (n instanceof BigNumberJs) { return "bignumber.js"; } @@ -582,7 +578,6 @@ describe("BigNumber matchers", function () { // a few particular combinations of types don't work: if ( typeof convertedActual === "string" && - !BigNumberEthers.isBigNumber(convertedExpected) && !BN.isBN(convertedExpected) && !BigNumberJs.isBigNumber(convertedExpected) ) { @@ -1208,11 +1203,6 @@ describe("BigNumber matchers", function () { "custom message" ); - // number and ethers bignumber - expect(() => - expect(1).to.equal(BigNumberEthers.from(2), "custom message") - ).to.throw(AssertionError, "custom message"); - // same but for deep comparisons expect(() => expect([1]).to.equal([2], "custom message")).to.throw( AssertionError, @@ -1224,10 +1214,5 @@ describe("BigNumber matchers", function () { AssertionError, "custom message" ); - - // number and ethers bignumber - expect(() => - expect([1]).to.equal([BigNumberEthers.from(2)], "custom message") - ).to.throw(AssertionError, "custom message"); }); }); diff --git a/packages/hardhat-chai-matchers/test/changeEtherBalance.ts b/packages/hardhat-chai-matchers/test/changeEtherBalance.ts index 12fc629588..9c2647ff48 100644 --- a/packages/hardhat-chai-matchers/test/changeEtherBalance.ts +++ b/packages/hardhat-chai-matchers/test/changeEtherBalance.ts @@ -1,10 +1,10 @@ import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; import { expect, AssertionError } from "chai"; -import { BigNumber, Contract } from "ethers"; import path from "path"; import util from "util"; import "../src/internal/add-chai-matchers"; +import { ChangeEtherBalance } from "./contracts"; import { useEnvironment, useEnvironmentWithNode } from "./helpers"; describe("INTEGRATION: changeEtherBalance matcher", function () { @@ -14,7 +14,9 @@ describe("INTEGRATION: changeEtherBalance matcher", function () { runTests(); }); - describe("connected to a hardhat node", function () { + // TODO re-enable this when + // https://github.com/ethers-io/ethers.js/issues/4014 is fixed + describe.skip("connected to a hardhat node", function () { useEnvironmentWithNode("hardhat-project"); runTests(); @@ -23,7 +25,7 @@ describe("INTEGRATION: changeEtherBalance matcher", function () { function runTests() { let sender: SignerWithAddress; let receiver: SignerWithAddress; - let contract: Contract; + let contract: ChangeEtherBalance; let txGasFees: number; beforeEach(async function () { @@ -31,7 +33,9 @@ describe("INTEGRATION: changeEtherBalance matcher", function () { sender = wallets[0]; receiver = wallets[1]; contract = await ( - await this.hre.ethers.getContractFactory("ChangeEtherBalance") + await this.hre.ethers.getContractFactory<[], ChangeEtherBalance>( + "ChangeEtherBalance" + ) ).deploy(); txGasFees = 1 * 21_000; await this.hre.network.provider.send( @@ -53,13 +57,22 @@ describe("INTEGRATION: changeEtherBalance matcher", function () { it("Should fail when block contains more than one transaction", async function () { await this.hre.network.provider.send("evm_setAutomine", [false]); - await sender.sendTransaction({ to: receiver.address, value: 200 }); + + // we set a gas limit to avoid using the whole block gas limit + await sender.sendTransaction({ + to: receiver.address, + value: 200, + gasLimit: 30_000, + }); + await this.hre.network.provider.send("evm_setAutomine", [true]); + await expect( expect(() => sender.sendTransaction({ to: receiver.address, value: 200, + gasLimit: 30_000, }) ).to.changeEtherBalance(sender, -200, { includeFee: true }) ).to.be.eventually.rejectedWith( @@ -86,15 +99,6 @@ describe("INTEGRATION: changeEtherBalance matcher", function () { ).to.changeEtherBalance(sender, BigInt("-200")); }); - it("Should pass when given an ethers BigNumber", async () => { - await expect(() => - sender.sendTransaction({ - to: receiver.address, - value: 200, - }) - ).to.changeEtherBalance(sender, BigNumber.from("-200")); - }); - it("Should pass when expected balance change is passed as int and is equal to an actual", async () => { await expect(() => sender.sendTransaction({ @@ -136,24 +140,6 @@ describe("INTEGRATION: changeEtherBalance matcher", function () { ).to.changeEtherBalance(sender, -200); }); - it("Should pass when expected balance change is passed as BN and is equal to an actual", async () => { - await expect(() => - sender.sendTransaction({ - to: receiver.address, - value: 200, - }) - ).to.changeEtherBalance(receiver, BigNumber.from(200)); - }); - - it("Should pass on negative case when expected balance change is not equal to an actual", async () => { - await expect(() => - sender.sendTransaction({ - to: receiver.address, - value: 200, - }) - ).to.not.changeEtherBalance(receiver, BigNumber.from(300)); - }); - it("Should throw when fee was not calculated correctly", async () => { await expect( expect(() => @@ -206,7 +192,8 @@ describe("INTEGRATION: changeEtherBalance matcher", function () { }); it("shouldn't run the transaction twice", async function () { - const receiverBalanceBefore = await receiver.getBalance(); + const receiverBalanceBefore: bigint = + await this.hre.ethers.provider.getBalance(receiver); await expect(() => sender.sendTransaction({ @@ -215,11 +202,12 @@ describe("INTEGRATION: changeEtherBalance matcher", function () { }) ).to.changeEtherBalance(sender, -200); - const receiverBalanceChange = (await receiver.getBalance()).sub( - receiverBalanceBefore - ); + const receiverBalanceAfter: bigint = + await this.hre.ethers.provider.getBalance(receiver); + const receiverBalanceChange = + receiverBalanceAfter - receiverBalanceBefore; - expect(receiverBalanceChange.toNumber()).to.equal(200); + expect(receiverBalanceChange).to.equal(200n); }); }); @@ -227,7 +215,7 @@ describe("INTEGRATION: changeEtherBalance matcher", function () { it("Should pass when expected balance change is passed as int and is equal to an actual", async () => { await expect(async () => sender.sendTransaction({ - to: contract.address, + to: contract, value: 200, }) ).to.changeEtherBalance(contract, 200); @@ -300,28 +288,6 @@ describe("INTEGRATION: changeEtherBalance matcher", function () { ).to.changeEtherBalance(sender, -200); }); - it("Should pass when expected balance change is passed as BN and is equal to an actual", async () => { - await expect(() => - sender.sendTransaction({ - to: receiver.address, - maxFeePerGas: 2, - maxPriorityFeePerGas: 1, - value: 200, - }) - ).to.changeEtherBalance(receiver, BigNumber.from(200)); - }); - - it("Should pass on negative case when expected balance change is not equal to an actual", async () => { - await expect(() => - sender.sendTransaction({ - to: receiver.address, - maxFeePerGas: 2, - maxPriorityFeePerGas: 1, - value: 200, - }) - ).to.not.changeEtherBalance(receiver, BigNumber.from(300)); - }); - it("Should throw when fee was not calculated correctly", async () => { await expect( expect(() => @@ -377,7 +343,7 @@ describe("INTEGRATION: changeEtherBalance matcher", function () { it("Should pass when expected balance change is passed as int and is equal to an actual", async () => { await expect(async () => sender.sendTransaction({ - to: contract.address, + to: contract, maxFeePerGas: 2, maxPriorityFeePerGas: 1, value: 200, @@ -387,17 +353,17 @@ describe("INTEGRATION: changeEtherBalance matcher", function () { it("Should take into account transaction fee", async function () { const tx = { - to: contract.address, + to: contract, maxFeePerGas: 2, maxPriorityFeePerGas: 1, value: 200, }; - const gas = await this.hre.ethers.provider.estimateGas(tx); + const gas: bigint = await this.hre.ethers.provider.estimateGas(tx); await expect(() => sender.sendTransaction(tx)).to.changeEtherBalance( sender, - -gas.add(200).toNumber(), + -(gas + 200n), { includeFee: true, } @@ -416,7 +382,8 @@ describe("INTEGRATION: changeEtherBalance matcher", function () { }); it("shouldn't run the transaction twice", async function () { - const receiverBalanceBefore = await receiver.getBalance(); + const receiverBalanceBefore: bigint = + await this.hre.ethers.provider.getBalance(receiver); await expect(() => sender.sendTransaction({ @@ -427,11 +394,12 @@ describe("INTEGRATION: changeEtherBalance matcher", function () { }) ).to.changeEtherBalance(sender, -200); - const receiverBalanceChange = (await receiver.getBalance()).sub( - receiverBalanceBefore - ); + const receiverBalanceAfter: bigint = + await this.hre.ethers.provider.getBalance(receiver); + const receiverBalanceChange = + receiverBalanceAfter - receiverBalanceBefore; - expect(receiverBalanceChange.toNumber()).to.equal(200); + expect(receiverBalanceChange).to.equal(200n); }); }); @@ -455,24 +423,6 @@ describe("INTEGRATION: changeEtherBalance matcher", function () { ).to.changeEtherBalance(receiver, 200); }); - it("Should pass when expected balance change is passed as BN and is equal to an actual", async () => { - await expect( - await sender.sendTransaction({ - to: receiver.address, - value: 200, - }) - ).to.changeEtherBalance(sender, BigNumber.from(-200)); - }); - - it("Should pass on negative case when expected balance change is not equal to an actual", async () => { - await expect( - await sender.sendTransaction({ - to: receiver.address, - value: 200, - }) - ).to.not.changeEtherBalance(receiver, BigNumber.from(300)); - }); - it("Should throw when expected balance change value was different from an actual", async () => { await expect( expect( @@ -506,7 +456,7 @@ describe("INTEGRATION: changeEtherBalance matcher", function () { it("Should pass when expected balance change is passed as int and is equal to an actual", async () => { await expect( await sender.sendTransaction({ - to: contract.address, + to: contract, value: 200, }) ).to.changeEtherBalance(contract, 200); @@ -534,24 +484,6 @@ describe("INTEGRATION: changeEtherBalance matcher", function () { ).to.changeEtherBalance(receiver, 200); }); - it("Should pass when expected balance change is passed as BN and is equal to an actual", async () => { - await expect( - sender.sendTransaction({ - to: receiver.address, - value: 200, - }) - ).to.changeEtherBalance(sender, BigNumber.from(-200)); - }); - - it("Should pass on negative case when expected balance change is not equal to an actual", async () => { - await expect( - sender.sendTransaction({ - to: receiver.address, - value: 200, - }) - ).to.not.changeEtherBalance(receiver, BigNumber.from(300)); - }); - it("Should throw when expected balance change value was different from an actual", async () => { await expect( expect( diff --git a/packages/hardhat-chai-matchers/test/changeEtherBalances.ts b/packages/hardhat-chai-matchers/test/changeEtherBalances.ts index 3fbbde9a94..b88e48fead 100644 --- a/packages/hardhat-chai-matchers/test/changeEtherBalances.ts +++ b/packages/hardhat-chai-matchers/test/changeEtherBalances.ts @@ -1,10 +1,10 @@ import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; import { expect, AssertionError } from "chai"; -import { BigNumber, Contract } from "ethers"; import path from "path"; import util from "util"; import "../src/internal/add-chai-matchers"; +import { ChangeEtherBalance } from "./contracts"; import { useEnvironment, useEnvironmentWithNode } from "./helpers"; describe("INTEGRATION: changeEtherBalances matcher", function () { @@ -15,6 +15,7 @@ describe("INTEGRATION: changeEtherBalances matcher", function () { }); describe("connected to a hardhat node", function () { + process.env.CHAIN_ID = "12345"; useEnvironmentWithNode("hardhat-project"); runTests(); @@ -23,7 +24,7 @@ describe("INTEGRATION: changeEtherBalances matcher", function () { function runTests() { let sender: SignerWithAddress; let receiver: SignerWithAddress; - let contract: Contract; + let contract: ChangeEtherBalance; let txGasFees: number; beforeEach(async function () { @@ -31,7 +32,9 @@ describe("INTEGRATION: changeEtherBalances matcher", function () { sender = wallets[0]; receiver = wallets[1]; contract = await ( - await this.hre.ethers.getContractFactory("ChangeEtherBalance") + await this.hre.ethers.getContractFactory<[], ChangeEtherBalance>( + "ChangeEtherBalance" + ) ).deploy(); txGasFees = 1 * 21_000; await this.hre.network.provider.send( @@ -45,7 +48,7 @@ describe("INTEGRATION: changeEtherBalances matcher", function () { it("Should pass when all expected balance changes are equal to actual values", async () => { await expect(() => sender.sendTransaction({ - to: contract.address, + to: contract, value: 200, }) ).to.changeEtherBalances([sender, contract], [-200, 200]); @@ -100,19 +103,6 @@ describe("INTEGRATION: changeEtherBalances matcher", function () { ); }); - it("Should pass when given ethers BigNumber", async () => { - await expect(() => - sender.sendTransaction({ - to: receiver.address, - gasPrice: 1, - value: 200, - }) - ).to.changeEtherBalances( - [sender, receiver], - [BigNumber.from("-200"), BigNumber.from(200)] - ); - }); - it("Should take into account transaction fee (legacy tx)", async () => { await expect(() => sender.sendTransaction({ @@ -213,7 +203,9 @@ describe("INTEGRATION: changeEtherBalances matcher", function () { }); it("shouldn't run the transaction twice", async function () { - const receiverBalanceBefore = await receiver.getBalance(); + const receiverBalanceBefore = await this.hre.ethers.provider.getBalance( + receiver + ); await expect(() => sender.sendTransaction({ @@ -223,11 +215,13 @@ describe("INTEGRATION: changeEtherBalances matcher", function () { }) ).to.changeEtherBalances([sender, receiver], [-200, 200]); - const receiverBalanceChange = (await receiver.getBalance()).sub( - receiverBalanceBefore + const receiverBalanceAfter = await this.hre.ethers.provider.getBalance( + receiver ); + const receiverBalanceChange = + receiverBalanceAfter - receiverBalanceBefore; - expect(receiverBalanceChange.toNumber()).to.equal(200); + expect(receiverBalanceChange).to.equal(200n); }); }); @@ -236,7 +230,7 @@ describe("INTEGRATION: changeEtherBalances matcher", function () { it("Should pass when all expected balance changes are equal to actual values", async () => { await expect( await sender.sendTransaction({ - to: contract.address, + to: contract, value: 200, }) ).to.changeEtherBalances([sender, contract], [-200, 200]); diff --git a/packages/hardhat-chai-matchers/test/changeTokenBalance.ts b/packages/hardhat-chai-matchers/test/changeTokenBalance.ts index a180425095..51fc3088f4 100644 --- a/packages/hardhat-chai-matchers/test/changeTokenBalance.ts +++ b/packages/hardhat-chai-matchers/test/changeTokenBalance.ts @@ -1,16 +1,19 @@ +import type { TransactionResponse } from "ethers"; + import assert from "assert"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; import { AssertionError, expect } from "chai"; -import { BigNumber, Contract, providers } from "ethers"; import path from "path"; import util from "util"; import "../src/internal/add-chai-matchers"; -import { clearTokenDescriptionsCache } from "../src/internal/changeTokenBalance"; +import { + clearTokenDescriptionsCache, + Token, +} from "../src/internal/changeTokenBalance"; +import { MatchersContract } from "./contracts"; import { useEnvironment, useEnvironmentWithNode } from "./helpers"; -type TransactionResponse = providers.TransactionResponse; - describe("INTEGRATION: changeTokenBalance and changeTokenBalances matchers", function () { describe("with the in-process hardhat network", function () { useEnvironment("hardhat-project"); @@ -31,21 +34,33 @@ describe("INTEGRATION: changeTokenBalance and changeTokenBalances matchers", fun function runTests() { let sender: SignerWithAddress; let receiver: SignerWithAddress; - let mockToken: Contract; + let mockToken: Token; + let matchers: MatchersContract; beforeEach(async function () { const wallets = await this.hre.ethers.getSigners(); sender = wallets[0]; receiver = wallets[1]; - const MockToken = await this.hre.ethers.getContractFactory("MockToken"); + const MockToken = await this.hre.ethers.getContractFactory<[], Token>( + "MockToken" + ); mockToken = await MockToken.deploy(); + + const Matchers = await this.hre.ethers.getContractFactory< + [], + MatchersContract + >("Matchers"); + matchers = await Matchers.deploy(); }); describe("transaction that doesn't move tokens", () => { it("with a promise of a TxResponse", async function () { + const transactionResponse = sender.sendTransaction({ + to: receiver.address, + }); await runAllAsserts( - sender.sendTransaction({ to: receiver.address }), + transactionResponse, mockToken, [sender, receiver], [0, 0] @@ -232,11 +247,10 @@ describe("INTEGRATION: changeTokenBalance and changeTokenBalances matchers", fun mockToken.transfer(receiver.address, 50) ).to.changeTokenBalance(mockToken, receiver, 50); - const receiverBalanceChange = ( - await mockToken.balanceOf(receiver.address) - ).sub(receiverBalanceBefore); + const receiverBalanceChange = + (await mockToken.balanceOf(receiver.address)) - receiverBalanceBefore; - expect(receiverBalanceChange.toNumber()).to.equal(50); + expect(receiverBalanceChange).to.equal(50n); }); it("changeTokenBalances shouldn't run the transaction twice", async function () { @@ -248,11 +262,10 @@ describe("INTEGRATION: changeTokenBalance and changeTokenBalances matchers", fun mockToken.transfer(receiver.address, 50) ).to.changeTokenBalances(mockToken, [sender, receiver], [-50, 50]); - const receiverBalanceChange = ( - await mockToken.balanceOf(receiver.address) - ).sub(receiverBalanceBefore); + const receiverBalanceChange = + (await mockToken.balanceOf(receiver.address)) - receiverBalanceBefore; - expect(receiverBalanceChange.toNumber()).to.equal(50); + expect(receiverBalanceChange).to.equal(50n); }); it("negated", async function () { @@ -334,9 +347,10 @@ describe("INTEGRATION: changeTokenBalance and changeTokenBalances matchers", fun }); it("uses the token name if the contract doesn't have a symbol", async function () { - const TokenWithOnlyName = await this.hre.ethers.getContractFactory( - "TokenWithOnlyName" - ); + const TokenWithOnlyName = await this.hre.ethers.getContractFactory< + [], + Token + >("TokenWithOnlyName"); const tokenWithOnlyName = await TokenWithOnlyName.deploy(); await expect( @@ -360,7 +374,7 @@ describe("INTEGRATION: changeTokenBalance and changeTokenBalances matchers", fun it("uses the contract address if the contract doesn't have name or symbol", async function () { const TokenWithoutNameNorSymbol = - await this.hre.ethers.getContractFactory( + await this.hre.ethers.getContractFactory<[], Token>( "TokenWithoutNameNorSymbol" ); const tokenWithoutNameNorSymbol = @@ -429,13 +443,17 @@ describe("INTEGRATION: changeTokenBalance and changeTokenBalances matchers", fun it("tx is not the only one in the block", async function () { await this.hre.network.provider.send("evm_setAutomine", [false]); - await sender.sendTransaction({ to: receiver.address }); + // we set a gas limit to avoid using the whole block gas limit + await sender.sendTransaction({ + to: receiver.address, + gasLimit: 30_000, + }); await this.hre.network.provider.send("evm_setAutomine", [true]); await expect( expect( - mockToken.transfer(receiver.address, 50) + mockToken.transfer(receiver.address, 50, { gasLimit: 100_000 }) ).to.changeTokenBalance(mockToken, sender, -50) ).to.be.rejectedWith(Error, "Multiple transactions found in block"); }); @@ -501,16 +519,33 @@ describe("INTEGRATION: changeTokenBalance and changeTokenBalances matchers", fun ); }); + it("arrays have different length, subject is a rejected promise", async function () { + expect(() => + expect(matchers.revertsWithoutReason()).to.changeTokenBalances( + mockToken, + [sender], + [-50, 50] + ) + ).to.throw( + Error, + "The number of accounts (1) is different than the number of expected balance changes (2)" + ); + }); + it("tx is not the only one in the block", async function () { await this.hre.network.provider.send("evm_setAutomine", [false]); - await sender.sendTransaction({ to: receiver.address }); + // we set a gas limit to avoid using the whole block gas limit + await sender.sendTransaction({ + to: receiver.address, + gasLimit: 30_000, + }); await this.hre.network.provider.send("evm_setAutomine", [true]); await expect( expect( - mockToken.transfer(receiver.address, 50) + mockToken.transfer(receiver.address, 50, { gasLimit: 100_000 }) ).to.changeTokenBalances(mockToken, [sender, receiver], [-50, 50]) ).to.be.rejectedWith(Error, "Multiple transactions found in block"); }); @@ -543,38 +578,6 @@ describe("INTEGRATION: changeTokenBalance and changeTokenBalances matchers", fun [BigInt(-50), BigInt(50)] ); }); - - it("ethers's bignumbers are accepted", async function () { - await expect( - mockToken.transfer(receiver.address, 50) - ).to.changeTokenBalance(mockToken, sender, BigNumber.from(-50)); - - await expect( - mockToken.transfer(receiver.address, 50) - ).to.changeTokenBalances( - mockToken, - [sender, receiver], - [BigNumber.from(-50), BigNumber.from(50)] - ); - }); - - it("mixed types are accepted", async function () { - await expect( - mockToken.transfer(receiver.address, 50) - ).to.changeTokenBalances( - mockToken, - [sender, receiver], - [BigInt(-50), BigNumber.from(50)] - ); - - await expect( - mockToken.transfer(receiver.address, 50) - ).to.changeTokenBalances( - mockToken, - [sender, receiver], - [BigNumber.from(-50), BigInt(50)] - ); - }); }); // smoke tests for stack traces @@ -638,9 +641,9 @@ async function runAllAsserts( | Promise | (() => TransactionResponse) | (() => Promise), - token: Contract, + token: Token, accounts: Array, - balances: Array + balances: Array ) { // changeTokenBalances works for the given arrays await expect(expr).to.changeTokenBalances(token, accounts, balances); diff --git a/packages/hardhat-chai-matchers/test/contracts.ts b/packages/hardhat-chai-matchers/test/contracts.ts new file mode 100644 index 0000000000..c955e55bcc --- /dev/null +++ b/packages/hardhat-chai-matchers/test/contracts.ts @@ -0,0 +1,127 @@ +import { + BaseContract, + BaseContractMethod, + ContractTransactionResponse, + BigNumberish, +} from "ethers"; + +export type MatchersContract = BaseContract & { + panicAssert: BaseContractMethod<[], void, ContractTransactionResponse>; + revertWithCustomErrorWithInt: BaseContractMethod< + [BigNumberish], + void, + ContractTransactionResponse + >; + revertWithCustomErrorWithPair: BaseContractMethod< + [BigNumberish, BigNumberish], + void, + ContractTransactionResponse + >; + revertWithCustomErrorWithUint: BaseContractMethod< + [BigNumberish], + void, + ContractTransactionResponse + >; + revertWithCustomErrorWithUintAndString: BaseContractMethod< + [BigNumberish, string], + void, + ContractTransactionResponse + >; + revertWithSomeCustomError: BaseContractMethod< + [], + void, + ContractTransactionResponse + >; + revertsWith: BaseContractMethod<[string], void, ContractTransactionResponse>; + revertsWithoutReason: BaseContractMethod< + [], + void, + ContractTransactionResponse + >; + succeeds: BaseContractMethod<[], void, ContractTransactionResponse>; +}; + +export type ChangeEtherBalance = BaseContract & { + returnHalf: BaseContractMethod<[], void, ContractTransactionResponse>; + transferTo: BaseContractMethod<[string], void, ContractTransactionResponse>; +}; + +export type EventsContract = BaseContract & { + doNotEmit: BaseContractMethod<[], void, ContractTransactionResponse>; + emitBytes32: BaseContractMethod<[string], void, ContractTransactionResponse>; + emitBytes32Array: BaseContractMethod< + [string, string], + void, + ContractTransactionResponse + >; + emitBytes: BaseContractMethod<[string], void, ContractTransactionResponse>; + emitIndexedBytes32: BaseContractMethod< + [string], + void, + ContractTransactionResponse + >; + emitIndexedBytes: BaseContractMethod< + [string], + void, + ContractTransactionResponse + >; + emitIndexedString: BaseContractMethod< + [string], + void, + ContractTransactionResponse + >; + emitInt: BaseContractMethod< + [BigNumberish], + void, + ContractTransactionResponse + >; + emitNestedUintFromAnotherContract: BaseContractMethod< + [BigNumberish], + void, + ContractTransactionResponse + >; + emitNestedUintFromSameContract: BaseContractMethod< + [BigNumberish], + void, + ContractTransactionResponse + >; + emitString: BaseContractMethod<[string], void, ContractTransactionResponse>; + emitStruct: BaseContractMethod< + [BigNumberish, BigNumberish], + void, + ContractTransactionResponse + >; + emitTwoUints: BaseContractMethod< + [BigNumberish, BigNumberish], + void, + ContractTransactionResponse + >; + emitTwoUintsAndTwoStrings: BaseContractMethod< + [BigNumberish, BigNumberish, string, string], + void, + ContractTransactionResponse + >; + emitUint: BaseContractMethod< + [BigNumberish], + void, + ContractTransactionResponse + >; + emitUintAndString: BaseContractMethod< + [BigNumberish, string], + void, + ContractTransactionResponse + >; + emitUintArray: BaseContractMethod< + [BigNumberish, BigNumberish], + void, + ContractTransactionResponse + >; + emitUintTwice: BaseContractMethod< + [BigNumberish, BigNumberish], + void, + ContractTransactionResponse + >; + emitWithoutArgs: BaseContractMethod<[], void, ContractTransactionResponse>; +}; + +export type AnotherContract = BaseContract & {}; diff --git a/packages/hardhat-chai-matchers/test/events.ts b/packages/hardhat-chai-matchers/test/events.ts index 7bd6dd7f6d..88392ccedf 100644 --- a/packages/hardhat-chai-matchers/test/events.ts +++ b/packages/hardhat-chai-matchers/test/events.ts @@ -1,15 +1,17 @@ import { expect, AssertionError } from "chai"; -import { BigNumber, Contract, ethers } from "ethers"; +import { ethers } from "ethers"; import { anyUint, anyValue } from "../src/withArgs"; import { useEnvironment, useEnvironmentWithNode } from "./helpers"; import "../src/internal/add-chai-matchers"; +import { AnotherContract, EventsContract, MatchersContract } from "./contracts"; describe(".to.emit (contract events)", () => { - let contract: Contract; - let otherContract: Contract; + let contract: EventsContract; + let otherContract: AnotherContract; + let matchers: MatchersContract; describe("with the in-process hardhat network", function () { useEnvironment("hardhat-project"); @@ -28,9 +30,18 @@ describe(".to.emit (contract events)", () => { otherContract = await ( await this.hre.ethers.getContractFactory("AnotherContract") ).deploy(); + contract = await ( - await this.hre.ethers.getContractFactory("Events") - ).deploy(otherContract.address); + await this.hre.ethers.getContractFactory<[string], EventsContract>( + "Events" + ) + ).deploy(await otherContract.getAddress()); + + const Matchers = await this.hre.ethers.getContractFactory< + [], + MatchersContract + >("Matchers"); + matchers = await Matchers.deploy(); }); it("Should fail when expecting an event that's not in the contract", async function () { @@ -82,6 +93,14 @@ describe(".to.emit (contract events)", () => { ).to.throw(Error, "Do not combine .not. with .withArgs()"); }); + it("Should fail when used with .not, subject is a rejected promise", async function () { + expect(() => + expect(matchers.revertsWithoutReason()) + .not.to.emit(contract, "WithUintArg") + .withArgs(1) + ).to.throw(Error, "Do not combine .not. with .withArgs()"); + }); + it("should fail if withArgs is called on its own", async function () { expect(() => expect(contract.emitUint(1)) @@ -124,13 +143,9 @@ describe(".to.emit (contract events)", () => { }); const string1 = "string1"; - const string1Bytes = ethers.utils.hexlify( - ethers.utils.toUtf8Bytes(string1) - ); + const string1Bytes = ethers.hexlify(ethers.toUtf8Bytes(string1)); const string2 = "string2"; - const string2Bytes = ethers.utils.hexlify( - ethers.utils.toUtf8Bytes(string2) - ); + const string2Bytes = ethers.hexlify(ethers.toUtf8Bytes(string2)); // for abbreviating long strings in diff views like chai does: function abbrev(longString: string): string { @@ -138,7 +153,7 @@ describe(".to.emit (contract events)", () => { } function hash(s: string): string { - return ethers.utils.keccak256(s); + return ethers.keccak256(s); } describe("with a string argument", function () { @@ -264,8 +279,8 @@ describe(".to.emit (contract events)", () => { }); }); - const string1Bytes32 = ethers.utils.zeroPad(string1Bytes, 32); - const string2Bytes32 = ethers.utils.zeroPad(string2Bytes, 32); + const string1Bytes32 = ethers.zeroPadValue(string1Bytes, 32); + const string2Bytes32 = ethers.zeroPadValue(string2Bytes, 32); describe("with a bytes32 argument", function () { it("Should match the argument", async function () { await expect(contract.emitBytes32(string1Bytes32)) @@ -281,8 +296,8 @@ describe(".to.emit (contract events)", () => { ).to.be.eventually.rejectedWith( AssertionError, `expected '${abbrev( - ethers.utils.hexlify(string2Bytes32) - )}' to equal '${abbrev(ethers.utils.hexlify(string1Bytes32))}'` + ethers.hexlify(string2Bytes32) + )}' to equal '${abbrev(ethers.hexlify(string1Bytes32))}'` ); }); }); @@ -302,8 +317,8 @@ describe(".to.emit (contract events)", () => { ).to.be.eventually.rejectedWith( AssertionError, `expected '${abbrev( - ethers.utils.hexlify(string2Bytes32) - )}' to equal '${abbrev(ethers.utils.hexlify(string1Bytes32))}'` + ethers.hexlify(string2Bytes32) + )}' to equal '${abbrev(ethers.hexlify(string1Bytes32))}'` ); }); @@ -321,12 +336,6 @@ describe(".to.emit (contract events)", () => { .withArgs([1, 2]); }); - it("Should succeed when expectations are met with BigNumber", async function () { - await expect(contract.emitUintArray(1, 2)) - .to.emit(contract, "WithUintArray") - .withArgs([BigInt(1), BigNumber.from(2)]); - }); - it("Should fail when expectations are not met", async function () { await expect( expect(contract.emitUintArray(1, 2)) diff --git a/packages/hardhat-chai-matchers/test/fixture-projects/hardhat-project/hardhat.config.js b/packages/hardhat-chai-matchers/test/fixture-projects/hardhat-project/hardhat.config.js index 1940c36a74..10d1d15ed6 100644 --- a/packages/hardhat-chai-matchers/test/fixture-projects/hardhat-project/hardhat.config.js +++ b/packages/hardhat-chai-matchers/test/fixture-projects/hardhat-project/hardhat.config.js @@ -3,6 +3,9 @@ require("@nomiclabs/hardhat-ethers"); module.exports = { solidity: "0.8.4", networks: { + hardhat: { + chainId: Number(process.env.CHAIN_ID ?? "31337"), + }, localhost: { url: `http://127.0.0.1:${process.env.HARDHAT_NODE_PORT}`, }, diff --git a/packages/hardhat-chai-matchers/test/helpers.ts b/packages/hardhat-chai-matchers/test/helpers.ts index 1aa41438f0..de2dc7e889 100644 --- a/packages/hardhat-chai-matchers/test/helpers.ts +++ b/packages/hardhat-chai-matchers/test/helpers.ts @@ -118,8 +118,8 @@ export async function runSuccessfulAsserts({ }) { await successfulAssert(matchers[method](...args)); await successfulAssert(matchers[`${method}View`](...args)); - await successfulAssert(matchers.estimateGas[method](...args)); - await successfulAssert(matchers.callStatic[method](...args)); + await successfulAssert(matchers[method].estimateGas(...args)); + await successfulAssert(matchers[method].staticCall(...args)); } /** @@ -147,9 +147,9 @@ export async function runFailedAsserts({ failedAssert(matchers[`${method}View`](...args)) ).to.be.rejectedWith(AssertionError, failedAssertReason); await expect( - failedAssert(matchers.estimateGas[method](...args)) + failedAssert(matchers[method].estimateGas(...args)) ).to.be.rejectedWith(AssertionError, failedAssertReason); await expect( - failedAssert(matchers.callStatic[method](...args)) + failedAssert(matchers[method].staticCall(...args)) ).to.be.rejectedWith(AssertionError, failedAssertReason); } diff --git a/packages/hardhat-chai-matchers/test/panic.ts b/packages/hardhat-chai-matchers/test/panic.ts index 1f19ebe993..efb6549161 100644 --- a/packages/hardhat-chai-matchers/test/panic.ts +++ b/packages/hardhat-chai-matchers/test/panic.ts @@ -1,5 +1,5 @@ import { assert } from "chai"; -import { BigNumber } from "ethers"; +import { toBigInt } from "ethers"; import { PANIC_CODES, @@ -9,7 +9,7 @@ import { describe("panic codes", function () { it("all exported panic codes should have a description", async function () { for (const [key, code] of Object.entries(PANIC_CODES)) { - const description = panicErrorCodeToReason(BigNumber.from(code)); + const description = panicErrorCodeToReason(toBigInt(code)); assert.isDefined(description, `No description for panic code ${key}`); } }); diff --git a/packages/hardhat-chai-matchers/test/reverted/reverted.ts b/packages/hardhat-chai-matchers/test/reverted/reverted.ts index 18ab3b12dc..4d7a378536 100644 --- a/packages/hardhat-chai-matchers/test/reverted/reverted.ts +++ b/packages/hardhat-chai-matchers/test/reverted/reverted.ts @@ -11,6 +11,7 @@ import { } from "../helpers"; import "../../src/internal/add-chai-matchers"; +import { MatchersContract } from "../contracts"; describe("INTEGRATION: Reverted", function () { describe("with the in-process hardhat network", function () { @@ -27,9 +28,12 @@ describe("INTEGRATION: Reverted", function () { function runTests() { // deploy Matchers contract before each test - let matchers: any; + let matchers: MatchersContract; beforeEach("deploy matchers contract", async function () { - const Matchers = await this.hre.ethers.getContractFactory("Matchers"); + const Matchers = await this.hre.ethers.getContractFactory< + [], + MatchersContract + >("Matchers"); matchers = await Matchers.deploy(); }); @@ -189,9 +193,9 @@ describe("INTEGRATION: Reverted", function () { it("TxReceipt of a reverted transaction", async function () { const tx = await mineRevertedTransaction(this.hre); - const receipt = await this.hre.ethers.provider.waitForTransaction( + const receipt = await this.hre.ethers.provider.getTransactionReceipt( tx.hash - ); // tx.wait rejects, so we use provider.waitForTransaction + ); // tx.wait rejects, so we use provider.getTransactionReceipt await expect(receipt).to.be.reverted; await expectAssertionError( @@ -213,9 +217,9 @@ describe("INTEGRATION: Reverted", function () { it("promise of a TxReceipt of a reverted transaction", async function () { const tx = await mineRevertedTransaction(this.hre); - const receiptPromise = this.hre.ethers.provider.waitForTransaction( + const receiptPromise = this.hre.ethers.provider.getTransactionReceipt( tx.hash - ); // tx.wait rejects, so we use provider.waitForTransaction + ); // tx.wait rejects, so we use provider.getTransactionReceipt await expect(receiptPromise).to.be.reverted; await expectAssertionError( @@ -352,12 +356,15 @@ describe("INTEGRATION: Reverted", function () { randomPrivateKey, this.hre.ethers.provider ); + const matchersFromSenderWithoutFunds = matchers.connect( + signer + ) as MatchersContract; // this transaction will fail because of lack of funds, not because of a // revert await expect( expect( - matchers.connect(signer).revertsWithoutReason({ + matchersFromSenderWithoutFunds.revertsWithoutReason({ gasLimit: 1_000_000, }) ).to.not.be.reverted @@ -374,7 +381,9 @@ describe("INTEGRATION: Reverted", function () { try { await expect(matchers.succeeds()).to.be.reverted; } catch (e: any) { - expect(util.inspect(e)).to.include( + const errorString = util.inspect(e); + expect(errorString).to.include("Expected transaction to be reverted"); + expect(errorString).to.include( path.join("test", "reverted", "reverted.ts") ); diff --git a/packages/hardhat-chai-matchers/test/reverted/revertedWith.ts b/packages/hardhat-chai-matchers/test/reverted/revertedWith.ts index ca4257c6d7..02ffe4298c 100644 --- a/packages/hardhat-chai-matchers/test/reverted/revertedWith.ts +++ b/packages/hardhat-chai-matchers/test/reverted/revertedWith.ts @@ -11,6 +11,7 @@ import { } from "../helpers"; import "../../src/internal/add-chai-matchers"; +import { MatchersContract } from "../contracts"; describe("INTEGRATION: Reverted with", function () { describe("with the in-process hardhat network", function () { @@ -27,10 +28,13 @@ describe("INTEGRATION: Reverted with", function () { function runTests() { // deploy Matchers contract before each test - let matchers: any; + let matchers: MatchersContract; beforeEach("deploy matchers contract", async function () { - const Matchers = await this.hre.ethers.getContractFactory("Matchers"); + const Matchers = await this.hre.ethers.getContractFactory< + [], + MatchersContract + >("Matchers"); matchers = await Matchers.deploy(); }); @@ -213,6 +217,18 @@ describe("INTEGRATION: Reverted with", function () { ); }); + it("non-string as expectation, subject is a rejected promise", async function () { + const tx = matchers.revertsWithoutReason(); + + expect(() => + // @ts-expect-error + expect(tx).to.be.revertedWith(10) + ).to.throw( + TypeError, + "Expected the revert reason to be a string or a regular expression" + ); + }); + it("errors that are not related to a reverted transaction", async function () { // use an address that almost surely doesn't have balance const randomPrivateKey = @@ -221,12 +237,15 @@ describe("INTEGRATION: Reverted with", function () { randomPrivateKey, this.hre.ethers.provider ); + const matchersFromSenderWithoutFunds = matchers.connect( + signer + ) as MatchersContract; // this transaction will fail because of lack of funds, not because of a // revert await expect( expect( - matchers.connect(signer).revertsWithoutReason({ + matchersFromSenderWithoutFunds.revertsWithoutReason({ gasLimit: 1_000_000, }) ).to.not.be.revertedWith("some reason") @@ -243,7 +262,11 @@ describe("INTEGRATION: Reverted with", function () { try { await expect(matchers.revertsWith("bar")).to.be.revertedWith("foo"); } catch (e: any) { - expect(util.inspect(e)).to.include( + const errorString = util.inspect(e); + expect(errorString).to.include( + "Expected transaction to be reverted with reason 'foo', but it reverted with reason 'bar'" + ); + expect(errorString).to.include( path.join("test", "reverted", "revertedWith.ts") ); diff --git a/packages/hardhat-chai-matchers/test/reverted/revertedWithCustomError.ts b/packages/hardhat-chai-matchers/test/reverted/revertedWithCustomError.ts index e5a9d2a037..7df2848cd3 100644 --- a/packages/hardhat-chai-matchers/test/reverted/revertedWithCustomError.ts +++ b/packages/hardhat-chai-matchers/test/reverted/revertedWithCustomError.ts @@ -1,5 +1,4 @@ import { AssertionError, expect } from "chai"; -import { BigNumber } from "ethers"; import { ProviderError } from "hardhat/internal/core/providers/errors"; import path from "path"; import util from "util"; @@ -13,6 +12,7 @@ import { import "../../src/internal/add-chai-matchers"; import { anyUint, anyValue } from "../../src/withArgs"; +import { MatchersContract } from "../contracts"; describe("INTEGRATION: Reverted with custom error", function () { describe("with the in-process hardhat network", function () { @@ -29,9 +29,12 @@ describe("INTEGRATION: Reverted with custom error", function () { function runTests() { // deploy Matchers contract before each test - let matchers: any; + let matchers: MatchersContract; beforeEach("deploy matchers contract", async function () { - const Matchers = await this.hre.ethers.getContractFactory("Matchers"); + const Matchers = await this.hre.ethers.getContractFactory< + [], + MatchersContract + >("Matchers"); matchers = await Matchers.deploy(); }); @@ -368,20 +371,6 @@ describe("INTEGRATION: Reverted with custom error", function () { ); }); - it("should work with bigints and bignumbers", async function () { - await expect(matchers.revertWithCustomErrorWithUint(1)) - .to.be.revertedWithCustomError(matchers, "CustomErrorWithUint") - .withArgs(BigInt(1)); - - await expect(matchers.revertWithCustomErrorWithUint(1)) - .to.be.revertedWithCustomError(matchers, "CustomErrorWithUint") - .withArgs(BigNumber.from(1)); - - await expect(matchers.revertWithCustomErrorWithPair(1, 2)) - .to.be.revertedWithCustomError(matchers, "CustomErrorWithPair") - .withArgs([BigInt(1), BigNumber.from(2)]); - }); - it("should work with predicates", async function () { await expect(matchers.revertWithCustomErrorWithUint(1)) .to.be.revertedWithCustomError(matchers, "CustomErrorWithUint") @@ -460,12 +449,15 @@ describe("INTEGRATION: Reverted with custom error", function () { randomPrivateKey, this.hre.ethers.provider ); + const matchersFromSenderWithoutFunds = matchers.connect( + signer + ) as MatchersContract; // this transaction will fail because of lack of funds, not because of a // revert await expect( expect( - matchers.connect(signer).revertsWithoutReason({ + matchersFromSenderWithoutFunds.revertsWithoutReason({ gasLimit: 1_000_000, }) ).to.not.be.revertedWithCustomError(matchers, "SomeCustomError") @@ -481,10 +473,14 @@ describe("INTEGRATION: Reverted with custom error", function () { it("includes test file", async function () { try { await expect( - matchers.revertedWith("some reason") + matchers.revertsWith("some reason") ).to.be.revertedWithCustomError(matchers, "SomeCustomError"); } catch (e: any) { - expect(util.inspect(e)).to.include( + const errorString = util.inspect(e); + expect(errorString).to.include( + "Expected transaction to be reverted with custom error 'SomeCustomError', but it reverted with reason 'some reason'" + ); + expect(errorString).to.include( path.join("test", "reverted", "revertedWithCustomError.ts") ); diff --git a/packages/hardhat-chai-matchers/test/reverted/revertedWithPanic.ts b/packages/hardhat-chai-matchers/test/reverted/revertedWithPanic.ts index 82c3071dc7..0ca7e47a0d 100644 --- a/packages/hardhat-chai-matchers/test/reverted/revertedWithPanic.ts +++ b/packages/hardhat-chai-matchers/test/reverted/revertedWithPanic.ts @@ -1,11 +1,11 @@ import { AssertionError, expect } from "chai"; -import { BigNumber } from "ethers"; import { ProviderError } from "hardhat/internal/core/providers/errors"; import path from "path"; import util from "util"; import "../../src/internal/add-chai-matchers"; import { PANIC_CODES } from "../../src/panic"; +import { MatchersContract } from "../contracts"; import { runSuccessfulAsserts, runFailedAsserts, @@ -28,9 +28,12 @@ describe("INTEGRATION: Reverted with panic", function () { function runTests() { // deploy Matchers contract before each test - let matchers: any; + let matchers: MatchersContract; beforeEach("deploy matchers contract", async function () { - const Matchers = await this.hre.ethers.getContractFactory("Matchers"); + const Matchers = await this.hre.ethers.getContractFactory< + [], + MatchersContract + >("Matchers"); matchers = await Matchers.deploy(); }); @@ -269,15 +272,6 @@ describe("INTEGRATION: Reverted with panic", function () { successfulAssert: (x) => expect(x).not.to.be.revertedWithPanic("1"), }); }); - - it("ethers's BigNumber", async function () { - await runSuccessfulAsserts({ - matchers, - method: "succeeds", - successfulAssert: (x) => - expect(x).not.to.be.revertedWithPanic(BigNumber.from(1)), - }); - }); }); describe("invalid values", function () { @@ -296,6 +290,15 @@ describe("INTEGRATION: Reverted with panic", function () { ); }); + it("non-number as expectation, subject is a rejected promise", async function () { + const tx = matchers.revertsWithoutReason(); + + expect(() => expect(tx).to.be.revertedWithPanic("invalid")).to.throw( + TypeError, + "Expected the given panic code to be a number-like value, but got 'invalid'" + ); + }); + it("errors that are not related to a reverted transaction", async function () { // use an address that almost surely doesn't have balance const randomPrivateKey = @@ -304,12 +307,15 @@ describe("INTEGRATION: Reverted with panic", function () { randomPrivateKey, this.hre.ethers.provider ); + const matchersFromSenderWithoutFunds = matchers.connect( + signer + ) as MatchersContract; // this transaction will fail because of lack of funds, not because of a // revert await expect( expect( - matchers.connect(signer).revertsWithoutReason({ + matchersFromSenderWithoutFunds.revertsWithoutReason({ gasLimit: 1_000_000, }) ).to.not.be.revertedWithPanic() @@ -326,7 +332,11 @@ describe("INTEGRATION: Reverted with panic", function () { try { await expect(matchers.panicAssert()).to.not.be.revertedWithPanic(); } catch (e: any) { - expect(util.inspect(e)).to.include( + const errorString = util.inspect(e); + expect(errorString).to.include( + "Expected transaction NOT to be reverted with some panic code, but it reverted with panic code 0x01 (Assertion error)" + ); + expect(errorString).to.include( path.join("test", "reverted", "revertedWithPanic.ts") ); diff --git a/packages/hardhat-chai-matchers/test/reverted/revertedWithoutReason.ts b/packages/hardhat-chai-matchers/test/reverted/revertedWithoutReason.ts index 3fa8f7f425..8fcb3fd263 100644 --- a/packages/hardhat-chai-matchers/test/reverted/revertedWithoutReason.ts +++ b/packages/hardhat-chai-matchers/test/reverted/revertedWithoutReason.ts @@ -11,6 +11,7 @@ import { } from "../helpers"; import "../../src/internal/add-chai-matchers"; +import { MatchersContract } from "../contracts"; describe("INTEGRATION: Reverted without reason", function () { describe("with the in-process hardhat network", function () { @@ -27,9 +28,12 @@ describe("INTEGRATION: Reverted without reason", function () { function runTests() { // deploy Matchers contract before each test - let matchers: any; + let matchers: MatchersContract; beforeEach("deploy matchers contract", async function () { - const Matchers = await this.hre.ethers.getContractFactory("Matchers"); + const Matchers = await this.hre.ethers.getContractFactory< + [], + MatchersContract + >("Matchers"); matchers = await Matchers.deploy(); }); @@ -155,12 +159,15 @@ describe("INTEGRATION: Reverted without reason", function () { randomPrivateKey, this.hre.ethers.provider ); + const matchersFromSenderWithoutFunds = matchers.connect( + signer + ) as MatchersContract; // this transaction will fail because of lack of funds, not because of a // revert await expect( expect( - matchers.connect(signer).revertsWithoutReason({ + matchersFromSenderWithoutFunds.revertsWithoutReason({ gasLimit: 1_000_000, }) ).to.not.be.revertedWithoutReason() @@ -179,7 +186,11 @@ describe("INTEGRATION: Reverted without reason", function () { matchers.revertsWithoutReason() ).to.not.be.revertedWithoutReason(); } catch (e: any) { - expect(util.inspect(e)).to.include( + const errorString = util.inspect(e); + expect(errorString).to.include( + "Expected transaction NOT to be reverted without a reason, but it was" + ); + expect(errorString).to.include( path.join("test", "reverted", "revertedWithoutReason.ts") ); diff --git a/packages/hardhat-ethers/.eslintrc.js b/packages/hardhat-ethers/.eslintrc.js index 44ed8ed6d5..7f3a838a47 100644 --- a/packages/hardhat-ethers/.eslintrc.js +++ b/packages/hardhat-ethers/.eslintrc.js @@ -4,4 +4,7 @@ module.exports = { project: `${__dirname}/src/tsconfig.json`, sourceType: "module", }, + rules: { + "@typescript-eslint/no-non-null-assertion": "error" + } }; diff --git a/packages/hardhat-ethers/package.json b/packages/hardhat-ethers/package.json index 2e315938a1..eb6b0dbad2 100644 --- a/packages/hardhat-ethers/package.json +++ b/packages/hardhat-ethers/package.json @@ -1,6 +1,6 @@ { "name": "@nomiclabs/hardhat-ethers", - "version": "2.2.3", + "version": "3.0.0", "description": "Hardhat plugin for ethers", "homepage": "https://github.com/nomiclabs/hardhat/tree/main/packages/hardhat-ethers", "repository": "github:nomiclabs/hardhat", @@ -51,7 +51,7 @@ "eslint-plugin-import": "2.24.1", "eslint-plugin-no-only-tests": "3.0.0", "eslint-plugin-prettier": "3.4.0", - "ethers": "^5.0.0", + "ethers": "^6.1.0", "hardhat": "^2.0.0", "mocha": "^10.0.0", "prettier": "2.4.1", @@ -60,7 +60,7 @@ "typescript": "~4.7.4" }, "peerDependencies": { - "ethers": "^5.0.0", + "ethers": "^6.1.0", "hardhat": "^2.0.0" } } diff --git a/packages/hardhat-ethers/src/dist/src/signer-with-address.ts b/packages/hardhat-ethers/src/dist/src/signer-with-address.ts index 51cc2d2bfc..dbdd5440d3 100644 --- a/packages/hardhat-ethers/src/dist/src/signer-with-address.ts +++ b/packages/hardhat-ethers/src/dist/src/signer-with-address.ts @@ -1 +1 @@ -export { SignerWithAddress } from "../../signers"; +export { CustomEthersSigner as SignerWithAddress } from "../../signers"; diff --git a/packages/hardhat-ethers/src/internal/custom-ethers-provider.ts b/packages/hardhat-ethers/src/internal/custom-ethers-provider.ts new file mode 100644 index 0000000000..c7f97b5b83 --- /dev/null +++ b/packages/hardhat-ethers/src/internal/custom-ethers-provider.ts @@ -0,0 +1,683 @@ +import type { AddressLike } from "ethers/types/address"; +import type { + BlockTag, + TransactionRequest, + Filter, + FilterByBlockHash, + ProviderEvent, + PerformActionTransaction, + TransactionResponseParams, + BlockParams, + TransactionReceiptParams, + LogParams, + PerformActionFilter, +} from "ethers/types/providers"; +import type { Listener } from "ethers/types/utils"; + +import { + Block, + FeeData, + Log, + Network as EthersNetwork, + Transaction, + TransactionReceipt, + TransactionResponse, + ethers, + getBigInt, + isHexString, + resolveAddress, + toQuantity, +} from "ethers"; +import { EthereumProvider } from "hardhat/types"; +import { CustomEthersSigner } from "../signers"; +import { + copyRequest, + formatBlock, + formatLog, + formatTransactionReceipt, + formatTransactionResponse, + getRpcTransaction, +} from "./ethers-utils"; +import { + AccountIndexOutOfRange, + BroadcastedTxDifferentHash, + HardhatEthersError, + NonStringEventError, + NotImplementedError, +} from "./errors"; + +export class CustomEthersProvider implements ethers.Provider { + constructor( + private readonly _hardhatProvider: EthereumProvider, + private readonly _networkName: string + ) {} + + public get provider(): this { + return this; + } + + public destroy() {} + + public async send(method: string, params?: any[]): Promise { + return this._hardhatProvider.send(method, params); + } + + public async getSigner( + address?: number | string + ): Promise { + if (address === null || address === undefined) { + address = 0; + } + + const accountsPromise = this.send("eth_accounts", []); + + // Account index + if (typeof address === "number") { + const accounts: string[] = await accountsPromise; + if (address >= accounts.length) { + throw new AccountIndexOutOfRange(address, accounts.length); + } + return CustomEthersSigner.create(this, accounts[address]); + } + + if (typeof address === "string") { + return CustomEthersSigner.create(this, address); + } + + throw new HardhatEthersError(`Couldn't get account ${address as any}`); + } + + public async getBlockNumber(): Promise { + const blockNumber = await this._hardhatProvider.send("eth_blockNumber"); + + return Number(blockNumber); + } + + public async getNetwork(): Promise { + const chainId = await this._hardhatProvider.send("eth_chainId"); + return new EthersNetwork(this._networkName, Number(chainId)); + } + + public async getFeeData(): Promise { + let gasPrice: bigint | undefined; + let maxFeePerGas: bigint | undefined; + let maxPriorityFeePerGas: bigint | undefined; + + try { + gasPrice = BigInt(await this._hardhatProvider.send("eth_gasPrice")); + } catch {} + + const latestBlock = await this.getBlock("latest"); + const baseFeePerGas = latestBlock?.baseFeePerGas; + if (baseFeePerGas !== undefined && baseFeePerGas !== null) { + maxPriorityFeePerGas = 1_000_000_000n; + maxFeePerGas = 2n * baseFeePerGas + maxPriorityFeePerGas; + } + + return new FeeData(gasPrice, maxFeePerGas, maxPriorityFeePerGas); + } + + public async getBalance( + address: AddressLike, + blockTag?: BlockTag | undefined + ): Promise { + const resolvedAddress = await this._getAddress(address); + const resolvedBlockTag = await this._getBlockTag(blockTag); + const rpcBlockTag = this._getRpcBlockTag(resolvedBlockTag); + + const balance = await this._hardhatProvider.send("eth_getBalance", [ + resolvedAddress, + rpcBlockTag, + ]); + + return BigInt(balance); + } + + public async getTransactionCount( + address: AddressLike, + blockTag?: BlockTag | undefined + ): Promise { + const resolvedAddress = await this._getAddress(address); + const resolvedBlockTag = await this._getBlockTag(blockTag); + const rpcBlockTag = this._getRpcBlockTag(resolvedBlockTag); + + const transactionCount = await this._hardhatProvider.send( + "eth_getTransactionCount", + [resolvedAddress, rpcBlockTag] + ); + + return Number(transactionCount); + } + + public async getCode( + address: AddressLike, + blockTag?: BlockTag | undefined + ): Promise { + const resolvedAddress = await this._getAddress(address); + const resolvedBlockTag = await this._getBlockTag(blockTag); + const rpcBlockTag = this._getRpcBlockTag(resolvedBlockTag); + + return this._hardhatProvider.send("eth_getCode", [ + resolvedAddress, + rpcBlockTag, + ]); + } + + public async getStorage( + address: AddressLike, + position: ethers.BigNumberish, + blockTag?: BlockTag | undefined + ): Promise { + const resolvedAddress = await this._getAddress(address); + const resolvedPosition = getBigInt(position, "position"); + const resolvedBlockTag = await this._getBlockTag(blockTag); + const rpcBlockTag = this._getRpcBlockTag(resolvedBlockTag); + + return this._hardhatProvider.send("eth_getStorageAt", [ + resolvedAddress, + `0x${resolvedPosition.toString(16)}`, + rpcBlockTag, + ]); + } + + public async estimateGas(tx: TransactionRequest): Promise { + const blockTag = + tx.blockTag === undefined ? "pending" : this._getBlockTag(tx.blockTag); + const [resolvedTx, resolvedBlockTag] = await Promise.all([ + this._getTransactionRequest(tx), + blockTag, + ]); + + const rpcTransaction = getRpcTransaction(resolvedTx); + const rpcBlockTag = this._getRpcBlockTag(resolvedBlockTag); + + const gasEstimation = await this._hardhatProvider.send("eth_estimateGas", [ + rpcTransaction, + rpcBlockTag, + ]); + + return BigInt(gasEstimation); + } + + public async call(tx: TransactionRequest): Promise { + const [resolvedTx, resolvedBlockTag] = await Promise.all([ + this._getTransactionRequest(tx), + this._getBlockTag(tx.blockTag), + ]); + const rpcTransaction = getRpcTransaction(resolvedTx); + const rpcBlockTag = this._getRpcBlockTag(resolvedBlockTag); + + return this._hardhatProvider.send("eth_call", [ + rpcTransaction, + rpcBlockTag, + ]); + } + + public async broadcastTransaction( + signedTx: string + ): Promise { + const hashPromise = this._hardhatProvider.send("eth_sendRawTransaction", [ + signedTx, + ]); + + const [hash, blockNumber] = await Promise.all([ + hashPromise, + this.getBlockNumber(), + ]); + + const tx = Transaction.from(signedTx); + if (tx.hash === null) { + throw new HardhatEthersError( + "Assertion error: hash of signed tx shouldn't be null" + ); + } + + if (tx.hash !== hash) { + throw new BroadcastedTxDifferentHash(tx.hash, hash); + } + + return this._wrapTransactionResponse(tx as any).replaceableTransaction( + blockNumber + ); + } + + public async getBlock( + blockHashOrBlockTag: BlockTag, + prefetchTxs?: boolean | undefined + ): Promise { + const block = await this._getBlock( + blockHashOrBlockTag, + prefetchTxs ?? false + ); + + // eslint-disable-next-line eqeqeq + if (block == null) { + return null; + } + + return this._wrapBlock(block); + } + + public async getTransaction( + hash: string + ): Promise { + const transaction = await this._hardhatProvider.send( + "eth_getTransactionByHash", + [hash] + ); + + // eslint-disable-next-line eqeqeq + if (transaction == null) { + return null; + } + + return this._wrapTransactionResponse( + formatTransactionResponse(transaction) + ); + } + + public async getTransactionReceipt( + hash: string + ): Promise { + const receipt = await this._hardhatProvider.send( + "eth_getTransactionReceipt", + [hash] + ); + + // eslint-disable-next-line eqeqeq + if (receipt == null) { + return null; + } + + return this._wrapTransactionReceipt(receipt); + } + + public async getTransactionResult(_hash: string): Promise { + throw new NotImplementedError("CustomEthersProvider.getTransactionResult"); + } + + public async getLogs( + filter: Filter | FilterByBlockHash + ): Promise { + const resolvedFilter = await this._getFilter(filter); + + const logs = await this._hardhatProvider.send("eth_getLogs", [ + resolvedFilter, + ]); + + return logs.map((log: any) => this._wrapLog(formatLog(log))); + } + + public async resolveName(_ensName: string): Promise { + throw new NotImplementedError("CustomEthersProvider.resolveName"); + } + + public async lookupAddress(_address: string): Promise { + throw new NotImplementedError("CustomEthersProvider.lookupAddress"); + } + + public async waitForTransaction( + _hash: string, + _confirms?: number | undefined, + _timeout?: number | undefined + ): Promise { + throw new NotImplementedError("CustomEthersProvider.waitForTransaction"); + } + + public async waitForBlock( + _blockTag?: BlockTag | undefined + ): Promise { + throw new NotImplementedError("CustomEthersProvider.waitForBlock"); + } + + public async on(event: ProviderEvent, listener: Listener): Promise { + if (typeof event === "string") { + this._hardhatProvider.on(event, listener); + } else { + throw new NonStringEventError("on", event); + } + + return this; + } + + public async once(event: ProviderEvent, listener: Listener): Promise { + if (typeof event === "string") { + this._hardhatProvider.once(event, listener); + } else { + throw new NonStringEventError("once", event); + } + + return this; + } + + public async emit(event: ProviderEvent, ...args: any[]): Promise { + if (typeof event === "string") { + return this._hardhatProvider.emit(event, ...args); + } else { + throw new NonStringEventError("emit", event); + } + } + + public async listenerCount( + event?: ProviderEvent | undefined + ): Promise { + if (typeof event === "string") { + return this._hardhatProvider.listenerCount(event); + } else { + throw new NonStringEventError("listenerCount", event); + } + } + + public async listeners( + event?: ProviderEvent | undefined + ): Promise { + if (typeof event === "string") { + return this._hardhatProvider.listeners(event) as any; + } else { + throw new NonStringEventError("listeners", event); + } + } + + public async off( + event: ProviderEvent, + listener?: Listener | undefined + ): Promise { + if (typeof event === "string" && listener !== undefined) { + this._hardhatProvider.off(event, listener); + } else { + throw new NonStringEventError("off", event); + } + + return this; + } + + public async removeAllListeners( + event?: ProviderEvent | undefined + ): Promise { + if (event === undefined || typeof event === "string") { + this._hardhatProvider.removeAllListeners(event); + } else { + throw new NonStringEventError("removeAllListeners", event); + } + + return this; + } + + public async addListener( + event: ProviderEvent, + listener: Listener + ): Promise { + if (typeof event === "string") { + this._hardhatProvider.addListener(event, listener); + } else { + throw new NonStringEventError("addListener", event); + } + + return this; + } + + public async removeListener( + event: ProviderEvent, + listener: Listener + ): Promise { + if (typeof event === "string") { + this._hardhatProvider.removeListener(event, listener); + } else { + throw new NonStringEventError("removeListener", event); + } + + return this; + } + + public toJSON() { + return ""; + } + + private _getAddress(address: AddressLike): string | Promise { + return resolveAddress(address, this); + } + + private _getBlockTag(blockTag?: BlockTag): string | Promise { + // eslint-disable-next-line eqeqeq + if (blockTag == null) { + return "latest"; + } + + switch (blockTag) { + case "earliest": + return "0x0"; + case "latest": + case "pending": + case "safe": + case "finalized": + return blockTag; + } + + if (isHexString(blockTag)) { + if (isHexString(blockTag, 32)) { + return blockTag; + } + return toQuantity(blockTag); + } + + if (typeof blockTag === "number") { + if (blockTag >= 0) { + return toQuantity(blockTag); + } + return this.getBlockNumber().then((b) => toQuantity(b + blockTag)); + } + + throw new HardhatEthersError(`Invalid blockTag: ${blockTag}`); + } + + private _getTransactionRequest( + _request: TransactionRequest + ): PerformActionTransaction | Promise { + const request = copyRequest(_request) as PerformActionTransaction; + + const promises: Array> = []; + ["to", "from"].forEach((key) => { + if ( + (request as any)[key] === null || + (request as any)[key] === undefined + ) { + return; + } + + const addr = resolveAddress((request as any)[key]); + if (isPromise(addr)) { + promises.push( + (async function () { + (request as any)[key] = await addr; + })() + ); + } else { + (request as any)[key] = addr; + } + }); + + if (request.blockTag !== null && request.blockTag !== undefined) { + const blockTag = this._getBlockTag(request.blockTag); + if (isPromise(blockTag)) { + promises.push( + (async function () { + request.blockTag = await blockTag; + })() + ); + } else { + request.blockTag = blockTag; + } + } + + if (promises.length > 0) { + return (async function () { + await Promise.all(promises); + return request; + })(); + } + + return request; + } + + private _wrapTransactionResponse( + tx: TransactionResponseParams + ): TransactionResponse { + return new TransactionResponse(tx, this); + } + + private async _getBlock( + block: BlockTag | string, + includeTransactions: boolean + ): Promise { + if (isHexString(block, 32)) { + return this._hardhatProvider.send("eth_getBlockByHash", [ + block, + includeTransactions, + ]); + } + + let blockTag = this._getBlockTag(block); + if (typeof blockTag !== "string") { + blockTag = await blockTag; + } + + return this._hardhatProvider.send("eth_getBlockByNumber", [ + blockTag, + includeTransactions, + ]); + } + + private _wrapBlock(value: BlockParams): Block { + return new Block(formatBlock(value), this); + } + + private _wrapTransactionReceipt( + value: TransactionReceiptParams + ): TransactionReceipt { + return new TransactionReceipt(formatTransactionReceipt(value), this); + } + + private _getFilter( + filter: Filter | FilterByBlockHash + ): PerformActionFilter | Promise { + // Create a canonical representation of the topics + const topics = (filter.topics ?? []).map((topic) => { + // eslint-disable-next-line eqeqeq + if (topic == null) { + return null; + } + if (Array.isArray(topic)) { + return concisify(topic.map((t) => t.toLowerCase())); + } + return topic.toLowerCase(); + }); + + const blockHash = "blockHash" in filter ? filter.blockHash : undefined; + + const resolve = ( + _address: string[], + fromBlock?: string, + toBlock?: string + ) => { + let resolvedAddress: undefined | string | string[]; + switch (_address.length) { + case 0: + break; + case 1: + resolvedAddress = _address[0]; + break; + default: + _address.sort(); + resolvedAddress = _address; + } + + if (blockHash !== undefined) { + // eslint-disable-next-line eqeqeq + if (fromBlock != null || toBlock != null) { + throw new HardhatEthersError("invalid filter"); + } + } + + const resolvedFilter: any = {}; + if (resolvedAddress !== undefined) { + resolvedFilter.address = resolvedAddress; + } + if (topics.length > 0) { + resolvedFilter.topics = topics; + } + if (fromBlock !== undefined) { + resolvedFilter.fromBlock = fromBlock; + } + if (toBlock !== undefined) { + resolvedFilter.toBlock = toBlock; + } + if (blockHash !== undefined) { + resolvedFilter.blockHash = blockHash; + } + + return resolvedFilter; + }; + + // Addresses could be async (ENS names or Addressables) + const address: Array> = []; + if (filter.address !== undefined) { + if (Array.isArray(filter.address)) { + for (const addr of filter.address) { + address.push(this._getAddress(addr)); + } + } else { + address.push(this._getAddress(filter.address)); + } + } + + let resolvedFromBlock: undefined | string | Promise; + if ("fromBlock" in filter) { + resolvedFromBlock = this._getBlockTag(filter.fromBlock); + } + + let resolvedToBlock: undefined | string | Promise; + if ("toBlock" in filter) { + resolvedToBlock = this._getBlockTag(filter.toBlock); + } + + if ( + address.filter((a) => typeof a !== "string").length > 0 || + // eslint-disable-next-line eqeqeq + (resolvedFromBlock != null && typeof resolvedFromBlock !== "string") || + // eslint-disable-next-line eqeqeq + (resolvedToBlock != null && typeof resolvedToBlock !== "string") + ) { + return Promise.all([ + Promise.all(address), + resolvedFromBlock, + resolvedToBlock, + ]).then((result) => { + return resolve(result[0], result[1], result[2]); + }); + } + + return resolve(address as string[], resolvedFromBlock, resolvedToBlock); + } + + private _wrapLog(value: LogParams): Log { + return new Log(formatLog(value), this); + } + + private _getRpcBlockTag(blockTag: string): string | { blockHash: string } { + if (isHexString(blockTag, 32)) { + return { blockHash: blockTag }; + } + + return blockTag; + } +} + +function isPromise(value: any): value is Promise { + return Boolean(value) && typeof value.then === "function"; +} + +function concisify(items: string[]): string[] { + items = Array.from(new Set(items).values()); + items.sort(); + return items; +} diff --git a/packages/hardhat-ethers/src/internal/errors.ts b/packages/hardhat-ethers/src/internal/errors.ts new file mode 100644 index 0000000000..ce7f3a0233 --- /dev/null +++ b/packages/hardhat-ethers/src/internal/errors.ts @@ -0,0 +1,35 @@ +import { NomicLabsHardhatPluginError } from "hardhat/plugins"; + +export class HardhatEthersError extends NomicLabsHardhatPluginError { + constructor(message: string, parent?: Error) { + super("@nomiclabs/hardhat-ethers", message, parent); + } +} + +export class NotImplementedError extends HardhatEthersError { + constructor(method: string) { + super(`Method '${method}' is not implemented`); + } +} + +export class NonStringEventError extends HardhatEthersError { + constructor(method: string, event: any) { + super(`Method '${method}' only supports string events, got '${event}'`); + } +} + +export class AccountIndexOutOfRange extends HardhatEthersError { + constructor(accountIndex: number, accountsLength: number) { + super( + `Tried to get account with index ${accountIndex} but there are ${accountsLength} accounts` + ); + } +} + +export class BroadcastedTxDifferentHash extends HardhatEthersError { + constructor(txHash: string, broadcastedTxHash: string) { + super( + `Expected broadcasted transaction to have hash '${txHash}', but got '${broadcastedTxHash}'` + ); + } +} diff --git a/packages/hardhat-ethers/src/internal/ethers-provider-wrapper.ts b/packages/hardhat-ethers/src/internal/ethers-provider-wrapper.ts deleted file mode 100644 index b0aa8c9afd..0000000000 --- a/packages/hardhat-ethers/src/internal/ethers-provider-wrapper.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ethers } from "ethers"; -import { EthereumProvider } from "hardhat/types"; - -export class EthersProviderWrapper extends ethers.providers.JsonRpcProvider { - private readonly _hardhatProvider: EthereumProvider; - - constructor(hardhatProvider: EthereumProvider) { - super(); - this._hardhatProvider = hardhatProvider; - } - - public async send(method: string, params: any): Promise { - const result = await this._hardhatProvider.send(method, params); - - // We replicate ethers' behavior. - this.emit("debug", { - action: "send", - request: { - id: 42, - jsonrpc: "2.0", - method, - params, - }, - response: result, - provider: this, - }); - - return result; - } - - public toJSON() { - return ""; - } -} diff --git a/packages/hardhat-ethers/src/internal/ethers-utils.ts b/packages/hardhat-ethers/src/internal/ethers-utils.ts new file mode 100644 index 0000000000..a94e452c12 --- /dev/null +++ b/packages/hardhat-ethers/src/internal/ethers-utils.ts @@ -0,0 +1,459 @@ +// these helpers functions were copied verbatim from ethers + +import type { + TransactionRequest, + PreparedTransactionRequest, + BlockParams, + TransactionResponseParams, + TransactionReceiptParams, + LogParams, + JsonRpcTransactionRequest, +} from "ethers/types/providers"; + +import { + accessListify, + assert, + assertArgument, + getAddress, + getBigInt, + getCreateAddress, + getNumber, + hexlify, + isHexString, + Signature, + toQuantity, +} from "ethers"; +import { HardhatEthersError } from "./errors"; + +export type FormatFunc = (value: any) => any; + +export function copyRequest( + req: TransactionRequest +): PreparedTransactionRequest { + const result: any = {}; + + // These could be addresses, ENS names or Addressables + if (req.to !== null && req.to !== undefined) { + result.to = req.to; + } + if (req.from !== null && req.from !== undefined) { + result.from = req.from; + } + + if (req.data !== null && req.data !== undefined) { + result.data = hexlify(req.data); + } + + const bigIntKeys = + "chainId,gasLimit,gasPrice,maxFeePerGas,maxPriorityFeePerGas,value".split( + /,/ + ); + for (const key of bigIntKeys) { + if ( + !(key in req) || + (req as any)[key] === null || + (req as any)[key] === undefined + ) { + continue; + } + result[key] = getBigInt((req as any)[key], `request.${key}`); + } + + const numberKeys = "type,nonce".split(/,/); + for (const key of numberKeys) { + if ( + !(key in req) || + (req as any)[key] === null || + (req as any)[key] === undefined + ) { + continue; + } + result[key] = getNumber((req as any)[key], `request.${key}`); + } + + if (req.accessList !== null && req.accessList !== undefined) { + result.accessList = accessListify(req.accessList); + } + + if ("blockTag" in req) { + result.blockTag = req.blockTag; + } + + if ("enableCcipRead" in req) { + result.enableCcipReadEnabled = Boolean(req.enableCcipRead); + } + + if ("customData" in req) { + result.customData = req.customData; + } + + return result; +} + +export async function resolveProperties(value: { + [P in keyof T]: T[P] | Promise; +}): Promise { + const keys = Object.keys(value); + const results = await Promise.all( + keys.map((k) => Promise.resolve(value[k as keyof T])) + ); + return results.reduce((accum: any, v, index) => { + accum[keys[index]] = v; + return accum; + }, {} as { [P in keyof T]: T[P] }); +} + +export function formatBlock(value: any): BlockParams { + const result = _formatBlock(value); + result.transactions = value.transactions.map( + (tx: string | TransactionResponseParams) => { + if (typeof tx === "string") { + return tx; + } + return formatTransactionResponse(tx); + } + ); + return result; +} + +const _formatBlock = object({ + hash: allowNull(formatHash), + parentHash: formatHash, + number: getNumber, + + timestamp: getNumber, + nonce: allowNull(formatData), + difficulty: getBigInt, + + gasLimit: getBigInt, + gasUsed: getBigInt, + + miner: allowNull(getAddress), + extraData: formatData, + + baseFeePerGas: allowNull(getBigInt), +}); + +function object( + format: Record, + altNames?: Record +): FormatFunc { + return (value: any) => { + const result: any = {}; + // eslint-disable-next-line guard-for-in + for (const key in format) { + let srcKey = key; + if (altNames !== undefined && key in altNames && !(srcKey in value)) { + for (const altKey of altNames[key]) { + if (altKey in value) { + srcKey = altKey; + break; + } + } + } + + try { + const nv = format[key](value[srcKey]); + if (nv !== undefined) { + result[key] = nv; + } + } catch (error) { + const message = error instanceof Error ? error.message : "not-an-error"; + assert( + false, + `invalid value for value.${key} (${message})`, + "BAD_DATA", + { value } + ); + } + } + return result; + }; +} + +function allowNull(format: FormatFunc, nullValue?: any): FormatFunc { + return function (value: any) { + // eslint-disable-next-line eqeqeq + if (value === null || value === undefined) { + return nullValue; + } + return format(value); + }; +} + +function formatHash(value: any): string { + assertArgument(isHexString(value, 32), "invalid hash", "value", value); + return value; +} + +function formatData(value: string): string { + assertArgument(isHexString(value, true), "invalid data", "value", value); + return value; +} + +export function formatTransactionResponse( + value: any +): TransactionResponseParams { + // Some clients (TestRPC) do strange things like return 0x0 for the + // 0 address; correct this to be a real address + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (value.to && getBigInt(value.to) === 0n) { + value.to = "0x0000000000000000000000000000000000000000"; + } + + const result = object( + { + hash: formatHash, + + type: (v: any) => { + // eslint-disable-next-line eqeqeq + if (v === "0x" || v == null) { + return 0; + } + return getNumber(v); + }, + accessList: allowNull(accessListify, null), + + blockHash: allowNull(formatHash, null), + blockNumber: allowNull(getNumber, null), + transactionIndex: allowNull(getNumber, null), + + from: getAddress, + + // either (gasPrice) or (maxPriorityFeePerGas + maxFeePerGas) must be set + gasPrice: allowNull(getBigInt), + maxPriorityFeePerGas: allowNull(getBigInt), + maxFeePerGas: allowNull(getBigInt), + + gasLimit: getBigInt, + to: allowNull(getAddress, null), + value: getBigInt, + nonce: getNumber, + data: formatData, + + creates: allowNull(getAddress, null), + + chainId: allowNull(getBigInt, null), + }, + { + data: ["input"], + gasLimit: ["gas"], + } + )(value); + + // If to and creates are empty, populate the creates from the value + // eslint-disable-next-line eqeqeq + if (result.to == null && result.creates == null) { + result.creates = getCreateAddress(result); + } + + // @TODO: Check fee data + + // Add an access list to supported transaction types + // eslint-disable-next-line eqeqeq + if ((value.type === 1 || value.type === 2) && value.accessList == null) { + result.accessList = []; + } + + // Compute the signature + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (value.signature) { + result.signature = Signature.from(value.signature); + } else { + result.signature = Signature.from(value); + } + + // Some backends omit ChainId on legacy transactions, but we can compute it + // eslint-disable-next-line eqeqeq + if (result.chainId == null) { + const chainId = result.signature.legacyChainId; + // eslint-disable-next-line eqeqeq + if (chainId != null) { + result.chainId = chainId; + } + } + + // @TODO: check chainID + /* + if (value.chainId != null) { + let chainId = value.chainId; + + if (isHexString(chainId)) { + chainId = BigNumber.from(chainId).toNumber(); + } + + result.chainId = chainId; + + } else { + let chainId = value.networkId; + + // geth-etc returns chainId + if (chainId == null && result.v == null) { + chainId = value.chainId; + } + + if (isHexString(chainId)) { + chainId = BigNumber.from(chainId).toNumber(); + } + + if (typeof(chainId) !== "number" && result.v != null) { + chainId = (result.v - 35) / 2; + if (chainId < 0) { chainId = 0; } + chainId = parseInt(chainId); + } + + if (typeof(chainId) !== "number") { chainId = 0; } + + result.chainId = chainId; + } + */ + + // 0x0000... should actually be null + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (result.blockHash && getBigInt(result.blockHash) === 0n) { + result.blockHash = null; + } + + return result; +} + +function arrayOf(format: FormatFunc): FormatFunc { + return (array: any) => { + if (!Array.isArray(array)) { + throw new HardhatEthersError("not an array"); + } + return array.map((i) => format(i)); + }; +} + +const _formatReceiptLog = object( + { + transactionIndex: getNumber, + blockNumber: getNumber, + transactionHash: formatHash, + address: getAddress, + topics: arrayOf(formatHash), + data: formatData, + index: getNumber, + blockHash: formatHash, + }, + { + index: ["logIndex"], + } +); + +const _formatTransactionReceipt = object( + { + to: allowNull(getAddress, null), + from: allowNull(getAddress, null), + contractAddress: allowNull(getAddress, null), + // should be allowNull(hash), but broken-EIP-658 support is handled in receipt + index: getNumber, + root: allowNull(hexlify), + gasUsed: getBigInt, + logsBloom: allowNull(formatData), + blockHash: formatHash, + hash: formatHash, + logs: arrayOf(formatReceiptLog), + blockNumber: getNumber, + cumulativeGasUsed: getBigInt, + effectiveGasPrice: allowNull(getBigInt), + status: allowNull(getNumber), + type: allowNull(getNumber, 0), + }, + { + effectiveGasPrice: ["gasPrice"], + hash: ["transactionHash"], + index: ["transactionIndex"], + } +); + +export function formatTransactionReceipt(value: any): TransactionReceiptParams { + return _formatTransactionReceipt(value); +} + +export function formatReceiptLog(value: any): LogParams { + return _formatReceiptLog(value); +} + +function formatBoolean(value: any): boolean { + switch (value) { + case true: + case "true": + return true; + case false: + case "false": + return false; + } + assertArgument( + false, + `invalid boolean; ${JSON.stringify(value)}`, + "value", + value + ); +} + +const _formatLog = object( + { + address: getAddress, + blockHash: formatHash, + blockNumber: getNumber, + data: formatData, + index: getNumber, + removed: formatBoolean, + topics: arrayOf(formatHash), + transactionHash: formatHash, + transactionIndex: getNumber, + }, + { + index: ["logIndex"], + } +); + +export function formatLog(value: any): LogParams { + return _formatLog(value); +} + +export function getRpcTransaction( + tx: TransactionRequest +): JsonRpcTransactionRequest { + const result: JsonRpcTransactionRequest = {}; + + // JSON-RPC now requires numeric values to be "quantity" values + [ + "chainId", + "gasLimit", + "gasPrice", + "type", + "maxFeePerGas", + "maxPriorityFeePerGas", + "nonce", + "value", + ].forEach((key) => { + if ((tx as any)[key] === null || (tx as any)[key] === undefined) { + return; + } + let dstKey = key; + if (key === "gasLimit") { + dstKey = "gas"; + } + (result as any)[dstKey] = toQuantity( + getBigInt((tx as any)[key], `tx.${key}`) + ); + }); + + // Make sure addresses and data are lowercase + ["from", "to", "data"].forEach((key) => { + if ((tx as any)[key] === null || (tx as any)[key] === undefined) { + return; + } + (result as any)[key] = hexlify((tx as any)[key]); + }); + + // Normalize the access list object + if (tx.accessList !== null && tx.accessList !== undefined) { + result.accessList = accessListify(tx.accessList); + } + + return result; +} diff --git a/packages/hardhat-ethers/src/internal/helpers.ts b/packages/hardhat-ethers/src/internal/helpers.ts index 3d9e95c0f8..c622e333da 100644 --- a/packages/hardhat-ethers/src/internal/helpers.ts +++ b/packages/hardhat-ethers/src/internal/helpers.ts @@ -1,13 +1,9 @@ -import type { ethers } from "ethers"; -import type { SignerWithAddress } from "../signers"; +import type { ethers as EthersT, BaseContract } from "ethers"; +import type { CustomEthersSigner } from "../signers"; import type { FactoryOptions, Libraries } from "../types"; -import { NomicLabsHardhatPluginError } from "hardhat/plugins"; -import { - Artifact, - HardhatRuntimeEnvironment, - NetworkConfig, -} from "hardhat/types"; +import { Artifact, HardhatRuntimeEnvironment } from "hardhat/types"; +import { HardhatEthersError } from "./errors"; interface Link { sourceName: string; @@ -15,8 +11,6 @@ interface Link { address: string; } -const pluginName = "hardhat-ethers"; - function isArtifact(artifact: any): artifact is Artifact { const { contractName, @@ -41,8 +35,8 @@ function isArtifact(artifact: any): artifact is Artifact { export async function getSigners( hre: HardhatRuntimeEnvironment -): Promise { - const accounts = await hre.ethers.provider.listAccounts(); +): Promise { + const accounts: string[] = await hre.ethers.provider.send("eth_accounts", []); const signersWithAddress = await Promise.all( accounts.map((account) => getSigner(hre, account)) @@ -54,14 +48,15 @@ export async function getSigners( export async function getSigner( hre: HardhatRuntimeEnvironment, address: string -): Promise { - const { SignerWithAddress: SignerWithAddressImpl } = await import( +): Promise { + const { CustomEthersSigner: SignerWithAddressImpl } = await import( "../signers" ); - const signer = hre.ethers.provider.getSigner(address); - - const signerWithAddress = await SignerWithAddressImpl.create(signer); + const signerWithAddress = await SignerWithAddressImpl.create( + hre.ethers.provider, + address + ); return signerWithAddress; } @@ -69,68 +64,76 @@ export async function getSigner( export async function getImpersonatedSigner( hre: HardhatRuntimeEnvironment, address: string -): Promise { +): Promise { await hre.ethers.provider.send("hardhat_impersonateAccount", [address]); return getSigner(hre, address); } -export function getContractFactory( +export function getContractFactory( hre: HardhatRuntimeEnvironment, name: string, - signerOrOptions?: ethers.Signer | FactoryOptions -): Promise; + signerOrOptions?: EthersT.Signer | FactoryOptions +): Promise>; -export function getContractFactory( +export function getContractFactory( hre: HardhatRuntimeEnvironment, abi: any[], - bytecode: ethers.utils.BytesLike, - signer?: ethers.Signer -): Promise; - -export async function getContractFactory( + bytecode: EthersT.BytesLike, + signer?: EthersT.Signer +): Promise>; + +export async function getContractFactory< + A extends any[] = any[], + I = BaseContract +>( hre: HardhatRuntimeEnvironment, nameOrAbi: string | any[], bytecodeOrFactoryOptions?: - | (ethers.Signer | FactoryOptions) - | ethers.utils.BytesLike, - signer?: ethers.Signer -) { + | (EthersT.Signer | FactoryOptions) + | EthersT.BytesLike, + signer?: EthersT.Signer +): Promise> { if (typeof nameOrAbi === "string") { const artifact = await hre.artifacts.readArtifact(nameOrAbi); - return getContractFactoryFromArtifact( + return getContractFactoryFromArtifact( hre, artifact, - bytecodeOrFactoryOptions as ethers.Signer | FactoryOptions | undefined + bytecodeOrFactoryOptions as EthersT.Signer | FactoryOptions | undefined ); } return getContractFactoryByAbiAndBytecode( hre, nameOrAbi, - bytecodeOrFactoryOptions as ethers.utils.BytesLike, + bytecodeOrFactoryOptions as EthersT.BytesLike, signer ); } function isFactoryOptions( - signerOrOptions?: ethers.Signer | FactoryOptions + signerOrOptions?: EthersT.Signer | FactoryOptions ): signerOrOptions is FactoryOptions { - const { Signer } = require("ethers") as typeof ethers; - return signerOrOptions !== undefined && !Signer.isSigner(signerOrOptions); + if (signerOrOptions === undefined || "provider" in signerOrOptions) { + return false; + } + + return true; } -export async function getContractFactoryFromArtifact( +export async function getContractFactoryFromArtifact< + A extends any[] = any[], + I = BaseContract +>( hre: HardhatRuntimeEnvironment, artifact: Artifact, - signerOrOptions?: ethers.Signer | FactoryOptions -) { + signerOrOptions?: EthersT.Signer | FactoryOptions +): Promise> { let libraries: Libraries = {}; - let signer: ethers.Signer | undefined; + let signer: EthersT.Signer | undefined; if (!isArtifact(artifact)) { - throw new NomicLabsHardhatPluginError( - pluginName, + throw new HardhatEthersError( `You are trying to create a contract factory from an artifact, but you have not passed a valid artifact parameter.` ); } @@ -143,8 +146,7 @@ export async function getContractFactoryFromArtifact( } if (artifact.bytecode === "0x") { - throw new NomicLabsHardhatPluginError( - pluginName, + throw new HardhatEthersError( `You are trying to create a contract factory for the contract ${artifact.contractName}, which is abstract and can't be deployed. If you want to call a contract using ${artifact.contractName} as its interface use the "getContractAt" function instead.` ); @@ -164,7 +166,7 @@ async function collectLibrariesAndLink( artifact: Artifact, libraries: Libraries ) { - const { utils } = require("ethers") as typeof ethers; + const ethers = require("ethers") as typeof EthersT; const neededLibraries: Array<{ sourceName: string; @@ -182,10 +184,20 @@ async function collectLibrariesAndLink( for (const [linkedLibraryName, linkedLibraryAddress] of Object.entries( libraries )) { - if (!utils.isAddress(linkedLibraryAddress)) { - throw new NomicLabsHardhatPluginError( - pluginName, - `You tried to link the contract ${artifact.contractName} with the library ${linkedLibraryName}, but provided this invalid address: ${linkedLibraryAddress}` + let resolvedAddress: string; + if (ethers.isAddressable(linkedLibraryAddress)) { + resolvedAddress = await linkedLibraryAddress.getAddress(); + } else { + resolvedAddress = linkedLibraryAddress; + } + + if (!ethers.isAddress(resolvedAddress)) { + throw new HardhatEthersError( + `You tried to link the contract ${ + artifact.contractName + } with the library ${linkedLibraryName}, but provided this invalid address: ${ + resolvedAddress as any + }` ); } @@ -208,8 +220,7 @@ ${libraryFQNames}`; } else { detailedMessage = "This contract doesn't need linking any libraries."; } - throw new NomicLabsHardhatPluginError( - pluginName, + throw new HardhatEthersError( `You tried to link the contract ${artifact.contractName} with ${linkedLibraryName}, which is not one of its libraries. ${detailedMessage}` ); @@ -220,8 +231,7 @@ ${detailedMessage}` .map(({ sourceName, libName }) => `${sourceName}:${libName}`) .map((x) => `* ${x}`) .join("\n"); - throw new NomicLabsHardhatPluginError( - pluginName, + throw new HardhatEthersError( `The library name ${linkedLibraryName} is ambiguous for the contract ${artifact.contractName}. It may resolve to one of the following libraries: ${matchingNeededLibrariesFQNs} @@ -238,8 +248,7 @@ To fix this, choose one of these fully qualified library names and replace where // for it to be given twice in the libraries user input: // once as a library name and another as a fully qualified library name. if (linksToApply.has(neededLibraryFQN)) { - throw new NomicLabsHardhatPluginError( - pluginName, + throw new HardhatEthersError( `The library names ${neededLibrary.libName} and ${neededLibraryFQN} refer to the same library and were given as two separate library links. Remove one of them and review your library links before proceeding.` ); @@ -248,7 +257,7 @@ Remove one of them and review your library links before proceeding.` linksToApply.set(neededLibraryFQN, { sourceName: neededLibrary.sourceName, libraryName: neededLibrary.libName, - address: linkedLibraryAddress, + address: resolvedAddress, }); } @@ -259,8 +268,7 @@ Remove one of them and review your library links before proceeding.` .map((x) => `* ${x}`) .join("\n"); - throw new NomicLabsHardhatPluginError( - pluginName, + throw new HardhatEthersError( `The contract ${artifact.contractName} is missing links for the following libraries: ${missingLibraries} @@ -272,32 +280,30 @@ Learn more about linking contracts at https://hardhat.org/hardhat-runner/plugins return linkBytecode(artifact, [...linksToApply.values()]); } -async function getContractFactoryByAbiAndBytecode( +async function getContractFactoryByAbiAndBytecode< + A extends any[] = any[], + I = BaseContract +>( hre: HardhatRuntimeEnvironment, abi: any[], - bytecode: ethers.utils.BytesLike, - signer?: ethers.Signer -) { - const { ContractFactory } = require("ethers") as typeof ethers; + bytecode: EthersT.BytesLike, + signer?: EthersT.Signer +): Promise> { + const { ContractFactory } = require("ethers") as typeof EthersT; if (signer === undefined) { const signers = await hre.ethers.getSigners(); signer = signers[0]; } - const abiWithAddedGas = addGasToAbiMethodsIfNecessary( - hre.network.config, - abi - ); - - return new ContractFactory(abiWithAddedGas, bytecode, signer); + return new ContractFactory(abi, bytecode, signer); } export async function getContractAt( hre: HardhatRuntimeEnvironment, nameOrAbi: string | any[], - address: string, - signer?: ethers.Signer + address: string | EthersT.Addressable, + signer?: EthersT.Signer ) { if (typeof nameOrAbi === "string") { const artifact = await hre.artifacts.readArtifact(nameOrAbi); @@ -305,7 +311,7 @@ export async function getContractAt( return getContractAtFromArtifact(hre, artifact, address, signer); } - const { Contract } = require("ethers") as typeof ethers; + const ethers = require("ethers") as typeof EthersT; if (signer === undefined) { const signers = await hre.ethers.getSigners(); @@ -314,36 +320,38 @@ export async function getContractAt( // If there's no signer, we want to put the provider for the selected network here. // This allows read only operations on the contract interface. - const signerOrProvider: ethers.Signer | ethers.providers.Provider = + const signerOrProvider: EthersT.Signer | EthersT.Provider = signer !== undefined ? signer : hre.ethers.provider; - const abiWithAddedGas = addGasToAbiMethodsIfNecessary( - hre.network.config, - nameOrAbi - ); + let resolvedAddress; + if (ethers.isAddressable(address)) { + resolvedAddress = await address.getAddress(); + } else { + resolvedAddress = address; + } - return new Contract(address, abiWithAddedGas, signerOrProvider); + return new ethers.Contract(resolvedAddress, nameOrAbi, signerOrProvider); } export async function deployContract( hre: HardhatRuntimeEnvironment, name: string, args?: any[], - signerOrOptions?: ethers.Signer | FactoryOptions -): Promise; + signerOrOptions?: EthersT.Signer | FactoryOptions +): Promise; export async function deployContract( hre: HardhatRuntimeEnvironment, name: string, - signerOrOptions?: ethers.Signer | FactoryOptions -): Promise; + signerOrOptions?: EthersT.Signer | FactoryOptions +): Promise; export async function deployContract( hre: HardhatRuntimeEnvironment, name: string, - argsOrSignerOrOptions?: any[] | ethers.Signer | FactoryOptions, - signerOrOptions?: ethers.Signer | FactoryOptions -): Promise { + argsOrSignerOrOptions?: any[] | EthersT.Signer | FactoryOptions, + signerOrOptions?: EthersT.Signer | FactoryOptions +): Promise { let args = []; if (Array.isArray(argsOrSignerOrOptions)) { args = argsOrSignerOrOptions; @@ -351,74 +359,41 @@ export async function deployContract( signerOrOptions = argsOrSignerOrOptions; } const factory = await getContractFactory(hre, name, signerOrOptions); - return factory.deploy(...args); + return factory.deploy(...args) as any; } export async function getContractAtFromArtifact( hre: HardhatRuntimeEnvironment, artifact: Artifact, - address: string, - signer?: ethers.Signer + address: string | EthersT.Addressable, + signer?: EthersT.Signer ) { + const ethers = require("ethers") as typeof EthersT; if (!isArtifact(artifact)) { - throw new NomicLabsHardhatPluginError( - pluginName, + throw new HardhatEthersError( `You are trying to create a contract by artifact, but you have not passed a valid artifact parameter.` ); } - const factory = await getContractFactoryByAbiAndBytecode( - hre, - artifact.abi, - "0x", - signer - ); - - let contract = factory.attach(address); - // If there's no signer, we connect the contract instance to the provider for the selected network. - if (contract.provider === null) { - contract = contract.connect(hre.ethers.provider); + if (signer === undefined) { + const signers = await hre.ethers.getSigners(); + signer = signers[0]; } - return contract; -} - -// This helper adds a `gas` field to the ABI function elements if the network -// is set up to use a fixed amount of gas. -// This is done so that ethers doesn't automatically estimate gas limits on -// every call. -function addGasToAbiMethodsIfNecessary( - networkConfig: NetworkConfig, - abi: any[] -): any[] { - const { BigNumber } = require("ethers") as typeof ethers; - - if (networkConfig.gas === "auto" || networkConfig.gas === undefined) { - return abi; + let resolvedAddress; + if (ethers.isAddressable(address)) { + resolvedAddress = await address.getAddress(); + } else { + resolvedAddress = address; } - // ethers adds 21000 to whatever the abi `gas` field has. This may lead to - // OOG errors, as people may set the default gas to the same value as the - // block gas limit, especially on Hardhat Network. - // To avoid this, we substract 21000. - // HOTFIX: We substract 1M for now. See: https://github.com/ethers-io/ethers.js/issues/1058#issuecomment-703175279 - const gasLimit = BigNumber.from(networkConfig.gas).sub(1000000).toHexString(); - - const modifiedAbi: any[] = []; + let contract = new ethers.Contract(resolvedAddress, artifact.abi, signer); - for (const abiElement of abi) { - if (abiElement.type !== "function") { - modifiedAbi.push(abiElement); - continue; - } - - modifiedAbi.push({ - ...abiElement, - gas: gasLimit, - }); + if (contract.runner === null) { + contract = contract.connect(hre.ethers.provider) as EthersT.Contract; } - return modifiedAbi; + return contract; } function linkBytecode(artifact: Artifact, libraries: Link[]): string { diff --git a/packages/hardhat-ethers/src/internal/index.ts b/packages/hardhat-ethers/src/internal/index.ts index a8f46f51e0..12f4a0b7a3 100644 --- a/packages/hardhat-ethers/src/internal/index.ts +++ b/packages/hardhat-ethers/src/internal/index.ts @@ -1,9 +1,9 @@ import type EthersT from "ethers"; -import type * as ProviderProxyT from "./provider-proxy"; import { extendEnvironment } from "hardhat/config"; import { lazyObject } from "hardhat/plugins"; +import { CustomEthersProvider } from "./custom-ethers-provider"; import { getContractAt, getContractAtFromArtifact, @@ -16,29 +16,19 @@ import { } from "./helpers"; import "./type-extensions"; -const registerCustomInspection = (BigNumber: any) => { - const inspectCustomSymbol = Symbol.for("nodejs.util.inspect.custom"); - - BigNumber.prototype[inspectCustomSymbol] = function () { - return `BigNumber { value: "${this.toString()}" }`; - }; -}; - extendEnvironment((hre) => { hre.ethers = lazyObject(() => { - const { createProviderProxy } = - require("./provider-proxy") as typeof ProviderProxyT; - const { ethers } = require("ethers") as typeof EthersT; - registerCustomInspection(ethers.BigNumber); - - const providerProxy = createProviderProxy(hre.network.provider); + const provider = new CustomEthersProvider( + hre.network.provider, + hre.network.name + ); return { ...ethers, - provider: providerProxy, + provider, getSigner: (address: string) => getSigner(hre, address), getSigners: () => getSigners(hre), @@ -47,12 +37,11 @@ extendEnvironment((hre) => { // We cast to any here as we hit a limitation of Function#bind and // overloads. See: https://github.com/microsoft/TypeScript/issues/28582 getContractFactory: getContractFactory.bind(null, hre) as any, - getContractFactoryFromArtifact: getContractFactoryFromArtifact.bind( - null, - hre - ), - getContractAt: getContractAt.bind(null, hre), - getContractAtFromArtifact: getContractAtFromArtifact.bind(null, hre), + getContractFactoryFromArtifact: (...args) => + getContractFactoryFromArtifact(hre, ...args), + getContractAt: (...args) => getContractAt(hre, ...args), + getContractAtFromArtifact: (...args) => + getContractAtFromArtifact(hre, ...args), deployContract: deployContract.bind(null, hre) as any, }; }); diff --git a/packages/hardhat-ethers/src/internal/provider-proxy.ts b/packages/hardhat-ethers/src/internal/provider-proxy.ts deleted file mode 100644 index 4a722de9da..0000000000 --- a/packages/hardhat-ethers/src/internal/provider-proxy.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { - HARDHAT_NETWORK_RESET_EVENT, - HARDHAT_NETWORK_REVERT_SNAPSHOT_EVENT, -} from "hardhat/internal/constants"; -import { EthereumProvider } from "hardhat/types"; - -import { EthersProviderWrapper } from "./ethers-provider-wrapper"; -import { createUpdatableTargetProxy } from "./updatable-target-proxy"; - -/** - * This method returns a proxy that uses an underlying provider for everything. - * - * This underlying provider is replaced by a new one after a successful hardhat_reset, - * because ethers providers can have internal state that returns wrong results after - * the network is reset. - */ -export function createProviderProxy( - hardhatProvider: EthereumProvider -): EthersProviderWrapper { - const initialProvider = new EthersProviderWrapper(hardhatProvider); - - const { proxy: providerProxy, setTarget } = - createUpdatableTargetProxy(initialProvider); - - hardhatProvider.on(HARDHAT_NETWORK_RESET_EVENT, () => { - setTarget(new EthersProviderWrapper(hardhatProvider)); - }); - hardhatProvider.on(HARDHAT_NETWORK_REVERT_SNAPSHOT_EVENT, () => { - setTarget(new EthersProviderWrapper(hardhatProvider)); - }); - - return providerProxy; -} diff --git a/packages/hardhat-ethers/src/internal/updatable-target-proxy.ts b/packages/hardhat-ethers/src/internal/updatable-target-proxy.ts deleted file mode 100644 index 8ad55cd3ac..0000000000 --- a/packages/hardhat-ethers/src/internal/updatable-target-proxy.ts +++ /dev/null @@ -1,106 +0,0 @@ -/** - * Returns a read-only proxy that just forwards everything to a target, - * and a function that can be used to change that underlying target - */ -export function createUpdatableTargetProxy( - initialTarget: T -): { - proxy: T; - setTarget: (target: T) => void; -} { - const targetObject = { - target: initialTarget, - }; - - let isExtensible = Object.isExtensible(initialTarget); - - const handler: Required> = { - // these two functions are implemented because of the Required type - apply(_, _thisArg, _argArray) { - throw new Error( - "cannot be implemented because the target is not a function" - ); - }, - - construct(_, _argArray, _newTarget) { - throw new Error( - "cannot be implemented because the target is not a function" - ); - }, - - defineProperty(_, property, _descriptor) { - throw new Error( - `cannot define property ${String(property)} in read-only proxy` - ); - }, - - deleteProperty(_, property) { - throw new Error( - `cannot delete property ${String(property)} in read-only proxy` - ); - }, - - get(_, property, receiver) { - const result = Reflect.get(targetObject.target, property, receiver); - - if (result instanceof Function) { - return result.bind(targetObject.target); - } - - return result; - }, - - getOwnPropertyDescriptor(_, property) { - const descriptor = Reflect.getOwnPropertyDescriptor( - targetObject.target, - property - ); - - if (descriptor !== undefined) { - Object.defineProperty(targetObject.target, property, descriptor); - } - - return descriptor; - }, - - getPrototypeOf(_) { - return Reflect.getPrototypeOf(targetObject.target); - }, - - has(_, property) { - return Reflect.has(targetObject.target, property); - }, - - isExtensible(_) { - // we need to return the extensibility value of the original target - return isExtensible; - }, - - ownKeys(_) { - return Reflect.ownKeys(targetObject.target); - }, - - preventExtensions(_) { - isExtensible = false; - return Reflect.preventExtensions(targetObject.target); - }, - - set(_, property, _value, _receiver) { - throw new Error( - `cannot set property ${String(property)} in read-only proxy` - ); - }, - - setPrototypeOf(_, _prototype) { - throw new Error("cannot change the prototype in read-only proxy"); - }, - }; - - const proxy: T = new Proxy(initialTarget, handler); - - const setTarget = (newTarget: T) => { - targetObject.target = newTarget; - }; - - return { proxy, setTarget }; -} diff --git a/packages/hardhat-ethers/src/signers.ts b/packages/hardhat-ethers/src/signers.ts index 48294f1cb9..ebb068e8d4 100644 --- a/packages/hardhat-ethers/src/signers.ts +++ b/packages/hardhat-ethers/src/signers.ts @@ -1,49 +1,298 @@ -import { ethers } from "ethers"; +import type { BlockTag, TransactionRequest } from "ethers/types/providers"; +import { + assertArgument, + ethers, + getAddress, + hexlify, + resolveAddress, + toUtf8Bytes, + TransactionLike, + TypedDataEncoder, +} from "ethers"; +import { CustomEthersProvider } from "./internal/custom-ethers-provider"; +import { + copyRequest, + getRpcTransaction, + resolveProperties, +} from "./internal/ethers-utils"; +import { HardhatEthersError, NotImplementedError } from "./internal/errors"; -export class SignerWithAddress extends ethers.Signer { - public static async create(signer: ethers.providers.JsonRpcSigner) { - return new SignerWithAddress(await signer.getAddress(), signer); +export class CustomEthersSigner implements ethers.Signer { + public readonly address: string; + public readonly provider: ethers.JsonRpcProvider | CustomEthersProvider; + + public static async create(provider: CustomEthersProvider, address: string) { + const hre = await import("hardhat"); + let gasLimit: number | undefined; + if ( + hre.network.name === "hardhat" && + hre.network.config.gas !== "auto" && + hre.network.config.gas !== undefined + ) { + gasLimit = hre.network.config.gas; + } + + return new CustomEthersSigner(address, provider, gasLimit); } private constructor( - public readonly address: string, - private readonly _signer: ethers.providers.JsonRpcSigner + address: string, + _provider: ethers.JsonRpcProvider | CustomEthersProvider, + private readonly _gasLimit?: number ) { - super(); - (this as any).provider = _signer.provider; + this.address = getAddress(address); + this.provider = _provider; } - public async getAddress(): Promise { - return this.address; + public connect( + provider: ethers.JsonRpcProvider | CustomEthersProvider + ): ethers.Signer { + return new CustomEthersSigner(this.address, provider); } - public signMessage(message: string | ethers.utils.Bytes): Promise { - return this._signer.signMessage(message); + public getNonce(blockTag?: BlockTag | undefined): Promise { + return this.provider.getTransactionCount(this.address, blockTag); } - public signTransaction( - transaction: ethers.utils.Deferrable - ): Promise { - return this._signer.signTransaction(transaction); + public populateCall( + tx: TransactionRequest + ): Promise> { + return populate(this, tx); + } + + public populateTransaction( + tx: TransactionRequest + ): Promise> { + return this.populateCall(tx); + } + + public async estimateGas(tx: TransactionRequest): Promise { + return this.provider.estimateGas(await this.populateCall(tx)); + } + + public async call(tx: TransactionRequest): Promise { + return this.provider.call(await this.populateCall(tx)); + } + + public resolveName(name: string): Promise { + return this.provider.resolveName(name); + } + + public async signTransaction(_tx: TransactionRequest): Promise { + // TODO if we split the signer for the in-process and json-rpc networks, + // we can enable this method when using the in-process network or when the + // json-rpc network has a private key + throw new NotImplementedError("CustomEthersSigner.signTransaction"); } - public sendTransaction( - transaction: ethers.utils.Deferrable - ): Promise { - return this._signer.sendTransaction(transaction); + public async sendTransaction( + tx: TransactionRequest + ): Promise { + // This cannot be mined any earlier than any recent block + const blockNumber = await this.provider.getBlockNumber(); + + // Send the transaction + const hash = await this._sendUncheckedTransaction(tx); + + // Unfortunately, JSON-RPC only provides and opaque transaction hash + // for a response, and we need the actual transaction, so we poll + // for it; it should show up very quickly + + return new Promise((resolve) => { + const timeouts = [1000, 100]; + const checkTx = async () => { + // Try getting the transaction + const txPolled = await this.provider.getTransaction(hash); + if (txPolled !== null) { + resolve(txPolled.replaceableTransaction(blockNumber)); + return; + } + + // Wait another 4 seconds + setTimeout(() => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + checkTx(); + }, timeouts.pop() ?? 4000); + }; + // eslint-disable-next-line @typescript-eslint/no-floating-promises + checkTx(); + }); } - public connect(provider: ethers.providers.Provider): SignerWithAddress { - return new SignerWithAddress(this.address, this._signer.connect(provider)); + public signMessage(message: string | Uint8Array): Promise { + const resolvedMessage = + typeof message === "string" ? toUtf8Bytes(message) : message; + return this.provider.send("personal_sign", [ + hexlify(resolvedMessage), + this.address.toLowerCase(), + ]); } - public _signTypedData( - ...params: Parameters + public async signTypedData( + domain: ethers.TypedDataDomain, + types: Record, + value: Record ): Promise { - return this._signer._signTypedData(...params); + const copiedValue = deepCopy(value); + + // Populate any ENS names (in-place) + const populated = await TypedDataEncoder.resolveNames( + domain, + types, + copiedValue, + async (v: string) => { + return v; + } + ); + + return this.provider.send("eth_signTypedData_v4", [ + this.address.toLowerCase(), + JSON.stringify( + TypedDataEncoder.getPayload(populated.domain, types, populated.value), + (_k, v) => { + if (typeof v === "bigint") { + return v.toString(); + } + + return v; + } + ), + ]); + } + + public async getAddress(): Promise { + return this.address; } public toJSON() { return ``; } + + private async _sendUncheckedTransaction( + tx: TransactionRequest + ): Promise { + const resolvedTx = deepCopy(tx); + + const promises: Array> = []; + + // Make sure the from matches the sender + if (resolvedTx.from !== null && resolvedTx.from !== undefined) { + const _from = resolvedTx.from; + promises.push( + (async () => { + const from = await resolveAddress(_from, this.provider); + assertArgument( + from !== null && + from !== undefined && + from.toLowerCase() === this.address.toLowerCase(), + "from address mismatch", + "transaction", + tx + ); + resolvedTx.from = from; + })() + ); + } else { + resolvedTx.from = this.address; + } + + if (resolvedTx.gasLimit === null || resolvedTx.gasLimit === undefined) { + if (this._gasLimit !== undefined) { + resolvedTx.gasLimit = this._gasLimit; + } else { + promises.push( + (async () => { + resolvedTx.gasLimit = await this.provider.estimateGas({ + ...resolvedTx, + from: this.address, + }); + })() + ); + } + } + + // The address may be an ENS name or Addressable + if (resolvedTx.to !== null && resolvedTx.to !== undefined) { + const _to = resolvedTx.to; + promises.push( + (async () => { + resolvedTx.to = await resolveAddress(_to, this.provider); + })() + ); + } + + // Wait until all of our properties are filled in + if (promises.length > 0) { + await Promise.all(promises); + } + + const hexTx = getRpcTransaction(resolvedTx); + + return this.provider.send("eth_sendTransaction", [hexTx]); + } +} + +// exported as an alias to make migration easier +export { CustomEthersSigner as SignerWithAddress }; + +async function populate( + signer: ethers.Signer, + tx: TransactionRequest +): Promise> { + const pop: any = copyRequest(tx); + + if (pop.to !== null && pop.to !== undefined) { + pop.to = resolveAddress(pop.to, signer); + } + + if (pop.from !== null && pop.from !== undefined) { + const from = pop.from; + pop.from = Promise.all([ + signer.getAddress(), + resolveAddress(from, signer), + ]).then(([address, resolvedFrom]) => { + assertArgument( + address.toLowerCase() === resolvedFrom.toLowerCase(), + "transaction from mismatch", + "tx.from", + resolvedFrom + ); + return address; + }); + } else { + pop.from = signer.getAddress(); + } + + return resolveProperties(pop); +} + +const Primitive = "bigint,boolean,function,number,string,symbol".split(/,/g); +function deepCopy(value: T): T { + if ( + value === null || + value === undefined || + Primitive.indexOf(typeof value) >= 0 + ) { + return value; + } + + // Keep any Addressable + if (typeof (value as any).getAddress === "function") { + return value; + } + + if (Array.isArray(value)) { + return (value as any).map(deepCopy); + } + + if (typeof value === "object") { + return Object.keys(value).reduce((accum, key) => { + accum[key] = (value as any)[key]; + return accum; + }, {} as any); + } + + throw new HardhatEthersError( + `Assertion error: ${value as any} (${typeof value})` + ); } diff --git a/packages/hardhat-ethers/src/types/index.ts b/packages/hardhat-ethers/src/types/index.ts index b39b673ce3..54100a372a 100644 --- a/packages/hardhat-ethers/src/types/index.ts +++ b/packages/hardhat-ethers/src/types/index.ts @@ -1,10 +1,10 @@ import type * as ethers from "ethers"; -import type { SignerWithAddress } from "../signers"; - -import { Artifact } from "hardhat/types"; +import type { Artifact } from "hardhat/types"; +import type { CustomEthersProvider } from "../internal/custom-ethers-provider"; +import type { CustomEthersSigner } from "../signers"; export interface Libraries { - [libraryName: string]: string; + [libraryName: string]: string | ethers.Addressable; } export interface FactoryOptions { @@ -12,15 +12,21 @@ export interface FactoryOptions { libraries?: Libraries; } -export declare function getContractFactory( +export declare function getContractFactory< + A extends any[] = any[], + I = ethers.BaseContract +>( name: string, signerOrOptions?: ethers.Signer | FactoryOptions -): Promise; -export declare function getContractFactory( +): Promise>; +export declare function getContractFactory< + A extends any[] = any[], + I = ethers.BaseContract +>( abi: any[], - bytecode: ethers.utils.BytesLike, + bytecode: ethers.BytesLike, signer?: ethers.Signer -): Promise; +): Promise>; export declare function deployContract( name: string, @@ -33,17 +39,22 @@ export declare function deployContract( signerOrOptions?: ethers.Signer | FactoryOptions ): Promise; +export declare function getContractFactoryFromArtifact< + A extends any[] = any[], + I = ethers.BaseContract +>( + artifact: Artifact, + signerOrOptions?: ethers.Signer | FactoryOptions +): Promise>; + export interface HardhatEthersHelpers { - provider: ethers.providers.JsonRpcProvider; + provider: CustomEthersProvider; getContractFactory: typeof getContractFactory; - getContractFactoryFromArtifact: ( - artifact: Artifact, - signerOrOptions?: ethers.Signer | FactoryOptions - ) => Promise; + getContractFactoryFromArtifact: typeof getContractFactoryFromArtifact; getContractAt: ( nameOrAbi: string | any[], - address: string, + address: string | ethers.Addressable, signer?: ethers.Signer ) => Promise; getContractAtFromArtifact: ( @@ -51,8 +62,8 @@ export interface HardhatEthersHelpers { address: string, signer?: ethers.Signer ) => Promise; - getSigner: (address: string) => Promise; - getSigners: () => Promise; - getImpersonatedSigner: (address: string) => Promise; + getSigner: (address: string) => Promise; + getSigners: () => Promise; + getImpersonatedSigner: (address: string) => Promise; deployContract: typeof deployContract; } diff --git a/packages/hardhat-ethers/test/custom-ethers-provider.ts b/packages/hardhat-ethers/test/custom-ethers-provider.ts new file mode 100644 index 0000000000..b315bd70ac --- /dev/null +++ b/packages/hardhat-ethers/test/custom-ethers-provider.ts @@ -0,0 +1,1043 @@ +import { assert, use } from "chai"; +import chaiAsPromised from "chai-as-promised"; + +import { ExampleContract, EXAMPLE_CONTRACT } from "./example-contracts"; +import { + assertIsNotNull, + assertWithin, + usePersistentEnvironment, +} from "./helpers"; + +use(chaiAsPromised); + +describe("custom provider", function () { + usePersistentEnvironment("minimal-project"); + + it("can access itself through .provider", async function () { + assert.strictEqual( + this.env.ethers.provider, + this.env.ethers.provider.provider + ); + }); + + it("should have a destroy method", async function () { + this.env.ethers.provider.destroy(); + }); + + it("should have a send method for raw JSON-RPC requests", async function () { + const accounts = await this.env.ethers.provider.send("eth_accounts"); + + assert.isArray(accounts); + }); + + describe("getSigner", function () { + it("should get a signer using an index", async function () { + const signer = await this.env.ethers.provider.getSigner(0); + + assert.strictEqual( + await signer.getAddress(), + "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + ); + }); + + it("should get a signer using an address", async function () { + const signer = await this.env.ethers.provider.getSigner( + "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + ); + + assert.strictEqual( + await signer.getAddress(), + "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + ); + }); + + it("should get a signer even if the address is all lowercase", async function () { + const signer = await this.env.ethers.provider.getSigner( + "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266" + ); + + assert.strictEqual( + await signer.getAddress(), + "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + ); + }); + + it("should throw if the address checksum is wrong", async function () { + await assert.isRejected( + this.env.ethers.provider.getSigner( + "0XF39FD6E51AAD88F6F4CE6AB8827279CFFFB92266" + ), + "invalid address" + ); + }); + + it("should throw if the index doesn't match an account", async function () { + await assert.isRejected( + this.env.ethers.provider.getSigner(100), + "Tried to get account with index 100 but there are 20 accounts" + ); + }); + + it("should work for impersonated accounts", async function () { + const [s] = await this.env.ethers.getSigners(); + const randomAddress = "0xf965cceab9374d9f961581b6e38942e45e1cfeae"; + + await s.sendTransaction({ + to: randomAddress, + value: this.env.ethers.parseEther("1"), + }); + + await this.env.ethers.provider.send("hardhat_impersonateAccount", [ + randomAddress, + ]); + + const impersonatedSigner = await this.env.ethers.provider.getSigner( + randomAddress + ); + + // shouldn't revert + await impersonatedSigner.sendTransaction({ + to: s.address, + value: this.env.ethers.parseEther("0.1"), + }); + }); + }); + + it("should return the latest block number", async function () { + const latestBlockNumber = await this.env.ethers.provider.getBlockNumber(); + + assert.strictEqual(latestBlockNumber, 0); + + await this.env.ethers.provider.send("hardhat_mine"); + + assert.strictEqual(latestBlockNumber, 0); + }); + + it("should return the network", async function () { + const network = await this.env.ethers.provider.getNetwork(); + + assert.strictEqual(network.name, "hardhat"); + assert.strictEqual(network.chainId, 31337n); + }); + + it("should return fee data", async function () { + const feeData = await this.env.ethers.provider.getFeeData(); + + assert.typeOf(feeData.gasPrice, "bigint"); + assert.typeOf(feeData.maxFeePerGas, "bigint"); + assert.typeOf(feeData.maxPriorityFeePerGas, "bigint"); + }); + + describe("getBalance", function () { + beforeEach(async function () { + await this.env.network.provider.send("hardhat_reset"); + }); + + it("should return the balance of an address", async function () { + const balance = await this.env.ethers.provider.getBalance( + "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + ); + + assert.strictEqual(balance, this.env.ethers.parseEther("10000")); + }); + + it("should return the balance of a signer", async function () { + const signer = await this.env.ethers.provider.getSigner(0); + const balance = await this.env.ethers.provider.getBalance(signer); + + assert.strictEqual(balance, this.env.ethers.parseEther("10000")); + }); + + it("should accept block numbers", async function () { + const signer = await this.env.ethers.provider.getSigner(0); + const gasLimit = 21_000n; + const gasPrice = this.env.ethers.parseUnits("100", "gwei"); + const value = this.env.ethers.parseEther("1"); + + await signer.sendTransaction({ + to: this.env.ethers.ZeroAddress, + value, + gasLimit, + gasPrice, + }); + + const blockNumber = await this.env.ethers.provider.getBlockNumber(); + + const balanceAfter = await this.env.ethers.provider.getBalance( + signer, + "latest" + ); + assert.strictEqual( + balanceAfter, + this.env.ethers.parseEther("10000") - gasLimit * gasPrice - value + ); + + const balanceBefore = await this.env.ethers.provider.getBalance( + signer, + blockNumber - 1 + ); + assert.strictEqual(balanceBefore, this.env.ethers.parseEther("10000")); + }); + + it("should accept block hashes", async function () { + const block = await this.env.ethers.provider.getBlock("latest"); + + assertIsNotNull(block); + assertIsNotNull(block.hash); + + const balance = await this.env.ethers.provider.getBalance( + "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + block.hash + ); + + assert.strictEqual(balance, this.env.ethers.parseEther("10000")); + }); + + it("should return the balance of a contract", async function () { + // deploy a contract with some ETH + const signer = await this.env.ethers.provider.getSigner(0); + const factory = new this.env.ethers.ContractFactory( + EXAMPLE_CONTRACT.abi, + EXAMPLE_CONTRACT.deploymentBytecode, + signer + ); + const contract = await factory.deploy({ + value: this.env.ethers.parseEther("0.5"), + }); + + // check the balance of the contract + const balance = await this.env.ethers.provider.getBalance(contract); + + assert.strictEqual(balance, 5n * 10n ** 17n); + }); + }); + + describe("getTransactionCount", function () { + beforeEach(async function () { + await this.env.network.provider.send("hardhat_reset"); + }); + + it("should return the transaction count of an address", async function () { + const balance = await this.env.ethers.provider.getTransactionCount( + "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + ); + + assert.strictEqual(balance, 0); + }); + + it("should return the transaction count of a signer", async function () { + const signer = await this.env.ethers.provider.getSigner(0); + const balance = await this.env.ethers.provider.getTransactionCount( + signer + ); + + assert.strictEqual(balance, 0); + }); + + it("should accept block numbers", async function () { + const signer = await this.env.ethers.provider.getSigner(0); + + await signer.sendTransaction({ + to: this.env.ethers.ZeroAddress, + }); + + const blockNumber = await this.env.ethers.provider.getBlockNumber(); + + const transactionCountAfter = + await this.env.ethers.provider.getTransactionCount(signer, "latest"); + assert.strictEqual(transactionCountAfter, 1); + + const transactionCountBefore = + await this.env.ethers.provider.getTransactionCount( + signer, + blockNumber - 1 + ); + assert.strictEqual(transactionCountBefore, 0); + }); + + it("should accept block hashes", async function () { + const block = await this.env.ethers.provider.getBlock("latest"); + + assertIsNotNull(block); + assertIsNotNull(block.hash); + + const balance = await this.env.ethers.provider.getTransactionCount( + "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + block.hash + ); + + assert.strictEqual(balance, 0); + }); + }); + + describe("getCode", function () { + // deploys an empty contract + const deploymentBytecode = + "0x6080604052348015600f57600080fd5b50603f80601d6000396000f3fe6080604052600080fdfea2646970667358221220eeaf807039e8b863535433564733b36afab56700620e89f192795eaf32f272ee64736f6c63430008110033"; + const contractBytecode = + "0x6080604052600080fdfea2646970667358221220eeaf807039e8b863535433564733b36afab56700620e89f192795eaf32f272ee64736f6c63430008110033"; + + let contract: ExampleContract; + beforeEach(async function () { + const signer = await this.env.ethers.provider.getSigner(0); + const factory = new this.env.ethers.ContractFactory<[], ExampleContract>( + [], + deploymentBytecode, + signer + ); + contract = await factory.deploy(); + }); + + it("should return the code of an address", async function () { + const contractAddress = await contract.getAddress(); + + const code = await this.env.ethers.provider.getCode(contractAddress); + + assert.strictEqual(code, contractBytecode); + }); + + it("should return the code of a contract", async function () { + const code = await this.env.ethers.provider.getCode(contract); + + assert.strictEqual(code, contractBytecode); + }); + + it("should accept block numbers", async function () { + const codeAfter = await this.env.ethers.provider.getCode( + contract, + "latest" + ); + assert.strictEqual(codeAfter, contractBytecode); + + const blockNumber = await this.env.ethers.provider.getBlockNumber(); + const codeBefore = await this.env.ethers.provider.getCode( + contract, + blockNumber - 1 + ); + assert.strictEqual(codeBefore, "0x"); + }); + + it("should accept block hashes", async function () { + const block = await this.env.ethers.provider.getBlock("latest"); + + assertIsNotNull(block); + assertIsNotNull(block.hash); + + const code = await this.env.ethers.provider.getCode(contract, block.hash); + assert.strictEqual(code, contractBytecode); + }); + }); + + describe("getStorage", function () { + let contract: ExampleContract; + beforeEach(async function () { + const signer = await this.env.ethers.provider.getSigner(0); + const factory = new this.env.ethers.ContractFactory<[], ExampleContract>( + EXAMPLE_CONTRACT.abi, + EXAMPLE_CONTRACT.deploymentBytecode, + signer + ); + contract = await factory.deploy(); + }); + + it("should get the storage of an address", async function () { + const contractAddress = await contract.getAddress(); + + await contract.inc(); + + const value = await this.env.ethers.provider.getStorage( + contractAddress, + 0 + ); + const doubleValue = await this.env.ethers.provider.getStorage( + contractAddress, + 1 + ); + + assert.strictEqual( + value, + "0x0000000000000000000000000000000000000000000000000000000000000001" + ); + assert.strictEqual( + doubleValue, + "0x0000000000000000000000000000000000000000000000000000000000000002" + ); + }); + + it("should get the storage of a contract", async function () { + await contract.inc(); + + const value = await this.env.ethers.provider.getStorage(contract, 0); + const doubleValue = await this.env.ethers.provider.getStorage( + contract, + 1 + ); + + assert.strictEqual( + value, + "0x0000000000000000000000000000000000000000000000000000000000000001" + ); + assert.strictEqual( + doubleValue, + "0x0000000000000000000000000000000000000000000000000000000000000002" + ); + }); + + it("should accept block numbers", async function () { + await contract.inc(); + + const storageValueAfter = await this.env.ethers.provider.getStorage( + contract, + 0, + "latest" + ); + assert.strictEqual( + storageValueAfter, + "0x0000000000000000000000000000000000000000000000000000000000000001" + ); + + const blockNumber = await this.env.ethers.provider.getBlockNumber(); + const storageValueBefore = await this.env.ethers.provider.getStorage( + contract, + 0, + blockNumber - 1 + ); + assert.strictEqual( + storageValueBefore, + "0x0000000000000000000000000000000000000000000000000000000000000000" + ); + }); + + it("should accept block hashes", async function () { + await contract.inc(); + + const block = await this.env.ethers.provider.getBlock("latest"); + + assertIsNotNull(block); + assertIsNotNull(block.hash); + + const storageValue = await this.env.ethers.provider.getStorage( + contract, + 0, + block.hash + ); + + assert.strictEqual( + storageValue, + "0x0000000000000000000000000000000000000000000000000000000000000001" + ); + }); + + it("should accept short hex encode strings as the storage position", async function () { + await contract.inc(); + + const value = await this.env.ethers.provider.getStorage(contract, "0x0"); + const doubleValue = await this.env.ethers.provider.getStorage( + contract, + "0x1" + ); + assert.strictEqual( + value, + "0x0000000000000000000000000000000000000000000000000000000000000001" + ); + assert.strictEqual( + doubleValue, + "0x0000000000000000000000000000000000000000000000000000000000000002" + ); + }); + + it("should accept long hex encode strings as the storage position", async function () { + await contract.inc(); + + const value = await this.env.ethers.provider.getStorage( + contract, + "0x0000000000000000000000000000000000000000000000000000000000000000" + ); + const doubleValue = await this.env.ethers.provider.getStorage( + contract, + "0x0000000000000000000000000000000000000000000000000000000000000001" + ); + assert.strictEqual( + value, + "0x0000000000000000000000000000000000000000000000000000000000000001" + ); + assert.strictEqual( + doubleValue, + "0x0000000000000000000000000000000000000000000000000000000000000002" + ); + }); + + it("should accept bigints as the storage position", async function () { + await contract.inc(); + + const value = await this.env.ethers.provider.getStorage(contract, 0n); + const doubleValue = await this.env.ethers.provider.getStorage( + contract, + 1n + ); + assert.strictEqual( + value, + "0x0000000000000000000000000000000000000000000000000000000000000001" + ); + assert.strictEqual( + doubleValue, + "0x0000000000000000000000000000000000000000000000000000000000000002" + ); + }); + }); + + describe("estimateGas", function () { + it("should estimate gas for a value transaction", async function () { + const signer = await this.env.ethers.provider.getSigner(0); + const gasEstimation = await this.env.ethers.provider.estimateGas({ + from: signer.address, + to: signer.address, + }); + + assert.strictEqual(Number(gasEstimation), 21_001); + }); + + it("should estimate gas for a contract call", async function () { + const signer = await this.env.ethers.provider.getSigner(0); + const factory = new this.env.ethers.ContractFactory<[], ExampleContract>( + EXAMPLE_CONTRACT.abi, + EXAMPLE_CONTRACT.deploymentBytecode, + signer + ); + const contract = await factory.deploy(); + + const gasEstimation = await this.env.ethers.provider.estimateGas({ + from: signer.address, + to: await contract.getAddress(), + data: "0x371303c0", // inc() + }); + + assertWithin(Number(gasEstimation), 65_000, 70_000); + }); + + it("should accept a block number", async function () { + const signer = await this.env.ethers.provider.getSigner(0); + const factory = new this.env.ethers.ContractFactory<[], ExampleContract>( + EXAMPLE_CONTRACT.abi, + EXAMPLE_CONTRACT.deploymentBytecode, + signer + ); + const contract = await factory.deploy(); + + await contract.inc(); + const blockNumber = await this.env.ethers.provider.getBlockNumber(); + + const gasEstimationAfter = await this.env.ethers.provider.estimateGas({ + from: signer.address, + to: await contract.getAddress(), + data: "0x371303c0", // inc() + blockTag: "latest", + }); + + assertWithin(Number(gasEstimationAfter), 30_000, 35_000); + + const gasEstimationBefore = await this.env.ethers.provider.estimateGas({ + from: signer.address, + to: await contract.getAddress(), + data: "0x371303c0", // inc() + blockTag: blockNumber - 1, + }); + + assertWithin(Number(gasEstimationBefore), 65_000, 70_000); + }); + + it("should accept a block hash", async function () { + const signer = await this.env.ethers.provider.getSigner(0); + const factory = new this.env.ethers.ContractFactory( + EXAMPLE_CONTRACT.abi, + EXAMPLE_CONTRACT.deploymentBytecode, + signer + ); + const contract = await factory.deploy(); + + const block = await this.env.ethers.provider.getBlock("latest"); + + assertIsNotNull(block); + assertIsNotNull(block.hash); + + const gasEstimation = await this.env.ethers.provider.estimateGas({ + from: signer.address, + to: await contract.getAddress(), + data: "0x371303c0", // inc() + blockTag: block.hash, + }); + + assertWithin(Number(gasEstimation), 65_000, 70_000); + }); + + it("should use the pending block by default", async function () { + const signer = await this.env.ethers.provider.getSigner(0); + const factory = new this.env.ethers.ContractFactory<[], ExampleContract>( + EXAMPLE_CONTRACT.abi, + EXAMPLE_CONTRACT.deploymentBytecode, + signer + ); + const contract = await factory.deploy(); + + // this estimates the cost of increasing the value from 0 to 1 + const gasEstimationFirstInc = await this.env.ethers.provider.estimateGas({ + from: signer.address, + to: await contract.getAddress(), + data: "0x371303c0", // inc() + }); + + await this.env.ethers.provider.send("evm_setAutomine", [false]); + await contract.inc(); + + // if the pending block is used, this should estimate the cost of + // increasing the value from 1 to 2, and this should be cheaper than + // increasing it from 0 to 1 + const gasEstimationSecondInc = await this.env.ethers.provider.estimateGas( + { + from: signer.address, + to: await contract.getAddress(), + data: "0x371303c0", // inc() + } + ); + + assert.isTrue( + gasEstimationSecondInc < gasEstimationFirstInc, + "Expected second gas estimation to be lower" + ); + + await this.env.ethers.provider.send("evm_setAutomine", [true]); + }); + }); + + describe("call", function () { + it("should make a contract call using an address", async function () { + const signer = await this.env.ethers.provider.getSigner(0); + const factory = new this.env.ethers.ContractFactory<[], ExampleContract>( + EXAMPLE_CONTRACT.abi, + EXAMPLE_CONTRACT.deploymentBytecode, + signer + ); + const contract = await factory.deploy(); + await contract.inc(); + + const result = await this.env.ethers.provider.call({ + from: signer.address, + to: await contract.getAddress(), + data: "0x3fa4f245", // value() + }); + + assert.strictEqual( + result, + "0x0000000000000000000000000000000000000000000000000000000000000001" + ); + }); + + it("should make a contract call using a contract", async function () { + const signer = await this.env.ethers.provider.getSigner(0); + const factory = new this.env.ethers.ContractFactory<[], ExampleContract>( + EXAMPLE_CONTRACT.abi, + EXAMPLE_CONTRACT.deploymentBytecode, + signer + ); + const contract = await factory.deploy(); + await contract.inc(); + + const result = await this.env.ethers.provider.call({ + from: signer.address, + to: contract, + data: "0x3fa4f245", // value() + }); + + assert.strictEqual( + result, + "0x0000000000000000000000000000000000000000000000000000000000000001" + ); + }); + + it("should accept a block number", async function () { + const signer = await this.env.ethers.provider.getSigner(0); + const factory = new this.env.ethers.ContractFactory<[], ExampleContract>( + EXAMPLE_CONTRACT.abi, + EXAMPLE_CONTRACT.deploymentBytecode, + signer + ); + const contract = await factory.deploy(); + await contract.inc(); + + const blockNumber = await this.env.ethers.provider.getBlockNumber(); + + const resultAfter = await this.env.ethers.provider.call({ + from: signer.address, + to: contract, + data: "0x3fa4f245", // value() + blockTag: "latest", + }); + + assert.strictEqual( + resultAfter, + "0x0000000000000000000000000000000000000000000000000000000000000001" + ); + + const resultBefore = await this.env.ethers.provider.call({ + from: signer.address, + to: contract, + data: "0x3fa4f245", // value() + blockTag: blockNumber - 1, + }); + + assert.strictEqual( + resultBefore, + "0x0000000000000000000000000000000000000000000000000000000000000000" + ); + }); + + it("should accept a block hash", async function () { + const signer = await this.env.ethers.provider.getSigner(0); + const factory = new this.env.ethers.ContractFactory<[], ExampleContract>( + EXAMPLE_CONTRACT.abi, + EXAMPLE_CONTRACT.deploymentBytecode, + signer + ); + const contract = await factory.deploy(); + await contract.inc(); + + const block = await this.env.ethers.provider.getBlock("latest"); + + assertIsNotNull(block); + assertIsNotNull(block.hash); + + const result = await this.env.ethers.provider.call({ + from: signer.address, + to: contract, + data: "0x3fa4f245", // value() + blockTag: block.hash, + }); + + assert.strictEqual( + result, + "0x0000000000000000000000000000000000000000000000000000000000000001" + ); + }); + }); + + describe("broadcastTransaction", function () { + it("should send a raw transaction", async function () { + await this.env.ethers.provider.send("hardhat_reset"); + // private key of the first unlocked account + const wallet = new this.env.ethers.Wallet( + "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", + this.env.ethers.provider + ); + const rawTx = await wallet.signTransaction({ + to: this.env.ethers.ZeroAddress, + chainId: 31337, + gasPrice: 100n * 10n ** 9n, + gasLimit: 21_000, + }); + + const tx = await this.env.ethers.provider.broadcastTransaction(rawTx); + + assert.strictEqual(tx.from, wallet.address); + assert.strictEqual(tx.to, this.env.ethers.ZeroAddress); + assert.strictEqual(tx.gasLimit, 21_000n); + }); + }); + + describe("getBlock", function () { + it("should accept latest and earliest block tags", async function () { + await this.env.ethers.provider.send("hardhat_reset"); + await this.env.ethers.provider.send("hardhat_mine"); + await this.env.ethers.provider.send("hardhat_mine"); + await this.env.ethers.provider.send("hardhat_mine"); + + const latestBlock = await this.env.ethers.provider.getBlock("latest"); + assertIsNotNull(latestBlock); + assert.strictEqual(latestBlock.number, 3); + + const earliestBlock = await this.env.ethers.provider.getBlock("earliest"); + assertIsNotNull(earliestBlock); + assert.strictEqual(earliestBlock.number, 0); + }); + + it("should accept numbers", async function () { + await this.env.ethers.provider.send("hardhat_reset"); + await this.env.ethers.provider.send("hardhat_mine"); + await this.env.ethers.provider.send("hardhat_mine"); + await this.env.ethers.provider.send("hardhat_mine"); + + const latestBlock = await this.env.ethers.provider.getBlock(3); + assertIsNotNull(latestBlock); + assert.strictEqual(latestBlock.number, 3); + + const earliestBlock = await this.env.ethers.provider.getBlock(1); + assertIsNotNull(earliestBlock); + assert.strictEqual(earliestBlock.number, 1); + }); + + it("should accept block hashes", async function () { + await this.env.ethers.provider.send("hardhat_reset"); + + const blockByNumber = await this.env.ethers.provider.getBlock(0); + assertIsNotNull(blockByNumber); + assertIsNotNull(blockByNumber.hash); + + const blockByHash = await this.env.ethers.provider.getBlock( + blockByNumber.hash + ); + assertIsNotNull(blockByHash); + + assert.strictEqual(blockByNumber.number, blockByHash.number); + assert.strictEqual(blockByNumber.hash, blockByHash.hash); + }); + + it("shouldn't prefetch transactions by default", async function () { + const signer = await this.env.ethers.provider.getSigner(0); + const tx = await signer.sendTransaction({ to: signer.address }); + + const block = await this.env.ethers.provider.getBlock("latest"); + assertIsNotNull(block); + + assert.lengthOf(block.transactions, 1); + assert.strictEqual(block.transactions[0], tx.hash); + + assert.throws(() => block.prefetchedTransactions); + }); + + it("shouldn't prefetch transactions if false is passed", async function () { + const signer = await this.env.ethers.provider.getSigner(0); + const tx = await signer.sendTransaction({ to: signer.address }); + + const block = await this.env.ethers.provider.getBlock("latest", false); + assertIsNotNull(block); + + assert.lengthOf(block.transactions, 1); + assert.strictEqual(block.transactions[0], tx.hash); + + assert.throws(() => block.prefetchedTransactions); + }); + + it("should prefetch transactions if true is passed", async function () { + const signer = await this.env.ethers.provider.getSigner(0); + const tx = await signer.sendTransaction({ to: signer.address }); + + const block = await this.env.ethers.provider.getBlock("latest", true); + assertIsNotNull(block); + + assert.lengthOf(block.transactions, 1); + assert.strictEqual(block.transactions[0], tx.hash); + + assert.lengthOf(block.prefetchedTransactions, 1); + assert.strictEqual(block.prefetchedTransactions[0].hash, tx.hash); + assert.strictEqual(block.prefetchedTransactions[0].from, signer.address); + }); + }); + + describe("getTransaction", function () { + it("should get a transaction by its hash", async function () { + const signer = await this.env.ethers.provider.getSigner(0); + const sentTx = await signer.sendTransaction({ to: signer.address }); + + const fetchedTx = await this.env.ethers.provider.getTransaction( + sentTx.hash + ); + + assertIsNotNull(fetchedTx); + assert.strictEqual(fetchedTx.hash, sentTx.hash); + }); + + it("should return null if the transaction doesn't exist", async function () { + const tx = await this.env.ethers.provider.getTransaction( + "0x0000000000000000000000000000000000000000000000000000000000000000" + ); + + assert.isNull(tx); + }); + }); + + describe("getTransactionReceipt", function () { + it("should get a receipt by the transaction hash", async function () { + const signer = await this.env.ethers.provider.getSigner(0); + const tx = await signer.sendTransaction({ to: signer.address }); + + const receipt = await this.env.ethers.provider.getTransactionReceipt( + tx.hash + ); + + assertIsNotNull(receipt); + assert.strictEqual(receipt.hash, tx.hash); + assert.strictEqual(receipt.status, 1); + }); + + it("should return null if the transaction doesn't exist", async function () { + const receipt = await this.env.ethers.provider.getTransactionReceipt( + "0x0000000000000000000000000000000000000000000000000000000000000000" + ); + + assert.isNull(receipt); + }); + }); + + describe("getLogs", function () { + // keccak("Inc()") + const INC_EVENT_TOPIC = + "0xccf19ee637b3555bb918b8270dfab3f2b4ec60236d1ab717296aa85d6921224f"; + // keccak("AnotherEvent()") + const ANOTHER_EVENT_TOPIC = + "0x601d819e31a3cd164f83f7a7cf9cb5042ab1acff87b773c68f63d059c0af2dc0"; + + it("should get the logs from the latest block by default", async function () { + const signer = await this.env.ethers.provider.getSigner(0); + const factory = new this.env.ethers.ContractFactory<[], ExampleContract>( + EXAMPLE_CONTRACT.abi, + EXAMPLE_CONTRACT.deploymentBytecode, + signer + ); + const contract = await factory.deploy(); + + await contract.inc(); + + const logs = await this.env.ethers.provider.getLogs({}); + + assert.lengthOf(logs, 1); + + const log = logs[0]; + assert.strictEqual(log.address, await contract.getAddress()); + assert.lengthOf(log.topics, 1); + assert.strictEqual(log.topics[0], INC_EVENT_TOPIC); + }); + + it("should get the logs by block number", async function () { + const signer = await this.env.ethers.provider.getSigner(0); + const factory = new this.env.ethers.ContractFactory<[], ExampleContract>( + EXAMPLE_CONTRACT.abi, + EXAMPLE_CONTRACT.deploymentBytecode, + signer + ); + const contract = await factory.deploy(); + + await contract.inc(); + await this.env.ethers.provider.send("hardhat_mine"); + const blockNumber = await this.env.ethers.provider.getBlockNumber(); + + // latest block shouldn't have logs + const latestBlockLogs = await this.env.ethers.provider.getLogs({ + fromBlock: blockNumber, + toBlock: blockNumber, + }); + assert.lengthOf(latestBlockLogs, 0); + + const logs = await this.env.ethers.provider.getLogs({ + fromBlock: blockNumber - 1, + toBlock: blockNumber - 1, + }); + + assert.lengthOf(logs, 1); + + const log = logs[0]; + assert.strictEqual(log.address, await contract.getAddress()); + assert.lengthOf(log.topics, 1); + assert.strictEqual(log.topics[0], INC_EVENT_TOPIC); + }); + + it("should get the logs by address", async function () { + const signer = await this.env.ethers.provider.getSigner(0); + const factory = new this.env.ethers.ContractFactory<[], ExampleContract>( + EXAMPLE_CONTRACT.abi, + EXAMPLE_CONTRACT.deploymentBytecode, + signer + ); + const contract1 = await factory.deploy(); + const contract2 = await factory.deploy(); + + await contract1.inc(); + await contract2.inc(); + const blockNumber = await this.env.ethers.provider.getBlockNumber(); + + const logs = await this.env.ethers.provider.getLogs({ + fromBlock: blockNumber - 1, + toBlock: blockNumber, + }); + + assert.lengthOf(logs, 2); + + const logsByAddress = await this.env.ethers.provider.getLogs({ + address: await contract1.getAddress(), + fromBlock: blockNumber - 1, + toBlock: blockNumber, + }); + + assert.lengthOf(logsByAddress, 1); + assert.strictEqual( + logsByAddress[0].address, + await contract1.getAddress() + ); + }); + + it("should get the logs by an array of addresses", async function () { + const signer = await this.env.ethers.provider.getSigner(0); + const factory = new this.env.ethers.ContractFactory<[], ExampleContract>( + EXAMPLE_CONTRACT.abi, + EXAMPLE_CONTRACT.deploymentBytecode, + signer + ); + const contract1 = await factory.deploy(); + const contract2 = await factory.deploy(); + const contract3 = await factory.deploy(); + + await contract1.inc(); + await contract2.inc(); + await contract3.inc(); + const blockNumber = await this.env.ethers.provider.getBlockNumber(); + + const logs = await this.env.ethers.provider.getLogs({ + fromBlock: blockNumber - 2, + toBlock: blockNumber, + }); + + assert.lengthOf(logs, 3); + + const contract1Address = await contract1.getAddress(); + const contract2Address = await contract2.getAddress(); + const logsByAddress = await this.env.ethers.provider.getLogs({ + address: [contract1Address, contract2Address], + fromBlock: blockNumber - 2, + toBlock: blockNumber, + }); + + assert.lengthOf(logsByAddress, 2); + assert.strictEqual(logsByAddress[0].address, contract1Address); + assert.strictEqual(logsByAddress[1].address, contract2Address); + }); + + it("should get the logs by topic", async function () { + const signer = await this.env.ethers.provider.getSigner(0); + const factory = new this.env.ethers.ContractFactory<[], ExampleContract>( + EXAMPLE_CONTRACT.abi, + EXAMPLE_CONTRACT.deploymentBytecode, + signer + ); + const contract = await factory.deploy(); + + await contract.emitsTwoEvents(); + + const logs = await this.env.ethers.provider.getLogs({}); + assert.lengthOf(logs, 2); + + const incEventLogs = await this.env.ethers.provider.getLogs({ + topics: [INC_EVENT_TOPIC], + }); + + assert.lengthOf(incEventLogs, 1); + assert.lengthOf(incEventLogs[0].topics, 1); + assert.strictEqual(incEventLogs[0].topics[0], INC_EVENT_TOPIC); + + const anotherEventLogs = await this.env.ethers.provider.getLogs({ + topics: [ANOTHER_EVENT_TOPIC], + }); + + assert.lengthOf(anotherEventLogs, 1); + assert.lengthOf(anotherEventLogs[0].topics, 1); + assert.strictEqual(anotherEventLogs[0].topics[0], ANOTHER_EVENT_TOPIC); + }); + }); +}); diff --git a/packages/hardhat-ethers/test/custom-ethers-signer.ts b/packages/hardhat-ethers/test/custom-ethers-signer.ts new file mode 100644 index 0000000000..2bb725b4df --- /dev/null +++ b/packages/hardhat-ethers/test/custom-ethers-signer.ts @@ -0,0 +1,299 @@ +import { assert } from "chai"; + +import { ExampleContract, EXAMPLE_CONTRACT } from "./example-contracts"; +import { + assertIsNotNull, + assertWithin, + usePersistentEnvironment, +} from "./helpers"; + +describe("custom signer", function () { + describe("minimal project", function () { + usePersistentEnvironment("minimal-project"); + + it("has an address field that matches the address", async function () { + const signer = await this.env.ethers.provider.getSigner(0); + + assert.isString(signer.address); + assert.strictEqual(signer.address, await signer.getAddress()); + }); + + it("can be connected to a provider", async function () { + if ( + process.env.INFURA_URL === undefined || + process.env.INFURA_URL === "" + ) { + this.skip(); + } + + const signerConnectedToHardhat = await this.env.ethers.provider.getSigner( + 0 + ); + + const nonceInHardhat = await signerConnectedToHardhat.getNonce(); + + const mainnetProvider = new this.env.ethers.JsonRpcProvider( + process.env.INFURA_URL + ); + + const signerConnectedToMainnet = + signerConnectedToHardhat.connect(mainnetProvider); + + const nonceInMainnet = await signerConnectedToMainnet.getNonce(); + + assert.strictEqual(nonceInHardhat, 0); + assert.isAbove(nonceInMainnet, 0); + }); + + it("can get the nonce of the signer", async function () { + const signer = await this.env.ethers.provider.getSigner(0); + + assert.strictEqual(await signer.getNonce(), 0); + + await signer.sendTransaction({ to: signer }); + assert.strictEqual(await signer.getNonce(), 1); + }); + + it("should populate a call/tx", async function () { + const signer = await this.env.ethers.provider.getSigner(0); + + const populatedCall = await signer.populateCall({ + to: signer, + }); + + assert.strictEqual(populatedCall.from, signer.address); + + // populateTransaction does exactly the same + const populatedTx = await signer.populateCall({ + to: signer, + }); + + assert.strictEqual(populatedTx.from, signer.address); + }); + + describe("estimateGas", function () { + it("should estimate gas for a value transaction", async function () { + const signer = await this.env.ethers.provider.getSigner(0); + const gasEstimation = await signer.estimateGas({ + to: signer, + }); + + assert.strictEqual(Number(gasEstimation), 21_001); + }); + + it("should estimate gas for a contract call", async function () { + const signer = await this.env.ethers.provider.getSigner(0); + const factory = new this.env.ethers.ContractFactory< + [], + ExampleContract + >(EXAMPLE_CONTRACT.abi, EXAMPLE_CONTRACT.deploymentBytecode, signer); + const contract = await factory.deploy(); + + const gasEstimation = await signer.estimateGas({ + to: contract, + data: "0x371303c0", // inc() + }); + + assertWithin(Number(gasEstimation), 65_000, 70_000); + }); + }); + + describe("call", function () { + it("should make a contract call", async function () { + const signer = await this.env.ethers.provider.getSigner(0); + const factory = new this.env.ethers.ContractFactory< + [], + ExampleContract + >(EXAMPLE_CONTRACT.abi, EXAMPLE_CONTRACT.deploymentBytecode, signer); + const contract = await factory.deploy(); + await contract.inc(); + + const result = await signer.call({ + to: contract, + data: "0x3fa4f245", // value() + }); + + assert.strictEqual( + result, + "0x0000000000000000000000000000000000000000000000000000000000000001" + ); + }); + }); + + describe("sendTransaction", function () { + it("should send a transaction", async function () { + const sender = await this.env.ethers.provider.getSigner(0); + const receiver = await this.env.ethers.provider.getSigner(1); + + const balanceBefore = await this.env.ethers.provider.getBalance( + receiver + ); + + await sender.sendTransaction({ + to: receiver, + value: this.env.ethers.parseEther("1"), + }); + + const balanceAfter = await this.env.ethers.provider.getBalance( + receiver + ); + + const balanceDifference = balanceAfter - balanceBefore; + + assert.strictEqual(balanceDifference, 10n ** 18n); + }); + }); + + describe("signMessage", function () { + it("should sign a message", async function () { + const signer = await this.env.ethers.provider.getSigner(0); + + const signedMessage = await signer.signMessage("hello"); + + assert.strictEqual( + signedMessage, + "0xf16ea9a3478698f695fd1401bfe27e9e4a7e8e3da94aa72b021125e31fa899cc573c48ea3fe1d4ab61a9db10c19032026e3ed2dbccba5a178235ac27f94504311c" + ); + }); + }); + + describe("signTypedData", function () { + const types = { + Person: [ + { name: "name", type: "string" }, + { name: "wallet", type: "address" }, + ], + Mail: [ + { name: "from", type: "Person" }, + { name: "to", type: "Person" }, + { name: "contents", type: "string" }, + ], + }; + + const data = { + from: { + name: "John", + wallet: "0x0000000000000000000000000000000000000001", + }, + to: { + name: "Mark", + wallet: "0x0000000000000000000000000000000000000002", + }, + contents: "something", + }; + + it("should sign data", async function () { + const signer = await this.env.ethers.provider.getSigner(0); + + const signedData = await signer.signTypedData( + { + chainId: 31337, + }, + types, + data + ); + + assert.strictEqual( + signedData, + "0xbea20009786d1f69327eea384d6b8082f2d35b41212d1acbbd490516f0ae776748e93d4603df49033f89ce6a97afba4523d753d35e962ea431cc706642ad713f1b" + ); + }); + + it("should use the chain id", async function () { + const signer = await this.env.ethers.provider.getSigner(0); + + const signedData = await signer.signTypedData( + { + chainId: 10101, + }, + types, + data + ); + + // we get a different value from the different test because we changed the + // chainId + assert.strictEqual( + signedData, + "0x8a6a6aeca0cf03dbffd6d7b15207c0dcf5c7daa432e510b5de1ebecff8de6cd457e2eaa9fe96c11474a7344584f4b128c773153836142647c426b5f2c3eb6c701b" + ); + }); + }); + + describe("default gas limit", function () { + it("should use the block gas limit for the in-process hardhat network", async function () { + const signer = await this.env.ethers.provider.getSigner(0); + + const tx = await signer.sendTransaction({ to: signer }); + + if (!("blockGasLimit" in this.env.network.config)) { + assert.fail("test should be run in the hardhat network"); + } + + const blockGasLimit = this.env.network.config.blockGasLimit; + assert.strictEqual(Number(tx.gasLimit), blockGasLimit); + }); + + it("should use custom gas limit, if provided", async function () { + const signer = await this.env.ethers.provider.getSigner(0); + + const tx = await signer.sendTransaction({ + to: signer, + gasLimit: 30_000, + }); + + assert.strictEqual(tx.gasLimit, 30_000n); + }); + }); + + describe("nonce management", function () { + it("should send a second transaction with the right nonce if the first one wasn't mined", async function () { + const signer = await this.env.ethers.provider.getSigner(0); + + await this.env.ethers.provider.send("evm_setAutomine", [false]); + + const tx1 = await signer.sendTransaction({ + to: signer, + gasLimit: 30_000, + }); + const tx2 = await signer.sendTransaction({ + to: signer, + gasLimit: 30_000, + }); + + assert.notEqual(tx1.nonce, tx2.nonce); + assert.strictEqual(tx2.nonce, tx1.nonce + 1); + + await this.env.ethers.provider.send("hardhat_mine", []); + + const latestBlock = await this.env.ethers.provider.getBlock("latest"); + + assertIsNotNull(latestBlock); + + assert.lengthOf(latestBlock.transactions, 2); + }); + }); + }); + + describe('project with gas set to "auto"', function () { + usePersistentEnvironment("hardhat-project-with-gas-auto"); + + it("should estimate the gas of the transaction", async function () { + const signer = await this.env.ethers.provider.getSigner(0); + + const tx = await signer.sendTransaction({ to: signer }); + + assert.strictEqual(tx.gasLimit, 21_001n); + }); + + it("should use custom gas limit, if provided", async function () { + const signer = await this.env.ethers.provider.getSigner(0); + + const tx = await signer.sendTransaction({ + to: signer, + gasLimit: 30_000, + }); + + assert.strictEqual(tx.gasLimit, 30_000n); + }); + }); +}); diff --git a/packages/hardhat-ethers/test/ethers-provider-wrapper.ts b/packages/hardhat-ethers/test/ethers-provider-wrapper.ts deleted file mode 100644 index d1f730195f..0000000000 --- a/packages/hardhat-ethers/test/ethers-provider-wrapper.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { assert } from "chai"; -import { ethers } from "ethers"; - -import { EthersProviderWrapper } from "../src/internal/ethers-provider-wrapper"; - -import { useEnvironment } from "./helpers"; - -describe("Ethers provider wrapper", function () { - let realProvider: ethers.providers.JsonRpcProvider; - let wrapper: EthersProviderWrapper; - - useEnvironment("hardhat-project"); - - beforeEach(function () { - realProvider = new ethers.providers.JsonRpcProvider( - "http://127.0.0.1:8545" - ); - wrapper = new EthersProviderWrapper(this.env.network.provider); - }); - - it("Should return the same as the real provider", async function () { - const response = await realProvider.send("eth_accounts", []); - const response2 = await wrapper.send("eth_accounts", []); - - assert.deepEqual(response, response2); - }); - - it("Should return the same error", async function () { - this.skip(); - // We disable this test for RskJ - // See: https://github.com/rsksmart/rskj/issues/876 - const version: string = await this.env.network.provider.send( - "web3_clientVersion" - ); - if (version.includes("RskJ")) { - this.skip(); - } - - try { - await realProvider.send("error_please", []); - assert.fail("Ethers provider should have failed"); - } catch (err: any) { - try { - await wrapper.send("error_please", []); - assert.fail("Wrapped provider should have failed"); - } catch (err2: any) { - assert.deepEqual(err2.message, err.message); - } - } - }); -}); diff --git a/packages/hardhat-ethers/test/example-contracts.ts b/packages/hardhat-ethers/test/example-contracts.ts new file mode 100644 index 0000000000..7f463fcd38 --- /dev/null +++ b/packages/hardhat-ethers/test/example-contracts.ts @@ -0,0 +1,58 @@ +import type { + BaseContract, + BaseContractMethod, + BigNumberish, + ContractTransactionResponse, +} from "ethers"; + +/* +contract Example { + uint public value; + uint public doubleValue; + event Inc(); + event AnotherEvent(); + + constructor() payable {} + + function inc() public { + value++; + doubleValue = 2 * value; + emit Inc(); + } + + function emitsTwoEvents() public { + emit Inc(); + emit AnotherEvent(); + } +} +*/ +export const EXAMPLE_CONTRACT = { + deploymentBytecode: + "0x6080604052610284806100136000396000f3fe608060405234801561001057600080fd5b506004361061004c5760003560e01c8063371303c0146100515780633fa4f2451461005b578063b190115914610079578063e377818414610083575b600080fd5b6100596100a1565b005b6100636100fb565b604051610070919061017a565b60405180910390f35b610081610101565b005b61008b61015b565b604051610098919061017a565b60405180910390f35b6000808154809291906100b3906101c4565b919050555060005460026100c7919061020c565b6001819055507fccf19ee637b3555bb918b8270dfab3f2b4ec60236d1ab717296aa85d6921224f60405160405180910390a1565b60005481565b7fccf19ee637b3555bb918b8270dfab3f2b4ec60236d1ab717296aa85d6921224f60405160405180910390a17f601d819e31a3cd164f83f7a7cf9cb5042ab1acff87b773c68f63d059c0af2dc060405160405180910390a1565b60015481565b6000819050919050565b61017481610161565b82525050565b600060208201905061018f600083018461016b565b92915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60006101cf82610161565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff820361020157610200610195565b5b600182019050919050565b600061021782610161565b915061022283610161565b925082820261023081610161565b9150828204841483151761024757610246610195565b5b509291505056fea2646970667358221220bedfe038de0cf21194c025de5a282c3415bf29f716ef1af0073bc2c45d803e8164736f6c63430008110033", + abi: [ + "event Inc()", + "event AnotherEvent()", + "function value() public view returns (uint256)", + "function doubleValue() public view returns (uint256)", + "function inc() public", + "function emitsTwoEvents() public", + ], +}; + +export type ExampleContract = BaseContract & { + inc: BaseContractMethod<[], void, ContractTransactionResponse>; + emitsTwoEvents: BaseContractMethod<[], void, ContractTransactionResponse>; +}; + +export type TestContractLib = BaseContract & { + printNumber: BaseContractMethod< + [BigNumberish], + bigint, + ContractTransactionResponse + >; +}; + +export type GreeterContract = BaseContract & { + greet: BaseContractMethod<[], string, string>; + setGreeting: BaseContractMethod<[string], void, ContractTransactionResponse>; +}; diff --git a/packages/hardhat-ethers/test/fixture-projects/hardhat-project-with-gas-auto/.gitignore b/packages/hardhat-ethers/test/fixture-projects/hardhat-project-with-gas-auto/.gitignore new file mode 100644 index 0000000000..4a4ecc528f --- /dev/null +++ b/packages/hardhat-ethers/test/fixture-projects/hardhat-project-with-gas-auto/.gitignore @@ -0,0 +1,2 @@ +cache/ +artifacts/ diff --git a/packages/hardhat-ethers/test/fixture-projects/hardhat-project-with-gas-auto/hardhat.config.js b/packages/hardhat-ethers/test/fixture-projects/hardhat-project-with-gas-auto/hardhat.config.js new file mode 100644 index 0000000000..18a8d57186 --- /dev/null +++ b/packages/hardhat-ethers/test/fixture-projects/hardhat-project-with-gas-auto/hardhat.config.js @@ -0,0 +1,10 @@ +require("../../../src/internal/index"); + +module.exports = { + solidity: "0.5.15", + networks: { + hardhat: { + gas: "auto", + }, + }, +}; diff --git a/packages/hardhat-ethers/test/fixture-projects/minimal-project/hardhat.config.js b/packages/hardhat-ethers/test/fixture-projects/minimal-project/hardhat.config.js new file mode 100644 index 0000000000..c1f3384738 --- /dev/null +++ b/packages/hardhat-ethers/test/fixture-projects/minimal-project/hardhat.config.js @@ -0,0 +1,5 @@ +require("../../../src/internal/index"); + +module.exports = { + solidity: "0.8.0", +}; diff --git a/packages/hardhat-ethers/test/helpers.ts b/packages/hardhat-ethers/test/helpers.ts index 17642ecd83..318ad7e5b6 100644 --- a/packages/hardhat-ethers/test/helpers.ts +++ b/packages/hardhat-ethers/test/helpers.ts @@ -1,3 +1,5 @@ +import { assert } from "chai"; +import { ContractRunner, Signer } from "ethers"; import { resetHardhatContext } from "hardhat/plugins-testing"; import { HardhatRuntimeEnvironment } from "hardhat/types"; import path from "path"; @@ -13,7 +15,7 @@ declare module "mocha" { export function useEnvironment( fixtureProjectName: string, - networkName = "localhost" + networkName = "hardhat" ) { beforeEach("Loading hardhat environment", function () { process.chdir(path.join(__dirname, "fixture-projects", fixtureProjectName)); @@ -26,3 +28,43 @@ export function useEnvironment( resetHardhatContext(); }); } + +export function usePersistentEnvironment( + fixtureProjectName: string, + networkName = "hardhat" +) { + before("Loading hardhat environment", function () { + process.chdir(path.join(__dirname, "fixture-projects", fixtureProjectName)); + process.env.HARDHAT_NETWORK = networkName; + + this.env = require("hardhat"); + }); + + after("Resetting hardhat", function () { + resetHardhatContext(); + }); +} + +export function assertWithin( + value: number | bigint, + min: number | bigint, + max: number | bigint +) { + if (value < min || value > max) { + assert.fail(`Expected ${value} to be between ${min} and ${max}`); + } +} + +export function assertIsNotNull( + value: T +): asserts value is Exclude { + assert.isNotNull(value); +} + +export function assertIsSigner( + value: ContractRunner | null +): asserts value is Signer { + assertIsNotNull(value); + assert.isTrue("getAddress" in value); + assert.isTrue("signTransaction" in value); +} diff --git a/packages/hardhat-ethers/test/index.ts b/packages/hardhat-ethers/test/index.ts index 50bd7757d8..4ae4bc9442 100644 --- a/packages/hardhat-ethers/test/index.ts +++ b/packages/hardhat-ethers/test/index.ts @@ -1,19 +1,21 @@ +import type { ethers as EthersT } from "ethers"; import chai, { assert } from "chai"; import chaiAsPromised from "chai-as-promised"; -import { ethers, Signer } from "ethers"; +import { ethers } from "ethers"; import { NomicLabsHardhatPluginError } from "hardhat/plugins"; import { Artifact } from "hardhat/types"; -import util from "util"; -import { EthersProviderWrapper } from "../src/internal/ethers-provider-wrapper"; +import { CustomEthersSigner } from "../src/signers"; +import { GreeterContract, TestContractLib } from "./example-contracts"; -import { useEnvironment } from "./helpers"; +import { assertIsSigner, useEnvironment } from "./helpers"; chai.use(chaiAsPromised); describe("Ethers plugin", function () { describe("ganache", function () { - useEnvironment("hardhat-project"); + useEnvironment("hardhat-project", "localhost"); + describe("HRE extensions", function () { it("should extend hardhat runtime environment", function () { assert.isDefined(this.env.ethers); @@ -26,84 +28,6 @@ describe("Ethers plugin", function () { ...Object.keys(ethers), ]); }); - - describe("Custom formatters", function () { - const assertBigNumberFormat = function ( - BigNumber: any, - value: string | number, - expected: string - ) { - assert.equal(util.format("%o", BigNumber.from(value)), expected); - }; - - describe("BigNumber", function () { - it("should format zero unaltered", function () { - assertBigNumberFormat( - this.env.ethers.BigNumber, - 0, - 'BigNumber { value: "0" }' - ); - }); - - it("should provide human readable versions of positive integers", function () { - const BigNumber = this.env.ethers.BigNumber; - - assertBigNumberFormat(BigNumber, 1, 'BigNumber { value: "1" }'); - assertBigNumberFormat(BigNumber, 999, 'BigNumber { value: "999" }'); - assertBigNumberFormat( - BigNumber, - 1000, - 'BigNumber { value: "1000" }' - ); - assertBigNumberFormat( - BigNumber, - 999999, - 'BigNumber { value: "999999" }' - ); - assertBigNumberFormat( - BigNumber, - 1000000, - 'BigNumber { value: "1000000" }' - ); - assertBigNumberFormat( - BigNumber, - "999999999999999999292", - 'BigNumber { value: "999999999999999999292" }' - ); - }); - - it("should provide human readable versions of negative integers", function () { - const BigNumber = this.env.ethers.BigNumber; - - assertBigNumberFormat(BigNumber, -1, 'BigNumber { value: "-1" }'); - assertBigNumberFormat( - BigNumber, - -999, - 'BigNumber { value: "-999" }' - ); - assertBigNumberFormat( - BigNumber, - -1000, - 'BigNumber { value: "-1000" }' - ); - assertBigNumberFormat( - BigNumber, - -999999, - 'BigNumber { value: "-999999" }' - ); - assertBigNumberFormat( - BigNumber, - -1000000, - 'BigNumber { value: "-1000000" }' - ); - assertBigNumberFormat( - BigNumber, - "-999999999999999999292", - 'BigNumber { value: "-999999999999999999292" }' - ); - }); - }); - }); }); describe("Provider", function () { @@ -112,12 +36,15 @@ describe("Ethers plugin", function () { "eth_accounts", [] ); - assert.equal(accounts[0], "0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1"); + assert.strictEqual( + accounts[0], + "0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1" + ); }); }); describe("Signers and contracts helpers", function () { - let signers: ethers.Signer[]; + let signers: CustomEthersSigner[]; let greeterArtifact: Artifact; let iGreeterArtifact: Artifact; @@ -132,7 +59,7 @@ describe("Ethers plugin", function () { describe("getSigners", function () { it("should return the signers", async function () { const sigs = await this.env.ethers.getSigners(); - assert.equal( + assert.strictEqual( await sigs[0].getAddress(), "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1" ); @@ -140,7 +67,7 @@ describe("Ethers plugin", function () { it("should expose the address synchronously", async function () { const sigs = await this.env.ethers.getSigners(); - assert.equal( + assert.strictEqual( sigs[0].address, "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1" ); @@ -171,7 +98,7 @@ describe("Ethers plugin", function () { const result = await sig.signMessage("hello"); - assert.equal( + assert.strictEqual( result, "0x1845faa75f53acb0c3e7247dcf294ce045c139722418dc9638709b54bafffa093591aeaaa195e7dc53f7e774c80e9a7f1371f0647a100d1c9e81db83d8ddd47801" ); @@ -181,40 +108,54 @@ describe("Ethers plugin", function () { const [sig] = await this.env.ethers.getSigners(); const Greeter = await this.env.ethers.getContractFactory("Greeter"); - const tx = Greeter.getDeployTransaction(); + const tx = await Greeter.getDeployTransaction(); - assert.throws(() => sig.signTransaction(tx)); + await assert.isRejected(sig.signTransaction(tx)); }); - it("should return the balance of the account", async function () { + // `signer.getBalance` is not present in ethers v6; we should re-enable + // this test when/if it's added back + it.skip("should return the balance of the account", async function () { const [sig] = await this.env.ethers.getSigners(); - assert.equal( + assert.strictEqual( + // @ts-expect-error (await sig.getBalance()).toString(), "100000000000000000000" ); }); + it("should return the balance of the account", async function () { + const [sig] = await this.env.ethers.getSigners(); + assert.strictEqual( + await this.env.ethers.provider.getBalance(sig), + 100000000000000000000n + ); + }); + it("should return the transaction count of the account", async function () { const [sig] = await this.env.ethers.getSigners(); - assert.equal((await sig.getTransactionCount()).toString(), "0"); + assert.strictEqual( + await this.env.ethers.provider.getTransactionCount(sig), + 0 + ); }); it("should allow to use the estimateGas method", async function () { const [sig] = await this.env.ethers.getSigners(); const Greeter = await this.env.ethers.getContractFactory("Greeter"); - const tx = Greeter.getDeployTransaction(); + const tx = await Greeter.getDeployTransaction(); const result = await sig.estimateGas(tx); - assert.isTrue(result.gt(0)); + assert.isTrue(result > 0n); }); it("should allow to use the call method", async function () { const [sig] = await this.env.ethers.getSigners(); const Greeter = await this.env.ethers.getContractFactory("Greeter"); - const tx = Greeter.getDeployTransaction(); + const tx = await Greeter.getDeployTransaction(); const result = await sig.call(tx); @@ -225,46 +166,39 @@ describe("Ethers plugin", function () { const [sig] = await this.env.ethers.getSigners(); const Greeter = await this.env.ethers.getContractFactory("Greeter"); - const tx = Greeter.getDeployTransaction(); + const tx = await Greeter.getDeployTransaction(); const response = await sig.sendTransaction(tx); const receipt = await response.wait(); - assert.equal(receipt.status, 1); + if (receipt === null) { + assert.fail("receipt shoudn't be null"); + } + assert.strictEqual(receipt.status, 1); }); it("should get the chainId", async function () { - const [sig] = await this.env.ethers.getSigners(); + const { chainId } = await this.env.ethers.provider.getNetwork(); - const chainId = await sig.getChainId(); - - assert.equal(chainId, 1337); + assert.strictEqual(chainId, 1337n); }); it("should get the gas price", async function () { - const [sig] = await this.env.ethers.getSigners(); - - const gasPrice = await sig.getGasPrice(); + const feeData = await this.env.ethers.provider.getFeeData(); - assert.equal(gasPrice.toString(), "20000000000"); + assert.strictEqual(feeData.gasPrice, 20000000000n); }); - it("should check and populate a transaction", async function () { + it("should populate a transaction", async function () { const [sig] = await this.env.ethers.getSigners(); const Greeter = await this.env.ethers.getContractFactory("Greeter"); - const tx = Greeter.getDeployTransaction(); + const tx = await Greeter.getDeployTransaction(); - const checkedTransaction = sig.checkTransaction(tx); + const populatedTransaction = await sig.populateTransaction(tx); - assert.equal(await checkedTransaction.from, sig.address); - - const populatedTransaction = await sig.populateTransaction( - checkedTransaction - ); - - assert.equal(populatedTransaction.from, sig.address); + assert.strictEqual(populatedTransaction.from, sig.address); }); }); @@ -276,13 +210,15 @@ describe("Ethers plugin", function () { "Greeter" ); - assert.containsAllKeys(contract.interface.functions, [ - "setGreeting(string)", - "greet()", - ]); + assert.isNotNull(contract.interface.getFunction("greet")); + assert.isNotNull(contract.interface.getFunction("setGreeting")); + + // non-existent functions should be null + assert.isNull(contract.interface.getFunction("doesntExist")); + assertIsSigner(contract.runner); - assert.equal( - await contract.signer.getAddress(), + assert.strictEqual( + await contract.runner.getAddress(), await signers[0].getAddress() ); }); @@ -315,19 +251,22 @@ describe("Ethers plugin", function () { ); const library = await libraryFactory.deploy(); - const contractFactory = await this.env.ethers.getContractFactory( - "TestContractLib", - { libraries: { TestLibrary: library.address } } - ); - assert.equal( - await contractFactory.signer.getAddress(), + const contractFactory = await this.env.ethers.getContractFactory< + [], + TestContractLib + >("TestContractLib", { + libraries: { TestLibrary: library.target }, + }); + assertIsSigner(contractFactory.runner); + assert.strictEqual( + await contractFactory.runner.getAddress(), await signers[0].getAddress() ); const numberPrinter = await contractFactory.deploy(); - const someNumber = 50; - assert.equal( - await numberPrinter.callStatic.printNumber(someNumber), - someNumber * 2 + const someNumber = 50n; + assert.strictEqual( + await numberPrinter.printNumber.staticCall(someNumber), + someNumber * 2n ); }); @@ -340,8 +279,9 @@ describe("Ethers plugin", function () { try { await this.env.ethers.getContractFactory("TestContractLib", { libraries: { - TestLibrary: library.address, - "contracts/TestContractLib.sol:TestLibrary": library.address, + TestLibrary: await library.getAddress(), + "contracts/TestContractLib.sol:TestLibrary": + await library.getAddress(), }, }); } catch (reason: any) { @@ -379,10 +319,11 @@ describe("Ethers plugin", function () { const contractFactory = await this.env.ethers.getContractFactory( "TestNonUniqueLib", - { libraries: { NonUniqueLibrary: library.address } } + { libraries: { NonUniqueLibrary: await library.getAddress() } } ); - assert.equal( - await contractFactory.signer.getAddress(), + assertIsSigner(contractFactory.runner); + assert.strictEqual( + await contractFactory.runner.getAddress(), await signers[0].getAddress() ); }); @@ -400,9 +341,9 @@ describe("Ethers plugin", function () { try { await this.env.ethers.getContractFactory("TestAmbiguousLib", { libraries: { - AmbiguousLibrary: library.address, + AmbiguousLibrary: await library.getAddress(), "contracts/AmbiguousLibrary2.sol:AmbiguousLibrary": - library2.address, + await library2.getAddress(), }, }); } catch (reason: any) { @@ -490,49 +431,27 @@ describe("Ethers plugin", function () { ); }); - it("should fail to create a contract factory when incorrectly linking a library with an ethers.Contract", async function () { + it("should contract instances as libraries", async function () { const libraryFactory = await this.env.ethers.getContractFactory( "TestLibrary" ); const library = await libraryFactory.deploy(); - try { - await this.env.ethers.getContractFactory("TestContractLib", { - libraries: { TestLibrary: library as any }, - }); - } catch (reason: any) { - assert.instanceOf( - reason, - NomicLabsHardhatPluginError, - "getContractFactory should fail with a hardhat plugin error" - ); - assert.isTrue( - reason.message.includes( - "invalid address", - "getContractFactory should report the invalid address as the cause" - ) - ); - // This assert is here just to make sure we don't end up printing an enormous object - // in the error message. This may happen if the argument received is particularly complex. - assert.isTrue( - reason.message.length <= 400, - "getContractFactory should fail with an error message that isn't too large" - ); - return; - } - - assert.fail( - "getContractFactory should fail to create a contract factory if there is an invalid address" - ); + await this.env.ethers.getContractFactory("TestContractLib", { + libraries: { TestLibrary: library }, + }); }); it("Should be able to send txs and make calls", async function () { - const Greeter = await this.env.ethers.getContractFactory("Greeter"); + const Greeter = await this.env.ethers.getContractFactory< + [], + GreeterContract + >("Greeter"); const greeter = await Greeter.deploy(); - assert.equal(await greeter.functions.greet(), "Hi"); - await greeter.functions.setGreeting("Hola"); - assert.equal(await greeter.functions.greet(), "Hola"); + assert.strictEqual(await greeter.greet(), "Hi"); + await greeter.setGreeting("Hola"); + assert.strictEqual(await greeter.greet(), "Hola"); }); describe("with custom signer", function () { @@ -543,13 +462,12 @@ describe("Ethers plugin", function () { signers[1] ); - assert.containsAllKeys(contract.interface.functions, [ - "setGreeting(string)", - "greet()", - ]); + assert.isNotNull(contract.interface.getFunction("greet")); + assert.isNotNull(contract.interface.getFunction("setGreeting")); + assertIsSigner(contract.runner); - assert.equal( - await contract.signer.getAddress(), + assert.strictEqual( + await contract.runner.getAddress(), await signers[1].getAddress() ); }); @@ -564,13 +482,12 @@ describe("Ethers plugin", function () { greeterArtifact.bytecode ); - assert.containsAllKeys(contract.interface.functions, [ - "setGreeting(string)", - "greet()", - ]); + assert.isNotNull(contract.interface.getFunction("greet")); + assert.isNotNull(contract.interface.getFunction("setGreeting")); + assertIsSigner(contract.runner); - assert.equal( - await contract.signer.getAddress(), + assert.strictEqual( + await contract.runner.getAddress(), await signers[0].getAddress() ); }); @@ -580,25 +497,26 @@ describe("Ethers plugin", function () { iGreeterArtifact.abi, iGreeterArtifact.bytecode ); - assert.equal(contract.bytecode, "0x"); - assert.containsAllKeys(contract.interface.functions, ["greet()"]); + assert.strictEqual(contract.bytecode, "0x"); + assert.isNotNull(contract.interface.getFunction("greet")); + assertIsSigner(contract.runner); - assert.equal( - await contract.signer.getAddress(), + assert.strictEqual( + await contract.runner.getAddress(), await signers[0].getAddress() ); }); it("Should be able to send txs and make calls", async function () { - const Greeter = await this.env.ethers.getContractFactory( - greeterArtifact.abi, - greeterArtifact.bytecode - ); + const Greeter = await this.env.ethers.getContractFactory< + [], + GreeterContract + >(greeterArtifact.abi, greeterArtifact.bytecode); const greeter = await Greeter.deploy(); - assert.equal(await greeter.functions.greet(), "Hi"); - await greeter.functions.setGreeting("Hola"); - assert.equal(await greeter.functions.greet(), "Hola"); + assert.strictEqual(await greeter.greet(), "Hi"); + await greeter.setGreeting("Hola"); + assert.strictEqual(await greeter.greet(), "Hola"); }); describe("with custom signer", function () { @@ -610,13 +528,12 @@ describe("Ethers plugin", function () { signers[1] ); - assert.containsAllKeys(contract.interface.functions, [ - "setGreeting(string)", - "greet()", - ]); + assert.isNotNull(contract.interface.getFunction("greet")); + assert.isNotNull(contract.interface.getFunction("setGreeting")); + assertIsSigner(contract.runner); - assert.equal( - await contract.signer.getAddress(), + assert.strictEqual( + await contract.runner.getAddress(), await signers[1].getAddress() ); }); @@ -630,13 +547,12 @@ describe("Ethers plugin", function () { greeterArtifact ); - assert.containsAllKeys(contract.interface.functions, [ - "setGreeting(string)", - "greet()", - ]); + assert.isNotNull(contract.interface.getFunction("greet")); + assert.isNotNull(contract.interface.getFunction("setGreeting")); + assertIsSigner(contract.runner); - assert.equal( - await contract.signer.getAddress(), + assert.strictEqual( + await contract.runner.getAddress(), await signers[0].getAddress() ); }); @@ -652,32 +568,37 @@ describe("Ethers plugin", function () { ); const contractFactory = - await this.env.ethers.getContractFactoryFromArtifact( - testContractLibArtifact, - { libraries: { TestLibrary: library.address } } - ); + await this.env.ethers.getContractFactoryFromArtifact< + [], + TestContractLib + >(testContractLibArtifact, { + libraries: { TestLibrary: await library.getAddress() }, + }); + assertIsSigner(contractFactory.runner); - assert.equal( - await contractFactory.signer.getAddress(), + assert.strictEqual( + await contractFactory.runner.getAddress(), await signers[0].getAddress() ); + const numberPrinter = await contractFactory.deploy(); - const someNumber = 50; - assert.equal( - await numberPrinter.callStatic.printNumber(someNumber), - someNumber * 2 + const someNumber = 50n; + assert.strictEqual( + await numberPrinter.printNumber.staticCall(someNumber), + someNumber * 2n ); }); it("Should be able to send txs and make calls", async function () { - const Greeter = await this.env.ethers.getContractFactoryFromArtifact( - greeterArtifact - ); + const Greeter = await this.env.ethers.getContractFactoryFromArtifact< + [], + GreeterContract + >(greeterArtifact); const greeter = await Greeter.deploy(); - assert.equal(await greeter.functions.greet(), "Hi"); - await greeter.functions.setGreeting("Hola"); - assert.equal(await greeter.functions.greet(), "Hola"); + assert.strictEqual(await greeter.greet(), "Hi"); + await greeter.setGreeting("Hola"); + assert.strictEqual(await greeter.greet(), "Hola"); }); describe("with custom signer", function () { @@ -688,13 +609,12 @@ describe("Ethers plugin", function () { signers[1] ); - assert.containsAllKeys(contract.interface.functions, [ - "setGreeting(string)", - "greet()", - ]); + assert.isNotNull(contract.interface.getFunction("greet")); + assert.isNotNull(contract.interface.getFunction("setGreeting")); + assertIsSigner(contract.runner); - assert.equal( - await contract.signer.getAddress(), + assert.strictEqual( + await contract.runner.getAddress(), await signers[1].getAddress() ); }); @@ -702,10 +622,13 @@ describe("Ethers plugin", function () { }); describe("getContractAt", function () { - let deployedGreeter: ethers.Contract; + let deployedGreeter: GreeterContract; beforeEach(async function () { - const Greeter = await this.env.ethers.getContractFactory("Greeter"); + const Greeter = await this.env.ethers.getContractFactory< + [], + GreeterContract + >("Greeter"); deployedGreeter = await Greeter.deploy(); }); @@ -719,16 +642,15 @@ describe("Ethers plugin", function () { it("Should return an instance of a contract", async function () { const contract = await this.env.ethers.getContractAt( "Greeter", - deployedGreeter.address + deployedGreeter.target ); - assert.containsAllKeys(contract.functions, [ - "setGreeting(string)", - "greet()", - ]); + assert.exists(contract.setGreeting); + assert.exists(contract.greet); + assertIsSigner(contract.runner); - assert.equal( - await contract.signer.getAddress(), + assert.strictEqual( + await contract.runner.getAddress(), await signers[0].getAddress() ); }); @@ -736,13 +658,14 @@ describe("Ethers plugin", function () { it("Should return an instance of an interface", async function () { const contract = await this.env.ethers.getContractAt( "IGreeter", - deployedGreeter.address + deployedGreeter.target ); - assert.containsAllKeys(contract.functions, ["greet()"]); + assert.isNotNull(contract.interface.getFunction("greet")); + assertIsSigner(contract.runner); - assert.equal( - await contract.signer.getAddress(), + assert.strictEqual( + await contract.runner.getAddress(), await signers[0].getAddress() ); }); @@ -750,24 +673,24 @@ describe("Ethers plugin", function () { it("Should be able to send txs and make calls", async function () { const greeter = await this.env.ethers.getContractAt( "Greeter", - deployedGreeter.address + deployedGreeter.target ); - assert.equal(await greeter.functions.greet(), "Hi"); - await greeter.functions.setGreeting("Hola"); - assert.equal(await greeter.functions.greet(), "Hola"); + assert.strictEqual(await greeter.greet(), "Hi"); + await greeter.setGreeting("Hola"); + assert.strictEqual(await greeter.greet(), "Hola"); }); describe("with custom signer", function () { it("Should return an instance of a contract associated to a custom signer", async function () { const contract = await this.env.ethers.getContractAt( "Greeter", - deployedGreeter.address, + deployedGreeter.target, signers[1] ); - - assert.equal( - await contract.signer.getAddress(), + assertIsSigner(contract.runner); + assert.strictEqual( + await contract.runner.getAddress(), await signers[1].getAddress() ); }); @@ -778,16 +701,15 @@ describe("Ethers plugin", function () { it("Should return an instance of a contract", async function () { const contract = await this.env.ethers.getContractAt( greeterArtifact.abi, - deployedGreeter.address + deployedGreeter.target ); - assert.containsAllKeys(contract.functions, [ - "setGreeting(string)", - "greet()", - ]); + assert.isNotNull(contract.interface.getFunction("greet")); + assert.isNotNull(contract.interface.getFunction("setGreeting")); + assertIsSigner(contract.runner); - assert.equal( - await contract.signer.getAddress(), + assert.strictEqual( + await contract.runner.getAddress(), await signers[0].getAddress() ); }); @@ -795,13 +717,14 @@ describe("Ethers plugin", function () { it("Should return an instance of an interface", async function () { const contract = await this.env.ethers.getContractAt( iGreeterArtifact.abi, - deployedGreeter.address + deployedGreeter.target ); - assert.containsAllKeys(contract.functions, ["greet()"]); + assert.isNotNull(contract.interface.getFunction("greet")); + assertIsSigner(contract.runner); - assert.equal( - await contract.signer.getAddress(), + assert.strictEqual( + await contract.runner.getAddress(), await signers[0].getAddress() ); }); @@ -809,52 +732,52 @@ describe("Ethers plugin", function () { it("Should be able to send txs and make calls", async function () { const greeter = await this.env.ethers.getContractAt( greeterArtifact.abi, - deployedGreeter.address + deployedGreeter.target ); - assert.equal(await greeter.functions.greet(), "Hi"); - await greeter.functions.setGreeting("Hola"); - assert.equal(await greeter.functions.greet(), "Hola"); + assert.strictEqual(await greeter.greet(), "Hi"); + await greeter.setGreeting("Hola"); + assert.strictEqual(await greeter.greet(), "Hola"); }); - it("Should be able to detect events", async function () { - const greeter = await this.env.ethers.getContractAt( - greeterArtifact.abi, - deployedGreeter.address - ); - - // at the time of this writing, ethers' default polling interval is - // 4000 ms. here we turn it down in order to speed up this test. - // see also - // https://github.com/ethers-io/ethers.js/issues/615#issuecomment-848991047 - const provider = greeter.provider as EthersProviderWrapper; - provider.pollingInterval = 100; - - let eventEmitted = false; - greeter.on("GreetingUpdated", () => { - eventEmitted = true; - }); - - await greeter.functions.setGreeting("Hola"); - - // wait for 1.5 polling intervals for the event to fire - await new Promise((resolve) => - setTimeout(resolve, provider.pollingInterval * 2) - ); - - assert.equal(eventEmitted, true); - }); + // TODO re-enable when we make .on("event") work + // it("Should be able to detect events", async function () { + // const greeter = await this.env.ethers.getContractAt( + // greeterArtifact.abi, + // deployedGreeter.target + // ); + // + // // at the time of this writing, ethers' default polling interval is + // // 4000 ms. here we turn it down in order to speed up this test. + // // see also + // // https://github.com/ethers-io/ethers.js/issues/615#issuecomment-848991047 + // // const provider = greeter.provider as any; + // // provider.pollingInterval = 100; + // + // let eventEmitted = false; + // await greeter.on("GreetingUpdated", () => { + // eventEmitted = true; + // }); + // + // await greeter.setGreeting("Hola"); + // + // // wait for 1.5 polling intervals for the event to fire + // await new Promise((resolve) => setTimeout(resolve, 10_000)); + // + // assert.strictEqual(eventEmitted, true); + // }); describe("with custom signer", function () { it("Should return an instance of a contract associated to a custom signer", async function () { const contract = await this.env.ethers.getContractAt( greeterArtifact.abi, - deployedGreeter.address, + deployedGreeter.target, signers[1] ); + assertIsSigner(contract.runner); - assert.equal( - await contract.signer.getAddress(), + assert.strictEqual( + await contract.runner.getAddress(), await signers[1].getAddress() ); }); @@ -866,31 +789,36 @@ describe("Ethers plugin", function () { ); const library = await libraryFactory.deploy(); - const contractFactory = await this.env.ethers.getContractFactory( - "TestContractLib", - { libraries: { TestLibrary: library.address } } - ); + const contractFactory = await this.env.ethers.getContractFactory< + [], + TestContractLib + >("TestContractLib", { + libraries: { TestLibrary: library.target }, + }); const numberPrinter = await contractFactory.deploy(); const numberPrinterAtAddress = await this.env.ethers.getContractAt( "TestContractLib", - numberPrinter.address + numberPrinter.target ); - const someNumber = 50; - assert.equal( - await numberPrinterAtAddress.callStatic.printNumber(someNumber), - someNumber * 2 + const someNumber = 50n; + assert.strictEqual( + await numberPrinterAtAddress.printNumber.staticCall(someNumber), + someNumber * 2n ); }); }); }); describe("getContractAtFromArtifact", function () { - let deployedGreeter: ethers.Contract; + let deployedGreeter: GreeterContract; beforeEach(async function () { - const Greeter = await this.env.ethers.getContractFactory("Greeter"); + const Greeter = await this.env.ethers.getContractFactory< + [], + GreeterContract + >("Greeter"); deployedGreeter = await Greeter.deploy(); }); @@ -898,16 +826,15 @@ describe("Ethers plugin", function () { it("Should return an instance of a contract", async function () { const contract = await this.env.ethers.getContractAtFromArtifact( greeterArtifact, - deployedGreeter.address + await deployedGreeter.getAddress() ); - assert.containsAllKeys(contract.functions, [ - "setGreeting(string)", - "greet()", - ]); + assert.isNotNull(contract.interface.getFunction("greet")); + assert.isNotNull(contract.interface.getFunction("setGreeting")); + assertIsSigner(contract.runner); - assert.equal( - await contract.signer.getAddress(), + assert.strictEqual( + await contract.runner.getAddress(), await signers[0].getAddress() ); }); @@ -915,24 +842,25 @@ describe("Ethers plugin", function () { it("Should be able to send txs and make calls", async function () { const greeter = await this.env.ethers.getContractAtFromArtifact( greeterArtifact, - deployedGreeter.address + await deployedGreeter.getAddress() ); - assert.equal(await greeter.functions.greet(), "Hi"); - await greeter.functions.setGreeting("Hola"); - assert.equal(await greeter.functions.greet(), "Hola"); + assert.strictEqual(await greeter.greet(), "Hi"); + await greeter.setGreeting("Hola"); + assert.strictEqual(await greeter.greet(), "Hola"); }); describe("with custom signer", function () { it("Should return an instance of a contract associated to a custom signer", async function () { const contract = await this.env.ethers.getContractAtFromArtifact( greeterArtifact, - deployedGreeter.address, + await deployedGreeter.getAddress(), signers[1] ); + assertIsSigner(contract.runner); - assert.equal( - await contract.signer.getAddress(), + assert.strictEqual( + await contract.runner.getAddress(), await signers[1].getAddress() ); }); @@ -997,16 +925,15 @@ describe("Ethers plugin", function () { }); async function assertContract( - contract: ethers.Contract, - signer: Signer + contract: EthersT.Contract, + signer: CustomEthersSigner ) { - assert.containsAllKeys(contract.interface.functions, [ - "setGreeting(string)", - "greet()", - ]); + assert.isNotNull(contract.interface.getFunction("greet")); + assert.isNotNull(contract.interface.getFunction("setGreeting")); + assertIsSigner(contract.runner); - assert.equal( - await contract.signer.getAddress(), + assert.strictEqual( + await contract.runner.getAddress(), await signer.getAddress() ); } @@ -1018,46 +945,45 @@ describe("Ethers plugin", function () { useEnvironment("hardhat-project", "hardhat"); describe("contract events", function () { - it("should be detected", async function () { - const Greeter = await this.env.ethers.getContractFactory("Greeter"); - const deployedGreeter: ethers.Contract = await Greeter.deploy(); - - // at the time of this writing, ethers' default polling interval is - // 4000 ms. here we turn it down in order to speed up this test. - // see also - // https://github.com/ethers-io/ethers.js/issues/615#issuecomment-848991047 - const provider = deployedGreeter.provider as EthersProviderWrapper; - provider.pollingInterval = 200; - - let eventEmitted = false; - deployedGreeter.on("GreetingUpdated", () => { - eventEmitted = true; - }); - - await deployedGreeter.functions.setGreeting("Hola"); - - // wait for 1.5 polling intervals for the event to fire - await new Promise((resolve) => - setTimeout(resolve, provider.pollingInterval * 2) - ); - - assert.equal(eventEmitted, true); - }); + // TODO re-enable when we make .on("event") work + // it("should be detected", async function () { + // const Greeter = await this.env.ethers.getContractFactory("Greeter"); + // const deployedGreeter: any = await Greeter.deploy(); + // + // // at the time of this writing, ethers' default polling interval is + // // 4000 ms. here we turn it down in order to speed up this test. + // // see also + // // https://github.com/ethers-io/ethers.js/issues/615#issuecomment-848991047 + // // const provider = deployedGreeter.provider as EthersProviderWrapper; + // // provider.pollingInterval = 200; + // + // let eventEmitted = false; + // deployedGreeter.on("GreetingUpdated", () => { + // eventEmitted = true; + // }); + // + // await deployedGreeter.setGreeting("Hola"); + // + // // wait for 1.5 polling intervals for the event to fire + // await new Promise((resolve) => setTimeout(resolve, 200 * 2)); + // + // assert.strictEqual(eventEmitted, true); + // }); }); describe("hardhat_reset", function () { it("should return the correct block number after a hardhat_reset", async function () { let blockNumber = await this.env.ethers.provider.getBlockNumber(); - assert.equal(blockNumber.toString(), "0"); + assert.strictEqual(blockNumber.toString(), "0"); await this.env.ethers.provider.send("evm_mine", []); await this.env.ethers.provider.send("evm_mine", []); blockNumber = await this.env.ethers.provider.getBlockNumber(); - assert.equal(blockNumber.toString(), "2"); + assert.strictEqual(blockNumber.toString(), "2"); await this.env.ethers.provider.send("hardhat_reset", []); blockNumber = await this.env.ethers.provider.getBlockNumber(); - assert.equal(blockNumber.toString(), "0"); + assert.strictEqual(blockNumber.toString(), "0"); }); it("should return the correct block after a hardhat_reset", async function () { @@ -1083,21 +1009,21 @@ describe("Ethers plugin", function () { sig.address ); - assert.equal(nonce, 0); + assert.strictEqual(nonce, 0); const response = await sig.sendTransaction({ from: sig.address, - to: this.env.ethers.constants.AddressZero, + to: this.env.ethers.ZeroAddress, value: "0x1", }); await response.wait(); nonce = await this.env.ethers.provider.getTransactionCount(sig.address); - assert.equal(nonce, 1); + assert.strictEqual(nonce, 1); await this.env.ethers.provider.send("hardhat_reset", []); nonce = await this.env.ethers.provider.getTransactionCount(sig.address); - assert.equal(nonce, 0); + assert.strictEqual(nonce, 0); }); it("should return the correct balance after a hardhat_reset", async function () { @@ -1105,32 +1031,38 @@ describe("Ethers plugin", function () { let balance = await this.env.ethers.provider.getBalance(sig.address); - assert.equal(balance.toString(), "10000000000000000000000"); + assert.strictEqual(balance.toString(), "10000000000000000000000"); const response = await sig.sendTransaction({ from: sig.address, - to: this.env.ethers.constants.AddressZero, + to: this.env.ethers.ZeroAddress, gasPrice: 8e9, }); await response.wait(); balance = await this.env.ethers.provider.getBalance(sig.address); - assert.equal(balance.toString(), "9999999832000000000000"); + assert.strictEqual(balance.toString(), "9999999832000000000000"); await this.env.ethers.provider.send("hardhat_reset", []); balance = await this.env.ethers.provider.getBalance(sig.address); - assert.equal(balance.toString(), "10000000000000000000000"); + assert.strictEqual(balance.toString(), "10000000000000000000000"); }); it("should return the correct code after a hardhat_reset", async function () { const [sig] = await this.env.ethers.getSigners(); const Greeter = await this.env.ethers.getContractFactory("Greeter"); - const tx = Greeter.getDeployTransaction(); + const tx = await Greeter.getDeployTransaction(); const response = await sig.sendTransaction(tx); const receipt = await response.wait(); + if (receipt === null) { + assert.fail("receipt shoudn't be null"); + } + if (receipt.contractAddress === null) { + assert.fail("receipt.contractAddress shoudn't be null"); + } let code = await this.env.ethers.provider.getCode( receipt.contractAddress @@ -1151,16 +1083,16 @@ describe("Ethers plugin", function () { [] ); let blockNumber = await this.env.ethers.provider.getBlockNumber(); - assert.equal(blockNumber.toString(), "0"); + assert.strictEqual(blockNumber.toString(), "0"); await this.env.ethers.provider.send("evm_mine", []); await this.env.ethers.provider.send("evm_mine", []); blockNumber = await this.env.ethers.provider.getBlockNumber(); - assert.equal(blockNumber.toString(), "2"); + assert.strictEqual(blockNumber.toString(), "2"); await this.env.ethers.provider.send("evm_revert", [snapshotId]); blockNumber = await this.env.ethers.provider.getBlockNumber(); - assert.equal(blockNumber.toString(), "0"); + assert.strictEqual(blockNumber.toString(), "0"); }); it("should return the correct block after a evm_revert", async function () { @@ -1194,21 +1126,21 @@ describe("Ethers plugin", function () { sig.address ); - assert.equal(nonce, 0); + assert.strictEqual(nonce, 0); const response = await sig.sendTransaction({ from: sig.address, - to: this.env.ethers.constants.AddressZero, + to: this.env.ethers.ZeroAddress, value: "0x1", }); await response.wait(); nonce = await this.env.ethers.provider.getTransactionCount(sig.address); - assert.equal(nonce, 1); + assert.strictEqual(nonce, 1); await this.env.ethers.provider.send("evm_revert", [snapshotId]); nonce = await this.env.ethers.provider.getTransactionCount(sig.address); - assert.equal(nonce, 0); + assert.strictEqual(nonce, 0); }); it("should return the correct balance after a evm_revert", async function () { @@ -1220,21 +1152,21 @@ describe("Ethers plugin", function () { let balance = await this.env.ethers.provider.getBalance(sig.address); - assert.equal(balance.toString(), "10000000000000000000000"); + assert.strictEqual(balance.toString(), "10000000000000000000000"); const response = await sig.sendTransaction({ from: sig.address, - to: this.env.ethers.constants.AddressZero, + to: this.env.ethers.ZeroAddress, gasPrice: 8e9, }); await response.wait(); balance = await this.env.ethers.provider.getBalance(sig.address); - assert.equal(balance.toString(), "9999999832000000000000"); + assert.strictEqual(balance.toString(), "9999999832000000000000"); await this.env.ethers.provider.send("evm_revert", [snapshotId]); balance = await this.env.ethers.provider.getBalance(sig.address); - assert.equal(balance.toString(), "10000000000000000000000"); + assert.strictEqual(balance.toString(), "10000000000000000000000"); }); it("should return the correct code after a evm_revert", async function () { @@ -1245,12 +1177,19 @@ describe("Ethers plugin", function () { const [sig] = await this.env.ethers.getSigners(); const Greeter = await this.env.ethers.getContractFactory("Greeter"); - const tx = Greeter.getDeployTransaction(); + const tx = await Greeter.getDeployTransaction(); const response = await sig.sendTransaction(tx); const receipt = await response.wait(); + if (receipt === null) { + assert.fail("receipt shoudn't be null"); + } + if (receipt.contractAddress === null) { + assert.fail("receipt.contractAddress shoudn't be null"); + } + let code = await this.env.ethers.provider.getCode( receipt.contractAddress ); @@ -1263,7 +1202,7 @@ describe("Ethers plugin", function () { }); }); - it("_signTypedData integration test", async function () { + it("signTypedData integration test", async function () { // See https://eips.ethereum.org/EIPS/eip-712#parameters // There's a json schema and an explanation for each field. const typedMessage = { @@ -1304,7 +1243,7 @@ describe("Ethers plugin", function () { }; const [signer] = await this.env.ethers.getSigners(); - const signature = await signer._signTypedData( + const signature = await signer.signTypedData( typedMessage.domain, typedMessage.types, typedMessage.message @@ -1316,28 +1255,30 @@ describe("Ethers plugin", function () { assert.lengthOf(signature, signatureSizeInBytes * byteToHex + hexPrefix); }); }); + describe("ganache via WebSocket", function () { useEnvironment("hardhat-project"); - it("should be able to detect events", async function () { - await this.env.run("compile", { quiet: true }); - - const Greeter = await this.env.ethers.getContractFactory("Greeter"); - const deployedGreeter: ethers.Contract = await Greeter.deploy(); - - const readonlyContract = deployedGreeter.connect( - new ethers.providers.WebSocketProvider("ws://127.0.0.1:8545") - ); - let emitted = false; - readonlyContract.on("GreetingUpdated", () => { - emitted = true; - }); - - await deployedGreeter.functions.setGreeting("Hola"); - - // wait for the event to fire - await new Promise((resolve) => setTimeout(resolve, 100)); - - assert.equal(emitted, true); - }); + // TODO re-enable when we make .on("event") work + // it("should be able to detect events", async function () { + // await this.env.run("compile", { quiet: true }); + // + // const Greeter = await this.env.ethers.getContractFactory("Greeter"); + // const deployedGreeter: any = await Greeter.deploy(); + // + // const readonlyContract = deployedGreeter.connect( + // new ethers.WebSocketProvider("ws://127.0.0.1:8545") + // ); + // let emitted = false; + // await readonlyContract.on("GreetingUpdated", () => { + // emitted = true; + // }); + // + // await deployedGreeter.setGreeting("Hola"); + // + // // wait for the event to fire + // await new Promise((resolve) => setTimeout(resolve, 100)); + // + // assert.strictEqual(emitted, true); + // }); }); }); diff --git a/packages/hardhat-ethers/test/no-accounts.ts b/packages/hardhat-ethers/test/no-accounts.ts index b068088a98..4f70fe468a 100644 --- a/packages/hardhat-ethers/test/no-accounts.ts +++ b/packages/hardhat-ethers/test/no-accounts.ts @@ -2,7 +2,7 @@ import { assert } from "chai"; import { TASK_COMPILE } from "hardhat/builtin-tasks/task-names"; import { HardhatRuntimeEnvironment } from "hardhat/types"; -import { SignerWithAddress } from "../src/signers"; +import { CustomEthersSigner } from "../src/signers"; import { useEnvironment } from "./helpers"; @@ -33,23 +33,36 @@ describe("hardhat-ethers plugin", function () { it("Should return an instance of a contract with a read-only provider", async function () { const receipt = await deployGreeter(this.env, signerAddress); + if (receipt === null) { + assert.fail("receipt shoudn't be null"); + } + if (receipt.contractAddress === null) { + assert.fail("receipt.contractAddress shoudn't be null"); + } + const contract = await this.env.ethers.getContractAt( "Greeter", receipt.contractAddress ); - assert.isDefined(contract.provider); - assert.isNotNull(contract.provider); + assert.isDefined(contract.runner); + assert.isNotNull(contract.runner); - const greeting = await contract.functions.greet(); + const greeting = await contract.greet(); - assert.equal(greeting, "Hi"); + assert.strictEqual(greeting, "Hi"); }); }); describe("with the abi and address", function () { it("Should return an instance of a contract with a read-only provider", async function () { const receipt = await deployGreeter(this.env, signerAddress); + if (receipt === null) { + assert.fail("receipt shoudn't be null"); + } + if (receipt.contractAddress === null) { + assert.fail("receipt.contractAddress shoudn't be null"); + } const signers = await this.env.ethers.getSigners(); assert.isEmpty(signers); @@ -63,12 +76,12 @@ describe("hardhat-ethers plugin", function () { receipt.contractAddress ); - assert.isDefined(contract.provider); - assert.isNotNull(contract.provider); + assert.isDefined(contract.runner); + assert.isNotNull(contract.runner); - const greeting = await contract.functions.greet(); + const greeting = await contract.greet(); - assert.equal(greeting, "Hi"); + assert.strictEqual(greeting, "Hi"); }); }); }); @@ -81,8 +94,8 @@ describe("hardhat-ethers plugin", function () { assert.isTrue(signers.every((aSigner) => aSigner.address !== address)); const signer = await this.env.ethers.getSigner(address); - assert.instanceOf(signer, SignerWithAddress); - assert.equal(signer.address, address); + assert.instanceOf(signer, CustomEthersSigner); + assert.strictEqual(signer.address, address); }); }); }); @@ -93,7 +106,7 @@ async function deployGreeter( signerAddress: string ) { const Greeter = await hre.ethers.getContractFactory("Greeter"); - const tx = Greeter.getDeployTransaction(); + const tx = await Greeter.getDeployTransaction(); tx.from = signerAddress; await hre.network.provider.request({ @@ -111,7 +124,10 @@ async function deployGreeter( }); assert.isDefined(hre.ethers.provider); const receipt = await hre.ethers.provider.getTransactionReceipt(txHash); - assert.equal(receipt.status, 1, "The deployment transaction failed."); + if (receipt === null) { + assert.fail("receipt shoudn't be null"); + } + assert.strictEqual(receipt.status, 1, "The deployment transaction failed."); return receipt; } diff --git a/packages/hardhat-ethers/test/updatable-target-proxy.ts b/packages/hardhat-ethers/test/updatable-target-proxy.ts deleted file mode 100644 index fac06e0e60..0000000000 --- a/packages/hardhat-ethers/test/updatable-target-proxy.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { assert } from "chai"; - -import { createUpdatableTargetProxy } from "../src/internal/updatable-target-proxy"; - -describe("updatable target proxy", function () { - it("should proxy properties", function () { - const o: any = { - a: 1, - getA() { - return this.a; - }, - b: {}, - getB() { - return this.b; - }, - }; - - const { proxy } = createUpdatableTargetProxy(o); - - assert.equal(proxy.a, 1); - assert.equal(proxy.getA(), 1); - assert.equal(proxy.b, o.b); - assert.equal(proxy.getB(), o.b); - }); - - it("should let set a new target", function () { - const o1: any = { - a: 1, - getA() { - return this.a; - }, - b: {}, - getB() { - return this.b; - }, - }; - - const o2: any = { - a: 2, - getA() { - return this.a; - }, - b: {}, - getB() { - return this.b; - }, - }; - - const { proxy, setTarget } = createUpdatableTargetProxy(o1); - - assert.equal(proxy.a, 1); - - setTarget(o2); - - assert.equal(proxy.a, 2); - assert.equal(proxy.getA(), 2); - assert.equal(proxy.b, o2.b); - assert.equal(proxy.getB(), o2.b); - }); - - it("shouldn't let you modify the proxied object", function () { - const o: any = { - a: 1, - }; - - const { proxy } = createUpdatableTargetProxy(o); - - assert.throws(() => { - proxy.a = 2; - }); - assert.throws(() => { - delete proxy.a; - }); - assert.throws(() => { - Object.defineProperty(proxy, "b", {}); - }); - assert.throws(() => { - Object.setPrototypeOf(proxy, {}); - }); - }); - - it("should let you call methods that modify the object", function () { - const o = { - a: 1, - inc() { - this.a++; - }, - }; - - const { proxy } = createUpdatableTargetProxy(o); - - assert.equal(proxy.a, 1); - proxy.inc(); - assert.equal(proxy.a, 2); - }); - - it("should trap getOwnPropertyDescriptor correctly", () => { - const o = { a: 1 }; - const { proxy, setTarget } = createUpdatableTargetProxy(o); - - assert.deepEqual(Object.getOwnPropertyDescriptor(proxy, "a"), { - value: 1, - writable: true, - enumerable: true, - configurable: true, - }); - - const o2 = { a: 2, b: 3 }; - setTarget(o2); - - assert.deepEqual(Object.getOwnPropertyDescriptor(proxy, "a"), { - value: 2, - writable: true, - enumerable: true, - configurable: true, - }); - assert.deepEqual(Object.getOwnPropertyDescriptor(proxy, "b"), { - value: 3, - writable: true, - enumerable: true, - configurable: true, - }); - }); - - it("should trap getPrototypeOf correctly", () => { - const proto = {}; - const o = Object.create(proto); - - const { proxy, setTarget } = createUpdatableTargetProxy(o); - - assert.equal(Object.getPrototypeOf(proxy), proto); - - const proto2 = {}; - const o2 = Object.create(proto2); - - setTarget(o2); - assert.equal(Object.getPrototypeOf(proxy), proto2); - }); - - it("should trap has correctly", () => { - const proto = { a: 1 }; - const o = Object.create(proto); - o.b = 2; - - const { proxy, setTarget } = createUpdatableTargetProxy(o); - - assert.isTrue("a" in proxy); - assert.isTrue("b" in proxy); - assert.isFalse("c" in proxy); - - const proto2 = { a: 2 }; - const o2 = Object.create(proto2); - o2.b = 4; - o2.c = 6; - - setTarget(o2); - assert.isTrue("a" in proxy); - assert.isTrue("b" in proxy); - assert.isTrue("c" in proxy); - assert.isFalse("d" in proxy); - }); - - it("should return isExtensible correctly", () => { - const o: any = {}; - Object.preventExtensions(o); - - const { proxy, setTarget } = createUpdatableTargetProxy(o); - - assert.isFalse(Object.isExtensible(proxy)); - - // if the proxy is initially not extensible, then it can't be made - // extensible afterwards - setTarget({}); - assert.isFalse(Object.isExtensible(proxy)); - }); - - it("should trap ownKeys correctly", () => { - const proto = { a: 1 }; - const o: any = Object.create(proto); - o.b = 1; - - const { proxy, setTarget } = createUpdatableTargetProxy(o); - assert.deepEqual(Object.getOwnPropertyNames(proxy), ["b"]); - - const proto2 = { c: 1 }; - const o2: any = Object.create(proto2); - o2.d = 1; - setTarget(o2); - assert.deepEqual(Object.getOwnPropertyNames(proxy), ["d"]); - }); - - it("should trap preventExtensions correctly", () => { - const o: any = {}; - - const { proxy } = createUpdatableTargetProxy(o); - assert.isTrue(Object.isExtensible(proxy)); - - Object.preventExtensions(proxy); - assert.isFalse(Object.isExtensible(proxy)); - }); -}); diff --git a/yarn.lock b/yarn.lock index 8f15159e61..636451fe90 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,11 @@ # yarn lockfile v1 +"@adraffy/ens-normalize@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.9.0.tgz#223572538f6bea336750039bb43a4016dcc8182d" + integrity sha512-iowxq3U30sghZotgl4s/oJRci6WPBfNO5YYgk2cIOMCHr3LeGPcsZjCEr+33Q4N+oV3OABDAtA+pyvWjbvBifQ== + "@ampproject/remapping@^2.2.0": version "2.2.1" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" @@ -1010,6 +1015,11 @@ lodash "^4.17.16" uuid "^7.0.3" +"@noble/hashes@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.1.2.tgz#e9e035b9b166ca0af657a7848eb2718f0f22f183" + integrity sha512-KYRCASVTv6aeUi1tsF8/vpyR7zpfs3FUzy2Jqm+MU+LmUKhQ0y2FpfwqkCcxSg2ua4GALJd8k2R76WxwZGbQpA== + "@noble/hashes@1.2.0", "@noble/hashes@~1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.2.0.tgz#a3150eeb09cc7ab207ebf6d7b9ad311a9bdbed12" @@ -1175,6 +1185,17 @@ mcl-wasm "^0.7.1" rustbn.js "~0.2.0" +"@nomicfoundation/hardhat-chai-matchers@^1.0.0": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-chai-matchers/-/hardhat-chai-matchers-1.0.6.tgz#72a2e312e1504ee5dd73fe302932736432ba96bc" + integrity sha512-f5ZMNmabZeZegEfuxn/0kW+mm7+yV7VNDxLpMOMGXWFJ2l/Ct3QShujzDRF9cOkK9Ui/hbDeOWGZqyQALDXVCQ== + dependencies: + "@ethersproject/abi" "^5.1.2" + "@types/chai-as-promised" "^7.1.3" + chai-as-promised "^7.1.1" + deep-eql "^4.0.1" + ordinal "^1.0.3" + "@nomicfoundation/solidity-analyzer-darwin-arm64@0.1.0": version "0.1.0" resolved "https://registry.yarnpkg.com/@nomicfoundation/solidity-analyzer-darwin-arm64/-/solidity-analyzer-darwin-arm64-0.1.0.tgz#83a7367342bd053a76d04bbcf4f373fef07cf760" @@ -1241,6 +1262,11 @@ "@nomicfoundation/solidity-analyzer-win32-ia32-msvc" "0.1.0" "@nomicfoundation/solidity-analyzer-win32-x64-msvc" "0.1.0" +"@nomiclabs/hardhat-ethers@^2.0.0": + version "2.2.2" + resolved "https://registry.yarnpkg.com/@nomiclabs/hardhat-ethers/-/hardhat-ethers-2.2.2.tgz#812d48929c3bf8fe840ec29eab4b613693467679" + integrity sha512-NLDlDFL2us07C0jB/9wzvR0kuLivChJWCXTKcj3yqjZqMoYp7g7wwS157F70VHx/+9gHIBGzak5pKDwG8gEefA== + "@nomiclabs/hardhat-etherscan@^3.0.0": version "3.1.7" resolved "https://registry.yarnpkg.com/@nomiclabs/hardhat-etherscan/-/hardhat-etherscan-3.1.7.tgz#72e3d5bd5d0ceb695e097a7f6f5ff6fcbf062b9a" @@ -2108,6 +2134,11 @@ aes-js@3.0.0: resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.0.0.tgz#e21df10ad6c2053295bcbb8dab40b09dbea87e4d" integrity sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw== +aes-js@4.0.0-beta.3: + version "4.0.0-beta.3" + resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-4.0.0-beta.3.tgz#da2253f0ff03a0b3a9e445c8cbdf78e7fda7d48c" + integrity sha512-/xJX0/VTPcbc5xQE2VUP91y1xN8q/rDfhEzLm+vLc3hYvb5+qHCnpJRuFcrKn63zumK/sCwYYzhG8HP78JYSTA== + agent-base@6: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" @@ -4227,6 +4258,18 @@ ethers@^5.0.0, ethers@^5.0.13, ethers@^5.4.7, ethers@^5.7.1: "@ethersproject/web" "5.7.1" "@ethersproject/wordlists" "5.7.0" +ethers@^6.1.0: + version "6.2.3" + resolved "https://registry.yarnpkg.com/ethers/-/ethers-6.2.3.tgz#9ddee438b5949e9724ba4c5d2c3b8deb5202ce96" + integrity sha512-l1Z/Yr+HrOk+7LTeYRHGMvYwVLGpTuVrT/kJ7Kagi3nekGISYILIby0f1ipV9BGzgERyy+w4emH+d3PhhcxIfA== + dependencies: + "@adraffy/ens-normalize" "1.9.0" + "@noble/hashes" "1.1.2" + "@noble/secp256k1" "1.7.1" + aes-js "4.0.0-beta.3" + tslib "2.4.0" + ws "8.5.0" + ethjs-abi@0.1.8: version "0.1.8" resolved "https://registry.yarnpkg.com/ethjs-abi/-/ethjs-abi-0.1.8.tgz#cd288583ed628cdfadaf8adefa3ba1dbcbca6c18" @@ -8779,6 +8822,11 @@ tsconfig-paths@^3.10.1: minimist "^1.2.6" strip-bom "^3.0.0" +tslib@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" + integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== + tslib@^1.8.1, tslib@^1.9.3: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" @@ -9562,6 +9610,11 @@ ws@7.4.6: resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== +ws@8.5.0: + version "8.5.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.5.0.tgz#bfb4be96600757fe5382de12c670dab984a1ed4f" + integrity sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg== + ws@^3.0.0: version "3.3.3" resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2"