Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/lemon-flies-reply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nomicfoundation/hardhat-ethers-chai-matchers": patch
---

Only use `AssertionError`s within assertion functions [#7993](https://github.com/NomicFoundation/hardhat/pull/7993)
5 changes: 5 additions & 0 deletions .changeset/wild-buckets-beam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nomicfoundation/hardhat-mocha": patch
---

Improve the error message shown when an `await` is missing [#7993](https://github.com/NomicFoundation/hardhat/pull/7993)
3 changes: 0 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion v-next/hardhat-ethers-chai-matchers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@
"typescript": "~5.8.0"
},
"dependencies": {
"@nomicfoundation/hardhat-errors": "workspace:^3.0.5",
Comment thread
kanej marked this conversation as resolved.
"@nomicfoundation/hardhat-utils": "workspace:^3.0.5",
"@types/chai-as-promised": "^8.0.1",
"chai-as-promised": "^8.0.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { assertHardhatInvariant } from "@nomicfoundation/hardhat-errors";
import { assert as chaiAssert } from "chai";
import { isAddress, isAddressable } from "ethers";

import { tryDereference } from "../utils/typed.js";
Expand Down Expand Up @@ -35,11 +35,15 @@ function tryGetAddressSync(value: any): string | undefined {
if ("address" in value) {
value = value.address;
} else {
assertHardhatInvariant(
chaiAssert.ok(
"target" in value,
"target property should exist in value",
"Addressable value should have the property target",
);
value = value.target;

/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions
-- This cast is here so the CI job with @types/chai@4.3.0 builds. Note
that we have the assertion that target exists above. */
value = (value as any).target;
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import util from "node:util";

import { HardhatError } from "@nomicfoundation/hardhat-errors";
import { toBigInt } from "@nomicfoundation/hardhat-utils/bigint";
import { AssertionError } from "chai";
import { assert as chaiAssert, AssertionError } from "chai";
import deepEqual from "deep-eql";

import { isBigInt } from "../utils/bigint.js";
Expand Down Expand Up @@ -120,11 +119,9 @@ function overwriteBigNumberFunction(
} else if (method === "lte") {
return lhs <= rhs;
} else {
throw new HardhatError(
HardhatError.ERRORS.CHAI_MATCHERS.GENERAL.UNKNOWN_COMPARISON_OPERATION,
{
method,
},
chaiAssert.fail(
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Unreachable
`Unknown comparison operation "${method as any}"`,
);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
import type { BalanceChangeOptions } from "../utils/balance.js";
import type { HardhatEthers } from "@nomicfoundation/hardhat-ethers/types";
import type { Addressable } from "ethers/address";
import type { TransactionResponse } from "ethers/providers";
import type { BigNumberish } from "ethers/utils";

import { assertHardhatInvariant } from "@nomicfoundation/hardhat-errors";
import { isObject } from "@nomicfoundation/hardhat-utils/lang";
import { assert as chaiAssert } from "chai";
import { toBigInt } from "ethers/utils";

import { CHANGE_ETHER_BALANCE_MATCHER } from "../constants.js";
import { getAddressOf } from "../utils/account.js";
import {
assertCanBeConvertedToBigint,
assertIsNotNull,
} from "../utils/asserts.js";
import { assertIsNotNull } from "../utils/asserts.js";
import { getBalances, type BalanceChangeOptions } from "../utils/balance.js";
import { buildAssert } from "../utils/build-assert.js";
import { preventAsyncMatcherChaining } from "../utils/prevent-chaining.js";

Expand Down Expand Up @@ -92,38 +88,34 @@ export async function getBalanceChange(
}

const txReceipt = await txResponse.wait();
assertIsNotNull(txReceipt, "txReceipt");
assertIsNotNull(
txReceipt,
"Transaction's receipt cannot be fetched from the network",
);
const txBlockNumber = txReceipt.blockNumber;

const block = await ethers.provider.getBlock(txReceipt.blockHash, false);

assertHardhatInvariant(block !== null, "The block doesn't exist");
assertIsNotNull(
block,
"The transaction's block cannot be fetched from the network",
);

assertHardhatInvariant(
isObject(block) &&
Array.isArray(block.transactions) &&
block.transactions.length === 1,
chaiAssert.equal(
block.transactions.length,
1,
"There should be only 1 transaction in the block",
);

const address = await getAddressOf(account);

const balanceAfterHex = await ethers.provider.getBalance(
address,
txBlockNumber,
);

const balanceBeforeHex = await ethers.provider.getBalance(
address,
const [balanceAfter] = await getBalances(ethers, [account], txBlockNumber);
const [balanceBefore] = await getBalances(
ethers,
[account],
txBlockNumber - 1,
);

assertCanBeConvertedToBigint(balanceAfterHex);
assertCanBeConvertedToBigint(balanceBeforeHex);

const balanceAfter = BigInt(balanceAfterHex);
const balanceBefore = BigInt(balanceBeforeHex);

if (options?.includeFee !== true && address === txResponse.from) {
const gasPrice = txReceipt.gasPrice;
const gasUsed = txReceipt.gasUsed;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { HardhatEthers } from "@nomicfoundation/hardhat-ethers/types";
import type { Addressable } from "ethers/address";
import type { TransactionResponse } from "ethers/providers";

import { HardhatError } from "@nomicfoundation/hardhat-errors";
import { assert as chaiAssert } from "chai";
import { toBigInt } from "ethers/utils";

import { CHANGE_ETHER_BALANCES_MATCHER } from "../constants.js";
Expand Down Expand Up @@ -120,12 +120,8 @@ function validateInput(
Array.isArray(balanceChanges) &&
accounts.length !== balanceChanges.length
) {
throw new HardhatError(
HardhatError.ERRORS.CHAI_MATCHERS.GENERAL.ACCOUNTS_NUMBER_DIFFERENT_FROM_BALANCE_CHANGES,
{
accounts: accounts.length,
balanceChanges: balanceChanges.length,
},
chaiAssert.fail(
`The number of accounts (${accounts.length}) is different than the number of expected balance changes (${balanceChanges.length})`,
);
}
} catch (e) {
Expand All @@ -145,7 +141,10 @@ export async function getBalanceChanges(
const txResponse = await transaction;

const txReceipt = await txResponse.wait();
assertIsNotNull(txReceipt, "txReceipt");
assertIsNotNull(
txReceipt,
"Transaction's receipt cannot be fetched from the network",
);
const txBlockNumber = txReceipt.blockNumber;

const balancesAfter = await getBalances(ethers, accounts, txBlockNumber);
Expand All @@ -170,7 +169,10 @@ async function getTxFees(
(await getAddressOf(account)) === txResponse.from
) {
const txReceipt = await txResponse.wait();
assertIsNotNull(txReceipt, "txReceipt");
assertIsNotNull(
txReceipt,
"Transaction's receipt cannot be fetched from the network",
);
const gasPrice = txReceipt.gasPrice ?? txResponse.gasPrice;
const gasUsed = txReceipt.gasUsed;
const txFee = gasPrice * gasUsed;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,8 @@ import type {
} from "ethers";
import type { TransactionResponse } from "ethers/providers";

import {
assertHardhatInvariant,
HardhatError,
} from "@nomicfoundation/hardhat-errors";
import { isObject } from "@nomicfoundation/hardhat-utils/lang";
import { assert as chaiAssert } from "chai";
import { toBigInt } from "ethers/utils";

import {
Expand Down Expand Up @@ -190,12 +187,8 @@ function validateInput(
Array.isArray(balanceChanges) &&
accounts.length !== balanceChanges.length
) {
throw new HardhatError(
HardhatError.ERRORS.CHAI_MATCHERS.GENERAL.ACCOUNTS_NUMBER_DIFFERENT_FROM_BALANCE_CHANGES,
{
accounts: accounts.length,
balanceChanges: balanceChanges.length,
},
chaiAssert.fail(
`The number of accounts (${accounts.length}) is different than the number of expected balance changes (${balanceChanges.length})`,
);
}
} catch (e) {
Expand All @@ -208,11 +201,8 @@ function validateInput(

function checkToken(token: unknown, method: string) {
if (!isObject(token) || token === null || !("interface" in token)) {
throw new HardhatError(
HardhatError.ERRORS.CHAI_MATCHERS.GENERAL.FIRST_ARGUMENT_MUST_BE_A_CONTRACT_INSTANCE,
{
method,
},
chaiAssert.fail(
`The first argument of "${method}" must be the contract instance of the token`,
);
} else if (
isObject(token) &&
Expand All @@ -222,9 +212,7 @@ function checkToken(token: unknown, method: string) {
typeof token.interface.getFunction === "function" &&
token.interface.getFunction("balanceOf") === null
) {
throw new HardhatError(
HardhatError.ERRORS.CHAI_MATCHERS.GENERAL.CONTRACT_IS_NOT_AN_ERC20_TOKEN,
);
chaiAssert.fail("The given contract instance is not an ERC20 token");
}
}

Expand All @@ -237,15 +225,22 @@ export async function getBalanceChange(
const txResponse = await transaction;

const txReceipt = await txResponse.wait();
assertIsNotNull(txReceipt, "txReceipt");
assertIsNotNull(
txReceipt,
"Transaction's receipt cannot be fetched from the network",
);
const txBlockNumber = txReceipt.blockNumber;

const block = await ethers.provider.getBlock(txReceipt.blockHash, false);

assertHardhatInvariant(block !== null, "The block doesn't exist");
assertIsNotNull(
block,
"The transaction's block cannot be fetched from the network",
);

assertHardhatInvariant(
Array.isArray(block.transactions) && block.transactions.length === 1,
chaiAssert.equal(
block.transactions.length,
1,
"There should be only 1 transaction in the block",
);

Expand Down
37 changes: 18 additions & 19 deletions v-next/hardhat-ethers-chai-matchers/src/internal/matchers/emit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ import type { Transaction } from "ethers/transaction";

import util from "node:util";

import { HardhatError } from "@nomicfoundation/hardhat-errors";
import { AssertionError } from "chai";
import { assert as chaiAssert, AssertionError } from "chai";

import { ASSERTION_ABORTED, EMIT_MATCHER } from "../constants.js";
import { assertArgsArraysEqual, assertIsNotNull } from "../utils/asserts.js";
Expand All @@ -30,10 +29,7 @@ async function waitForPendingTransaction(
}

if (hash === null) {
throw new HardhatError(
HardhatError.ERRORS.CHAI_MATCHERS.GENERAL.INVALID_TRANSACTION,
{ transaction: JSON.stringify(tx) },
);
chaiAssert.fail(`"${JSON.stringify(tx)}" is not a valid transaction`);
}

return provider.getTransactionReceipt(hash);
Expand Down Expand Up @@ -74,8 +70,12 @@ export function supportEmit(
} catch (e) {
if (e instanceof TypeError) {
const errorMessage = e.message.split(" (argument=")[0];
// eslint-disable-next-line no-restricted-syntax -- keep the original chai error structure
throw new AssertionError(errorMessage);
const error = new AssertionError(errorMessage);
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions
-- This cast is here because otherwise the CI job that ensures that
this package still works with chai v5 will fail */
(error as any).cause = e;
throw error;
}
}

Expand All @@ -89,14 +89,12 @@ export function supportEmit(
const topic = eventFragment.topicHash;
const contractAddress = contract.target;
if (typeof contractAddress !== "string") {
throw new HardhatError(
HardhatError.ERRORS.CHAI_MATCHERS.GENERAL.CONTRACT_TARGET_MUST_BE_A_STRING,
);
chaiAssert.fail("The contract target should be a string");
}

if (args.length > 0) {
throw new HardhatError(
HardhatError.ERRORS.CHAI_MATCHERS.GENERAL.EMIT_EXPECTS_TWO_ARGUMENTS,
chaiAssert.fail(
"The .emit matcher expects two arguments: the contract and the event name. Arguments should be asserted with the .withArgs helper.",
);
}

Expand Down Expand Up @@ -124,14 +122,15 @@ export function supportEmit(
}

if (contract.runner === null || contract.runner.provider === null) {
throw new HardhatError(
HardhatError.ERRORS.CHAI_MATCHERS.GENERAL.CONTRACT_RUNNER_PROVIDER_NOT_NULL,
);
chaiAssert.fail("contract.runner.provider shouldn't be null");
}

return waitForPendingTransaction(tx, contract.runner.provider).then(
(receipt) => {
assertIsNotNull(receipt, "receipt");
assertIsNotNull(
receipt,
"Transaction's receipt cannot be fetched from the network",
);
Comment on lines +130 to +133
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sense if we assume chai matchers can never be used against a non-automining chain, are we happy with that assumption?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't change that assumption here. It was always present. I guess we can create an issue to revisit it if needed. @ChristopherDedominici

Note that these matchers may assume a few extra things that are only present in dev networks.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created this PR (still in draft), to address the issue: #7997

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ChristopherDedominici how does this work in Hardhat 2, did chai matchers always assume you were running against an automine network?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, in HH2 it works like that. Much of this code was inherited from Waffle, which worked only with Ganache.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not block this PR on the other one though

return onSuccess(receipt);
},
);
Expand Down Expand Up @@ -183,7 +182,7 @@ const tryAssertArgsArraysEqual = (
const parsedLog = chaiUtils
.flag(context, "contract")
.interface.parseLog(logs[0]);
assertIsNotNull(parsedLog, "parsedLog");
assertIsNotNull(parsedLog, "Can't parse the first log");

return assertArgsArraysEqual(
Assertion,
Expand All @@ -204,7 +203,7 @@ const tryAssertArgsArraysEqual = (
const parsedLog = chaiUtils
.flag(context, "contract")
.interface.parseLog(logs[index]);
assertIsNotNull(parsedLog, "parsedLog");
assertIsNotNull(parsedLog, `Can't parse the log index ${index}`);

assertArgsArraysEqual(
Assertion,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { HardhatError } from "@nomicfoundation/hardhat-errors";
import { assert as chaiAssert } from "chai";

import { LEGACY_REVERTED_MATCHER } from "../../constants.js";

Expand All @@ -12,8 +12,8 @@ export function supportLegacyReverted(
this._obj.catch(() => {});
}

throw new HardhatError(
HardhatError.ERRORS.CHAI_MATCHERS.GENERAL.DEPRECATED_REVERTED_MATCHER,
chaiAssert.fail(
"The .reverted matcher has been deprecated. Use .revert(ethers) instead.",
);
});
Comment thread
kanej marked this conversation as resolved.
}
Loading
Loading