84
loading...
This website collects cookies to deliver better user experience
mkdir hardhat-tutorial
npm init -Y
, and then install hardhat:npm i -D hardhat
npx hardhat
and select "Create an empty hardhat.config.js":npm i -D @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers
npm i -D ts-node typescript @types/node @types/chai @types/mocha
hardhat.config.js
file to be hardhat.config.ts
:mv hardhat.config.js hardhat.config.ts
hardhat.config.ts
file, since with a Hardhat TypeScript project plugins need to be loaded with import
instead of require
, and functions must be explictly imported:// hardhat.config.ts
require("@nomiclabs/hardhat-waffle");
task("accounts", "Prints the list of accounts", async (taskArgs, hre) => {
const accounts = await hre.ethers.getSigners();
for (const account of accounts) {
console.log(await account.address);
}
});
/**
* @type import('hardhat/config').HardhatUserConfig
*/
module.exports = {
solidity: "0.7.3",
};
// hardhat.config.ts
import { task } from "hardhat/config"; // import function
import "@nomiclabs/hardhat-waffle"; // change require to import
task("accounts", "Prints the list of accounts", async (taskArgs, hre) => {
const accounts = await hre.ethers.getSigners();
for (const account of accounts) {
console.log(await account.address);
}
});
export default {
solidity: "0.7.3",
};
npx hardhat
again you should see some help instructions in your console:hardhat.config.ts
you'll see the sample "accounts" task definition. The task definition function takes 3 arguments - a name, a description, and a callback function that carries out the task. If you change the description of the "accounts" task, to "Hello, world!", and then run npx hardhat
in your console, you'll see that the "accounts" task now has the description "Hello, world!".// hardhat.config.ts
import { task } from "hardhat/config";
import "@nomiclabs/hardhat-waffle";
task("accounts", "Hello, world!", async (taskArgs, hre) => {
const accounts = await hre.ethers.getSigners();
for (const account of accounts) {
console.log(account.address);
}
});
/**
* @type import('hardhat/config').HardhatUserConfig
*/
export default {
solidity: "0.7.3",
};
contracts
in our root directory (Hardhat uses the "contracts" folder as the source folder by default - if you want to change that name, you'll need to configure it within the hardhat.config.ts
file):mdkir contracts
bored-ape.sol
in the contracts folder, then paste the contract code that we copied from Etherscan earlier. contracts
folder with the bored-ape.sol
contract inside it, we are ready to compile the contract. We can use a built-in compile
task to do this - all we need to do is run:npx hardhat compile
artifacts/contracts/<CONTRACT NAME>
folder. These two files (an "artifact" .json file, and a debug "dbg" .json file) will be generated for each contract - the Bored Ape contract code that we copied from Etherscan actually contains multiple "contracts". contracts/bored-ape.sol
file you can see that the "contract" keyword is used 15 times in total, and each instance has its own contract name - therefore, after compiling the bored-ape.sol
file we will end up with 30 files in the artifacts/contracts/bored-ape.sol/
folder. BoredApeYachtClub.json
artifact - this is the file that contains the "BoredApeYachtClub" ABI (the Application Binary Interface, a JSON representation of the contract's variables & functions), and is exactly what we need to pass into Ethers in order to create a contract instance.test
folder in the root directory, and call it bored-ape.test.ts
. Now we'll write a test, and I'll explain what we're doing in the code comments:// bored-ape.test.ts
// We are using TypeScript, so will use "import" syntax
import { ethers } from "hardhat"; // Import the Ethers library
import { expect } from "chai"; // Import the "expect" function from the Chai assertion library, we'll use this in our test
// "describe" is used to group tests & enhance readability
describe("Bored Ape", () => {
// "it" is a single test case - give it a descriptive name
it("Should initialize Bored Ape contract", async () => {
// We can refer to the contract by the contract name in
// `artifacts/contracts/bored-ape.sol/BoredApeYachtClub.json`
// initialize the contract factory: https://docs.ethers.io/v5/api/contract/contract-factory/
const BoredApeFactory = await ethers.getContractFactory("BoredApeYachtClub");
// create an instance of the contract, giving us access to all
// functions & variables
const boredApeContract = await BoredApeFactory.deploy(
"Bored Ape Yacht Club",
"BAYC",
10000,
1
);
// use the "expect" assertion, and read the MAX_APES variable
expect(await boredApeContract.MAX_APES()).to.equal(5000);
});
});
.deploy()
method, passing in variables that are required by the contract constructor. Here is the original contract constructor://bored-ape.sol
constructor(string memory name, string memory symbol, uint256 maxNftSupply, uint256 saleStart) ERC721(name, symbol)
npx hardhat test
AssertionError: Expected "10000" to be equal 5000
. This is nothing to worry about - I've deliberately added a test case that will fail on the first run - this is good practice, to help remove false positives. If we don't add a failing case to begin with, we can't be certain that we aren't accidentally writing a test that will always return true. A more thorough version of this method would actually begin with creating the test first and then gradually writing code to make it pass, but since it's not the focus of this tutorial we'll gloss over that. If you're interested in learning more about this style of writing tests and then implementing code to make it pass, here are a couple of good introductions: expect(await boredApeContract.MAX_APES()).to.equal(10000);
beforeEach
that will simplify the setup for each test, and allow us to reuse variables for each test. We'll move our contract deployment code into the beforeEach
function, and as you can see, we can use the boredApeContract
instance in our "initialize" test:// bored-ape.test.ts
import { expect } from "chai";
import { ethers } from "hardhat";
import { beforeEach } from "mocha";
import { Contract } from "ethers";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
describe("Bored Ape", () => {
let boredApeContract: Contract;
let owner: SignerWithAddress;
let address1: SignerWithAddress;
beforeEach(async () => {
const BoredApeFactory = await ethers.getContractFactory(
"BoredApeYachtClub"
);
[owner, address1] = await ethers.getSigners();
boredApeContract = await BoredApeFactory.deploy(
"Bored Ape Yacht Club",
"BAYC",
10000,
1
);
});
it("Should initialize the Bored Ape contract", async () => {
expect(await boredApeContract.MAX_APES()).to.equal(10000);
});
it("Should set the right owner", async () => {
expect(await boredApeContract.owner()).to.equal(await owner.address);
});
});
bored-ape.sol
file, notice that there is a function called mintApe
which takes in both a number of tokens (representing Bored Ape NFTs), and also expects to receive some ETH. Let's write a test for that function, which will let us try out payments, and force us to make use of some other methods in the contract to make the test pass. // bored-ape.test.ts
it("Should mint an ape", async () => {
expect(await boredApeContract.mintApe(1)).to.emit(
boredApeContract,
"Transfer"
);
});
mintApe
method doesn't return a value, we are going to listen for an event called "Transfer" - we can trace the mintApe
function's inheritance and see that ultimately it calls the _mint
function of an ERC-721 token and emits a { Transfer } event:mintApe
contains a number of conditions that we haven't fulfilled:flipSaleState
:// bored-ape.test.ts
await boredApeContract.flipSaleState();
npx hardhat test
and...we're still failing - but with a different error! A different error is actually great news, because it means we're making progress :) Looks like "Ether value sent is not correct" - which makes sense, since we didn't send any ETH along with our contract call. Notice that the mintApe
method signature contains the keyword "payable":// bored-ape.sol
function mintApe(uint numberOfTokens) public payable
apePrice
getter method:// bored-ape.sol
uint256 public constant apePrice = 80000000000000000; //0.08 ETH
apePrice
as our value, and send it through as ETH with our call to mintApe
. We'll also chain another method called withArgs
to our emit
call, which will give us the ability to listen to the arguments emitted by the "Transfer" event:// bored-ape.test.ts
import chai from "chai";
import { solidity } from "ethereum-waffle";
chai.use(solidity)
it("Should mint an ape", async () => {
await boredApeContract.flipSaleState();
const apePrice = await boredApeContract.apePrice();
const tokenId = await boredApeContract.totalSupply();
expect(
await boredApeContract.mintApe(1, {
value: apePrice,
})
)
.to.emit(boredApeContract, "Transfer")
.withArgs(ethers.constants.AddressZero, owner.address, tokenId);
});
mintApe
method as msg.value
, ensuring that we now satisfy the condition of the "Ether value sent is not correct" requirement:// bored-ape.sol
require(apePrice.mul(numberOfTokens) <= msg.value, "Ether value sent is not correct");
chai
into our test file so that we can use chai "matchers" - which we combine with the "solidity" matcher imported from "ethereum-waffle": https://ethereum-waffle.readthedocs.io/en/latest/matchers.html - now we are able to specify the exact arguments that we expect to receive from the "Transfer" event, and we can ensure that the test is actually passing as intended. _mint
method in bored-ape.sol
and see that Transfer
emits 3 arguments.// bored-ape.sol
emit Transfer(address(0), to, tokenId);
mintApe
transaction - in this case we're just using the owner's address. Lastly, the tokenId is defined within a for-loop in the mintApe
method, and is set to be equal to the return value of calling the tokenSupply
getter. withArgs
method, including a handy constant provided by the ethers library called AddressZero
:// bored-ape.test.ts
.withArgs(ethers.constants.AddressZero, owner.address, tokenId);
npx hardhat test
and we'll get a passing test. If you change any of the values in withArgs
you'll get a failing test - exactly what we expect! import { expect } from "chai";
import { ethers } from "hardhat";
import chai from "chai";
import { solidity } from "ethereum-waffle";
import { beforeEach } from "mocha";
import { Contract } from "ethers";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
chai.use(solidity);
describe("Bored Ape", () => {
let boredApeContract: Contract;
let owner: SignerWithAddress;
let address1: SignerWithAddress;
beforeEach(async () => {
const BoredApeFactory = await ethers.getContractFactory(
"BoredApeYachtClub"
);
[owner, address1] = await ethers.getSigners();
boredApeContract = await BoredApeFactory.deploy(
"Bored Ape Yacht Club",
"BAYC",
10000,
1
);
});
it("Should initialize the Bored Ape contract", async () => {
expect(await boredApeContract.MAX_APES()).to.equal(10000);
});
it("Should set the right owner", async () => {
expect(await boredApeContract.owner()).to.equal(await owner.address);
});
it("Should mint an ape", async () => {
await boredApeContract.flipSaleState();
const apePrice = await boredApeContract.apePrice();
const tokenId = await boredApeContract.totalSupply();
expect(
await boredApeContract.mintApe(1, {
value: apePrice,
})
)
.to.emit(boredApeContract, "Transfer")
.withArgs(ethers.constants.AddressZero, owner.address, tokenId);
});
});