Hardhat - Understanding the basics
What we'll be covering in this article?
Setting up Hardhat
Compilation
Testing
Debugging
Deployment
And much more ...
Hardhat is a developer environment, to compile,deploy, test and debug your etherium software in a easy way. Even though we had Truffle, why do we need Hardhat?
Getting started
npm init --yes //to create an entry point to your code
npm i hardhat --save-dev //install hardhat
npx hardhat
> Create an empty hardhat.config.js //just for now to understand the flow
the following is the structure you'll end up with
creating a contracts folder to add your contracts in, and a test folder for your tests. and finally, a scripts folder, to deploy your script. download the following libraries to use for test cases,
npm i @nomiclabs/hardhat-ethers ethers @nomiclabs/hardhat-waffle ethereum-waffle chai
import the "@nomiclabs/hardhat-waffle" in your hardhat.config.js
require("@nomiclabs/hardhat-waffle");
module.exports = {
solidity:"0.8.9"
}
Compilation in Hardhat
To compile a solidity file, we first move into a folder where the contract is saved. Next, we run the command npx hardhat compile
.npx hardhat clean
to clear the cache and delete the artifacts.
once we compile a file, the framework creates an artifact, the artifact holds all the metadata associated to the compiled contracts.
Testing ( testing using mocha )
const {expect} = require('chai');
describe("Token contract", function () {
//test case => 01
it("what you are testing",async function(){
const [owner] = await ethers.getSigners();
const InstanceContract = await ethers.getContractFactory("ContractName")
})
const HardhatDeploy = await InstanceContract.deploy();
//you can call the contract methods
const ownerBalance = await HardhatDeploy.balanceOf(owner.address);
//you can test by comparing expected value against what value you get when executing the contract methods
expect(
await HardhatDeploy.totalSupply()
).to.equal(ownerBalance)
});
ethers.getSigners():
In Hardhat, the ethers.getSigners()
function is used to retrieve the Ethereum addresses that are associated with the currently connected wallet or provider.
It returns an array of Signer
objects, which represent the Ethereum accounts that can be used to sign and send transactions on the network. These Signer
objects provide methods for interacting with the Ethereum blockchain, such as signing transactions, querying balances, and executing contract functions.
By calling ethers.getSigners()
, you can access the Ethereum addresses associated with the connected wallet or provider and perform various operations using those addresses.
await ethers.getContractFactory("ContractName")
This returns an instance of a contract.
await Token.deploy();
this deploys the contract on hardhat itself, not on any network.
Now that we've run the first test case, how do we execute it?
npx hardhat test
const {expect} = require('chai');
describe("Token contract", function () {
//test case => 01
it("what you are testing",async function(){
const [owner] = await ethers.getSigners();
const InstanceContract = await ethers.getContractFactory("ContractName")
})
const HardhatDeploy = await InstanceContract.deploy();
//you can call the contract methods
const ownerBalance = await HardhatDeploy.balanceOf(owner.address);
//you can test by comparing expected value against what value you get when executing the contract methods
expect(
await HardhatDeploy.totalSupply()
).to.equal(ownerBalance)
} //first test case
//test case => 01
it("second test function",async function(){
const [owner,addr1,addr2] = await ethers.getSigners();
const InstanceContract = await ethers.getContractFactory("ContractName")
})
const HardhatDeploy = await InstanceContract.deploy();
//transfer from A to B
await HardhatDeploy.transfer(addr1.address,10);
expect(await HardhatDeploy.balanceOf(addr1.address))
.to.equal(10);
//what if you want to connect this contract to another address
//I mean another user is calling this contract.
await HardhatDeploy.connect(addr1).transfer(addr2.address,5);
expect(await HardhatDeploy.balanceOf(addr2.address))
.to.equal(5);
} //second test case
);
what if you want to connect this contract to another address. I mean another user is calling this contract. we use the .connect(addr1 object)await InstanceContract.connect(addr1).transfer(addr2.address,5);
So that is how it is done, I dont know if you've noticed that there is a lot of repeatation of code, for example deploying this contract. To solve this problem, we'll use a hook provided to us, it not only reduces repeatative code, but also reduces execution time, as the code gets deployed once, and an instance of the contract is created once. although for every test, it acts as a fresh deploy.
const {expect} = require('chai');
describe("Contract test",function(){
let InstancContract;
let HardhatDeploy;
let owner;
let addr1;
let addr2;
//using the beforeEach hook
beforeEach(async function({
InstancContract = await ethers.getContractFactory("ContractName");
[owner,addr1,addr2] = await ethers.getSigners();
HardhatDeploy= await InstancContract.deploy();
})
describe("after deployment",function(){
it("should set the right owner",async function(){
expect(await HardhatDeploy.owner())
.to.equal(owner.address)
})
it("should assign the total supply to owner",async function(){
const ownerBalance = await HardhatDeploy.balanceOf(owner.address)
expect(await HardhatDeploy.totalSupply()).to.equal(ownerBalance)
})
})
describe("transactions",async function(){
it('should transfer tokens between accounts',async function(){
//from owner
await HardhatDeploy.transfer(addr1.address,10);
const addr1Bal = await HardhatDeploy.balanceOf(addr1.address);
expect(addr1Bal).to.equal(10);
//from addr1, remember connect to object, not address
await HardhatDeploy.connect(addr1).transfer(addr2.address,5);
const addr2Bal = await HardhatDeploy.balanceOf(addr2.address)
expect(addr2Bal).to.equal(5);
}
it('if require fails',async function(){
const initialFunds = await HardhatDeploy.balanceOf(owner.address);
await expect(await HardhatDeploy.connect(addr1).transfer(addr2.address,10)
.to
.be
.revertedWith("Not enough funds") //the revert message
})
})
})
beforeEach is executed before each test case, i.e describe("",fn())
await expect().to.be.revertedWith("require message if fails")
: here we check for the error so we don't have to worry about it breaking.expect can have the await keyword
Debugging
if you've played around with remix, you know how challenging it is to debug your code. hardhat takes care of that problem.
to debug during runtime, you can import a file import "hardhat/console.log
, this file allows you to run console.log() in the contract. I usually use them when my test cases keep failing.
you can read the console when the contract runs during testing. this makes testing so much more easier.
Deploying to a live network
Now let's get to the main crux, The deployment of your project on a live network.
First, create a folder called scripts.
Secondly, create a file called deploy.js
//you should not have the {ethers} = require('ethers') line
async function main(){
const [deployed] = await ethers.getSigners();
const ContractObject = await ethers.getContractFactory("ContractName");
const deployedObject = await ContractObject.deploy();
console.log("contract address",deployedObject.address);
}
main()
.then(()=>process.exit(0))
.catch((e)=>{
console.log(e);
process.exit(1);
})
to run the deploy script npx hardhat run scripts/deploy.js
, now you have deployed on the mainnet, what if you want to deploy on the testnet?npx hardhat run scripts/deploy.js --network ropsten
< ropsten is the name of the network, if you are running on another such as sepoli, use that name in its place.
require("@nomiclabs/hardhat-waffle");
// Go to https://infura.io, sign up, create a new API key
// in its dashboard, and replace "KEY" with it
const INFURA_API_KEY = "KEY";
// Replace this private key with your Sepolia account private key
// To export your private key from Coinbase Wallet, go to
// Settings > Developer Settings > Show private key
// To export your private key from Metamask, open Metamask and
// go to Account Details > Export Private Key
// Beware: NEVER put real Ether into testing accounts
const SEPOLIA_PRIVATE_KEY = "YOUR SEPOLIA PRIVATE KEY";
module.exports = {
solidity: "0.8.9",
networks: {
sepolia: {
url: `https://sepolia.infura.io/v3/${INFURA_API_KEY}`,
accounts: [`0x${SEPOLIA_PRIVATE_KEY}`],
}
}
};
Thats all folks :)
Subscribe to my newsletter
Read articles from Aaron Rebelo directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Aaron Rebelo
Aaron Rebelo
Hi, I'm Aaron Rebelo, a MERN stack developer who's passionate about creating robust and engaging web applications. My expertise lies in using MongoDB, Express, React, and Node.js to build applications that are not only functional but also visually appealing. In addition to my work as a developer, I'm currently learning Solidity, a programming language used for developing smart contracts on the Ethereum blockchain. I believe that blockchain technology is the future, and I'm excited to be a part of this emerging industry. I'm also interested in digital marketing and believe that effective marketing strategies are key to the success of any project. That's why I'm committed to learning the latest marketing techniques and strategies to help my clients achieve their goals. In my spare time, I've started a video series on my YouTube channel, @thedecadehypothesis, where I document my progress on building new habits every 10 days. I believe that building new habits is essential for personal growth, and I'm excited to share my journey with others. Thank you for taking the time to learn more about me. I'm passionate about my work and excited about the opportunities that lie ahead. Please feel free to connect with me if you have any questions or if you're interested in working together.