120 lines
4.8 KiB
Markdown
120 lines
4.8 KiB
Markdown
---
|
|
name: hardhat-toolbox-mocha-ethers
|
|
description: Use alongside the `hardhat` skill when the project depends on `@nomicfoundation/hardhat-toolbox-mocha-ethers`. Covers the ethers helpers exposed on `network.create()`, contract interaction (`ethers.deployContract`, calling functions, `connect`), TypeChain-typed contract instances, and the chai matchers from `@nomicfoundation/hardhat-ethers-chai-matchers` (`.to.emit`, `.to.be.revertedWith*`, `.to.changeEtherBalance(s)`).
|
|
metadata:
|
|
package: "@nomicfoundation/hardhat-toolbox-mocha-ethers"
|
|
---
|
|
|
|
# Hardhat toolbox: ethers + Mocha
|
|
|
|
This skill builds on the core **`hardhat`** skill. Load that first for test organization, the `network.create()` shape, `networkHelpers`, fixtures, and the typechecking workflow. Everything below hangs off the connection returned by `network.create()`.
|
|
|
|
This stack runs tests under **Mocha** (global `describe` / `it`) and asserts with **chai**. Mocha doesn't await `describe` callbacks, so the connection is set up at the top of the file with top-level `await`:
|
|
|
|
```ts
|
|
import { expect } from "chai";
|
|
import { network } from "hardhat";
|
|
|
|
const { ethers, networkHelpers } = await network.create();
|
|
|
|
describe("Counter", function () {
|
|
// ...
|
|
});
|
|
```
|
|
|
|
One connection per file is the norm. State isolation between tests comes from `loadFixture` (see the `hardhat` skill), not from creating a fresh connection per `describe`.
|
|
|
|
## `ethers`: signers and contract interaction
|
|
|
|
Contract types are generated by **TypeChain** from the compiled ABI. Run `hardhat build` before typechecking so the types reflect the latest contracts.
|
|
|
|
```ts
|
|
// Provider (read-only chain data)
|
|
const blockNumber = await ethers.provider.getBlockNumber();
|
|
const balance = await ethers.provider.getBalance("0xabc...");
|
|
|
|
// Signers (for sending transactions)
|
|
const [owner, alice, bob] = await ethers.getSigners(); // default funded accounts
|
|
|
|
// Utilities
|
|
const oneEth = ethers.parseEther("1.0");
|
|
|
|
// Deploy a contract, returns a fully typed instance.
|
|
// Pass constructor args as the second argument, and (optionally) a signer
|
|
// other than the default as the third.
|
|
const counter = await ethers.deployContract("Counter");
|
|
const counterWithArgs = await ethers.deployContract("Counter", [42n]);
|
|
const counterFromAlice = await ethers.deployContract("Counter", [], alice);
|
|
|
|
// Read state and call functions. Args and return types are type-checked
|
|
// against the ABI at compile time; passing wrong types or wrong argument
|
|
// counts is a TypeScript error.
|
|
const value = await counter.x();
|
|
await counter.inc();
|
|
await counter.incBy(3n);
|
|
|
|
// Send from a different signer
|
|
await counter.connect(alice).inc();
|
|
|
|
// Send ETH along with a payable call
|
|
await counter.deposit({ value: oneEth });
|
|
|
|
// Attach to an already-deployed contract
|
|
const existing = await ethers.getContractAt("Counter", "0xabc...");
|
|
```
|
|
|
|
Inside a `loadFixture` setup function (see the `hardhat` skill for the surrounding pattern), `ethers.deployContract` is the canonical deploy step:
|
|
|
|
```ts
|
|
async function deployCounterFixture() {
|
|
const counter = await ethers.deployContract("Counter");
|
|
return { counter };
|
|
}
|
|
|
|
const { counter } = await networkHelpers.loadFixture(deployCounterFixture);
|
|
```
|
|
|
|
To send a transaction from an arbitrary address (not one of the default-funded accounts), fund it for gas and pull an impersonating signer with `ethers.getImpersonatedSigner` — it handles the impersonation internally, no separate `networkHelpers.impersonateAccount` call needed:
|
|
|
|
```ts
|
|
const addr = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
|
await networkHelpers.setBalance(addr, ethers.parseEther("1.0"));
|
|
const signer = await ethers.getImpersonatedSigner(addr);
|
|
await counter.connect(signer).inc();
|
|
```
|
|
|
|
## Chai matchers: Ethereum-specific assertions
|
|
|
|
The `hardhat-ethers-chai-matchers` plugin extends chai's `expect` with matchers for transactions, events, and balance changes. Pass the **transaction promise** (the result of a write call, unawaited) into `expect`:
|
|
|
|
```ts
|
|
import { expect } from "chai";
|
|
|
|
// Reverts
|
|
await expect(counter.connect(banned).inc()).to.revert(ethers);
|
|
await expect(counter.connect(banned).inc()).to.be.revertedWith(
|
|
"only the owner can increment the counter",
|
|
);
|
|
await expect(counter.connect(banned).inc()).to.be.revertedWithCustomError(
|
|
counter,
|
|
"Unauthorized",
|
|
);
|
|
await expect(counter.connect(banned).inc())
|
|
.to.be.revertedWithCustomError(counter, "Unauthorized")
|
|
.withArgs(banned.address);
|
|
|
|
// Events
|
|
await expect(counter.inc()).to.emit(counter, "Increment");
|
|
await expect(counter.inc()).to.emit(counter, "Increment").withArgs(1n);
|
|
|
|
// ETH balance changes (positive = received, negative = spent, before gas)
|
|
await expect(game.claim()).to.changeEtherBalance(ethers, winner, PRIZE);
|
|
await expect(game.claim()).to.changeEtherBalances(
|
|
ethers,
|
|
[winner, loser],
|
|
[PRIZE, -STAKE],
|
|
);
|
|
```
|
|
|
|
For plain assertions (equality, arrays, types), use `expect` from chai directly without the Ethereum matchers.
|