Hardhat - Understanding the basics

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 :)