Skip to main content

10 posts tagged with "Hardhat"

View All Tags
Go back

· 22 min read
John Fawole

Introduction

The reality is that a single individual or small group of people may not have the financial strength to fund a particular goal or agenda. This is where collective power becomes more effective as they invite more people to contribute towards the goal.

This process is known as crowdfunding. Reports have it that while the crowdfunding industry is valued at around \$1 billion in 2022, it will hit almost \$5 billion in 2025.

The most exciting fact is that more crowdfunding campaigns are run on the blockchain with the advent of blockchain technology and its innovations.

Thus, as a competent contract developer, you should know how to build, test, and deploy secure crowdfunding contracts. By the time you finish this article, you would have built your crowdfunding contract and deployed it on Celo.

Let’s jump into it.

Prerequisites​

These are the prerequisites you will need for this tutorial:

  • A fair understanding of Solidity and Javascript
  • An adequate understanding of the Celo blockchain and Hardhat framework

Requirements​

A Brief Overview of Celo

It is a good practice in blockchain in blockchain engineering to have a fair understanding of an ecosystem as a developer before attempting to build smart contracts or DApps on it.

Thus, we shall examine Celo briefly:

First of all, Celo is a self-sufficient layer-1 blockchain protocol that uses a battle-tested proof-of-stake consensus mechanism and is compatible with the Ethereum Virtual Machine. That means developers can use the same languages and frameworks used to build on Ethereum to build on Celo. The main thing that will change is switching the configuration when it is time to deploy.

But while they are both interoperable, Celo is distinct in terms of its architecture, ecological friendliness, economic model, and transaction processing technicalities.

One of the most distinguishing qualities of Celo is that it is a DeFi-focused blockchain with a unique identity system that can point a regular phone number to a wallet address.

Thus, Celo is one of the leading blockchains with the best user experience and is on track to banking those who are unbanked.

Having laid this brief foundation, let us roll our sleeves and proceed to write our contract.

Writing a Crowdfunding Contract with Solidity

The general idea behind the contract we are about to write is that people should be able to donate ERC-20 tokens to the campaign. If the benefactors can donate up to the target, the beneficiary can withdraw the funds.

But if it were to be the case that the campaign was not eventually successful, the smart contract would automatically refund the benefactors.

Let us go over it step-by-step:

## Step 1: Importing the IERC-20 Dependency
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./IERC20.sol";

The first thing we did was to specify the license of the contract to MIT; this is the license that allows you to use this for educational and general open-source purposes.

Then we declared the version of Solidity compiler we want to use. We imported an IERC20 interface. For that purpose, please create another file named IERC20.sol and paste this interface:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

interface IERC20 {
function allowance(address owner, address spender)
external
view
returns (uint256 remaining);

function approve(address spender, uint256 value)
external
returns (bool success);

function balanceOf(address owner) external view returns (uint256 balance);

function decimals() external view returns (uint8 decimalPlaces);

function decreaseApproval(address spender, uint256 addedValue)
external
returns (bool success);

function increaseApproval(address spender, uint256 subtractedValue)
external;

function name() external view returns (string memory tokenName);

function symbol() external view returns (string memory tokenSymbol);

function totalSupply() external view returns (uint256 totalTokensIssued);

function transfer(address to, uint256 value)
external
returns (bool success);

function transferFrom(
address from,
address to,
uint256 value
) external returns (bool success);
}

Step 2: The Campaign and Benefactors Struct

struct Campaign{
address payable beneficiary;
uint moneyRaised;
uint target;
uint beginning;
uint ending;
bool withdrawn;
Benefactors[] benefactorsInfo;
}

struct Benefactors {
address benefactor;
uint amount;
}

Just like other cousin languages, struct which is a variable container is also available in Solidity. For this contract, we created two separate structs for packing two sets of details.

Foremost, we declared a struct for the Campaign itself. Here, we inputted the address of the beneficiary, the money that has been raised so far, and the target of the campaign.

For the sake of time, we put in variables to track the beginning and end of the campaign. Since we can have several counts of the campaign, we would need to ascertain if the beneficiary of a particular campaign has withdrawn or not.

That is the reason we declared a boolean data type for withdrawal. Finally, we would need to pack in the array of the entire benefactors who gave towards a campaign in the struct.

Moving on to the second struct, its use is to track those who contributed to the crowdfunding. We are tracking two details about everyone of them; their addresses and the amount each of them gave.

Step 3: The Rest of The State Variables

   IERC20 public immutable token;

mapping(uint256 => Campaign) public campaigns;

// this mapping will be useful for ERC-20 transferFrom
mapping(uint256 => mapping(address => uint256)) public trackRaisedMoney;

uint256 public campaignCount;

constructor(address _token) {
if (_token == address(0)) revert();
token = IERC20(_token);
}

We introduced the ERC-20 token to this contract. Then we created two mappings. The first simple mapping will store the instances or lots of each campaign that will be created through this contract.

On the other hand, the following nested mapping will be useful for the transferFrom function, which we will eventually work on. Since we can have more than one campaign lots, we created a variable called campaignCount to track the campaigns.

The basic thing we did in the constructor was to initialize our token address.

Step 4: The getEndDate Function

 function getEndDate(uint8 _days) private pure returns (uint256) {
if (_days < 0) revert();
return uint256(_days * 86400);

We need to create this function first because we will need it during the launch. This function contains an if statement that a hypothetical day is less than 0, the EVM should return the day the campaign ends.

Step 5: The kickOff Function

  function kickOff(
address _beneficiary,
uint256 _target,
uint8 _endingDays
) external returns (uint256) {
// do this for auto-incrementation
campaignCount++;
campaigns[campaignCount].beneficiary = payable(_beneficiary);
campaigns[campaignCount].moneyRaised = 0;
campaigns[campaignCount].target = _target;
campaigns[campaignCount].beginning = block.timestamp;
campaigns[campaignCount].ending =
campaigns[campaignCount].beginning +
getEndDate(_endingDays);
uint256 endDate = campaigns[campaignCount].ending;
campaigns[campaignCount].withdrawn = false; // because the default of bool is false

require(
endDate < block.timestamp + 30 days,
"Campaign must end in 30 days"
);

emit Start(
campaignCount,
_beneficiary,
_target,
block.timestamp,
endDate
);
return campaignCount;
}

The kickOff function, as the name implies, is the function that will launch the contract. We incremented the campaignCount and set the members of the campaign struct. While most of the settings were done in the normal way and are straightforward, pay closer attention to how we set the ending member.

We added the beginning to the result of the getEndDate function we defined earlier. After this, we put a check that the day the campaign will end must be less than or equal to 30 days.

Moving on, we emitted some events into the log and return the campaignCount.

Step 6: The Give Function

    function give(uint256 _benefactorsId, uint256 _amount) external {
require(
campaigns[_benefactorsId].moneyRaised <=
campaigns[_benefactorsId].target,
"the target is reached already"
);
require(
block.timestamp <= campaigns[_benefactorsId].ending,
"can only give when the campaign has not ended"
);

token.transferFrom(msg.sender, address(this), _amount);
campaigns[_benefactorsId].moneyRaised += _amount;
trackRaisedMoney[_benefactorsId][msg.sender] += _amount;

Campaign storage campaign = campaigns[_benefactorsId];
campaign.benefactorsInfo.push(Benefactors(msg.sender, _amount));
emit Give(_benefactorsId, msg.sender, _amount);
}

This is the function that the benefactors will use to donate money towards the success of a campaign. We put in two checks before a benefactor can donate.

First, it must be the case that the total funds that have been raised so far have not reached the desired goal. Otherwise, there will be no need for donations any longer.

Second, a benefactor can only donate when the contract has not ended.

Once a benefactor has passed these checks, they can donate any amount to the beneficiary. After making the transfer call, we incremented the total amount in the pool.

For record purposes, we push each benefactor into the benefactor's array along with their addresses and the amount they donated.

Step 7: The unGive Function

  function undoGiving(uint256 _benefactorsId, uint256 _amount) external {
require(
block.timestamp <= campaigns[_benefactorsId].ending,
"can only ungive when the campaign has not ended"
);

// check that user indeed has token balance using the TRACKTOKENRAISED MAPPING
require(
trackRaisedMoney[_benefactorsId][msg.sender] >= _amount,
"Insufficient Balance"
);

campaigns[_benefactorsId].moneyRaised -= _amount;

trackRaisedMoney[_benefactorsId][msg.sender] -= _amount;
token.transfer(msg.sender, _amount);

// to remove msg.sender from benefactors
Campaign storage campaign = campaigns[_benefactorsId];
uint256 len = campaign.benefactorsInfo.length;
for (uint256 i = 0; i < len; ++i) {
Benefactors memory person = campaign.benefactorsInfo[i];
if (person.benefactor == msg.sender) {
campaign.benefactorsInfo[i] = campaign.benefactorsInfo[len - 1];
}
}
campaign.benefactorsInfo.pop();

emit UnGive(_benefactorsId, msg.sender, _amount);
}

Well, the reality is that sometimes, the donors might change their minds and conclude to take their donation back. Thus, there should be a function for this to allow fairness.

The first requirement we put in was that no donor can get a refund once the campaign has ended. Secondly, the said benefactor must have actually donated earlier.

Then we deducted the amount from the pool and sent the money back to the donor.

Now, we need to go a step further to remove such a donor from the benefactor's array.

Thus, we will need to carry out a for loop, and for composability, we stored what should have been campaign.benefactorsInfo.length variable. Afterward, we created a logic that such an earlier investor should be the last person in the benefactors' array.

Then we called a .pop, a method that removes the last person in an array.

Step 8: The Withdrawal Function

 function withdrawal(uint256 _Id) external {
require(
campaigns[_Id].beneficiary == msg.sender,
"Error, only the beneficiary can withdraw!"
);
require(
block.timestamp > campaigns[_Id].ending,
"cannot withdraw before ending"
);

require(campaigns[_Id].moneyRaised >= campaigns[_Id].target); // should be greater than or equal to
require(!campaigns[_Id].withdrawn, "Withdrawn already"); // recall that the default of bool is false

campaigns[_Id].withdrawn = true;
token.transfer(campaigns[_Id].beneficiary, campaigns[_Id].moneyRaised);

emit Withdrawal(_Id);
}

This is a vital function in the contract. We set out four conditions that are native to withdrawing:

  • Only the beneficiary can withdraw
  • The beneficiary can only withdraw when the campaign has ended
  • The money that has been raised should have hit the target
  • The funds in the current lot of the campaign must not have been withdrawn before

Before we can make the transfer call, we have to set the boolean withdrawal time from its false default to true. Then we made a transfer call where we sent the money raised to the beneficiary.

Step 9: The Refund Function

    // if the goal of the campaign is not met, everyone who donated should be refunded
function refund(uint256 _benefactorsId) external {
require(
block.timestamp > campaigns[_benefactorsId].ending,
"cannot withdraw before ending"
);
require(
campaigns[_benefactorsId].moneyRaised <
campaigns[_benefactorsId].target
);

uint256 bal = trackRaisedMoney[_benefactorsId][msg.sender];
// reset the balance
trackRaisedMoney[_benefactorsId][msg.sender] = 0;
token.transfer(msg.sender, bal);

emit Refund(_benefactorsId, msg.sender, bal);
}

What happens to all the money that has been raised if the campaign turns out to be unsuccessful? The purpose of this function is that the donors can call it to get their funds back if it were to be the case that they did not meet the target at the end of the day.

Step 10: The Detail Getter Functions

  function checkSuccess(uint256 _campaignCount)
external
view
returns (bool success)
{
if (
campaigns[_campaignCount].moneyRaised >=
campaigns[_campaignCount].target
// should be greater than or equal to
) {
success = true;
}
}

function getContributorAmount(uint256 _benefactorsInfo)
external
view
returns (uint256)
{
// the Data of everyone who contributed to the project is stored in the trackRaisedMoney mapping
return trackRaisedMoney[_benefactorsInfo][msg.sender];
}

// this function fetches us the data of everyone who contributed to the campaign
// first their number and the addresses of each of them
function getBenefactors(uint256 _benefactorsInfo)
external
view
returns (Benefactors[] memory)
{
// the Data of everyone who contributed to the project is stored in the trackRaisedMoney mapping
// generally unhealthy to use an array

Campaign memory campaign = campaigns[_benefactorsInfo];
// return campaigns[_benefactorsInfo].length;

return campaign.benefactorsInfo; // originally not initialized

// abeg help me fix this --Done my chief!
}

The rest of the functions in this contract are getter functions for some important details:

  • The checkSuccess function checks if any particular campaign was successful functions
  • The getContributorAmount function returns the amount that each donor gave
  • The getBenefactors returns all the donors that gave towards a particular outreach

Eventually, your full code base should appear like this:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./IERC20.sol";

contract RaiseMoney {
event Start(
uint256 id,
address benefactor,
uint256 target,
uint256 beginning,
uint256 ending
);
event Give(uint256 id, address benefactors, uint256 amount);
event UnGive(uint256 id, address benefactor, uint256 amount);
event Withdrawal(uint256 id);
event Refund(uint256 id, address benefactor, uint256 balance);

// struct to pack the variables of the campaign

struct Benefactors {
address benefactor;
uint256 amount;
}

// struct to pack the variables of the campaign
struct Campaign {
address payable beneficiary;
uint256 moneyRaised;
uint256 target;
uint256 beginning;
uint256 ending;
bool withdrawn;
Benefactors[] benefactorsInfo;
}

IERC20 public immutable token;

mapping(uint256 => Campaign) public campaigns;

// this mapping will be useful for ERC-20 transferFrom
mapping(uint256 => mapping(address => uint256)) public trackRaisedMoney;

uint256 public campaignCount;

constructor(address _token) {
if (_token == address(0)) revert();
token = IERC20(_token);
}
function getEndDate(uint8 _days) private pure returns (uint256) {
if (_days < 0) revert();
return uint256(_days * 86400);
}
/*
*@dev the _beginning param in the kickOff function
* was modifed to block.timestamp
*/

function kickOff(
address _beneficiary,
uint256 _target,
uint8 _endingDays
) external returns (uint256) {
// do this for auto-incrementation
campaignCount++;
campaigns[campaignCount].beneficiary = payable(_beneficiary);
campaigns[campaignCount].moneyRaised = 0;
campaigns[campaignCount].target = _target;
campaigns[campaignCount].beginning = block.timestamp;
campaigns[campaignCount].ending =
campaigns[campaignCount].beginning +
getEndDate(_endingDays);
uint256 endDate = campaigns[campaignCount].ending;
campaigns[campaignCount].withdrawn = false; // because the default of bool is false

require(
endDate < block.timestamp + 30 days,
"Campaign must end in 30 days"
);

emit Start(
campaignCount,
_beneficiary,
_target,
block.timestamp,
endDate
);
return campaignCount;
}

function give(uint256 _benefactorsId, uint256 _amount) external {
require(
campaigns[_benefactorsId].moneyRaised <=
campaigns[_benefactorsId].target,
"the target is reached already"
);
require(
block.timestamp <= campaigns[_benefactorsId].ending,
"can only give when the campaign has not ended"
);

token.transferFrom(msg.sender, address(this), _amount);
campaigns[_benefactorsId].moneyRaised += _amount;
trackRaisedMoney[_benefactorsId][msg.sender] += _amount;

Campaign storage campaign = campaigns[_benefactorsId];
campaign.benefactorsInfo.push(Benefactors(msg.sender, _amount));
emit Give(_benefactorsId, msg.sender, _amount);
}

function undoGiving(uint256 _benefactorsId, uint256 _amount) external {
require(
block.timestamp <= campaigns[_benefactorsId].ending,
"can only ungive when the campaign has not ended"
);

// check that user indeed has token balance using the TRACKTOKENRAISED MAPPING
require(
trackRaisedMoney[_benefactorsId][msg.sender] >= _amount,
"Insufficient Balance"
);

campaigns[_benefactorsId].moneyRaised -= _amount;

trackRaisedMoney[_benefactorsId][msg.sender] -= _amount;
token.transfer(msg.sender, _amount);

// to remove msg.sender from benefactors
Campaign storage campaign = campaigns[_benefactorsId];
uint256 len = campaign.benefactorsInfo.length;
for (uint256 i = 0; i < len; ++i) {
Benefactors memory person = campaign.benefactorsInfo[i];
if (person.benefactor == msg.sender) {
campaign.benefactorsInfo[i] = campaign.benefactorsInfo[len - 1];
}
}
campaign.benefactorsInfo.pop();

emit UnGive(_benefactorsId, msg.sender, _amount);
}

function withdrawal(uint256 _Id) external {
require(
campaigns[_Id].beneficiary == msg.sender,
"Error, only the beneficiary can withdraw!"
);
require(
block.timestamp > campaigns[_Id].ending,
"cannot withdraw before ending"
);

require(campaigns[_Id].moneyRaised >= campaigns[_Id].target); // should be greater than or equal to
require(!campaigns[_Id].withdrawn, "Withdrawn already"); // recall that the default of bool is false

campaigns[_Id].withdrawn = true;
token.transfer(campaigns[_Id].beneficiary, campaigns[_Id].moneyRaised);

emit Withdrawal(_Id);
}

// if the goal of the campaign is not met, everyone who donated should be refunded
function refund(uint256 _benefactorsId) external {
require(
block.timestamp > campaigns[_benefactorsId].ending,
"cannot withdraw before ending"
);
require(
campaigns[_benefactorsId].moneyRaised <
campaigns[_benefactorsId].target
);

uint256 bal = trackRaisedMoney[_benefactorsId][msg.sender];
// reset the balance
trackRaisedMoney[_benefactorsId][msg.sender] = 0;
token.transfer(msg.sender, bal);

emit Refund(_benefactorsId, msg.sender, bal);
}

// to check if a particular count of fundraising has been successful
function checkSuccess(uint256 _campaignCount)
external
view
returns (bool success)
{
if (
campaigns[_campaignCount].moneyRaised >=
campaigns[_campaignCount].target
// should be greater than or equal to
) {
success = true;
}
}

function getContributorAmount(uint256 _benefactorsInfo)
external
view
returns (uint256)
{
// the Data of everyone who contributed to the project is stored in the trackRaisedMoney mapping
return trackRaisedMoney[_benefactorsInfo][msg.sender];
}

// this function fetches us the data of everyone who contributed to the campaign
// first their number and the addresses of each of them
function getBenefactors(uint256 _benefactorsInfo)
external
view
returns (Benefactors[] memory)
{
// the Data of everyone who contributed to the project is stored in the trackRaisedMoney mapping
// generally unhealthy to use an array

Campaign memory campaign = campaigns[_benefactorsInfo];
// return campaigns[_benefactorsInfo].length;

return campaign.benefactorsInfo;
}
}

Creating the ERC-20 Contract

Since we imported an IERC20 interface, we will need to create an ERC-20 contract alongside our crowdfunding contract. Create a file and name it testERC.sol.

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract FooCoin is ERC20 {
address public owner;

constructor(uint256 initialAmount) ERC20("TestERC", "TST") {
owner = msg.sender;
_mint(msg.sender, initialAmount);
}

function mintMore(uint256 _amount) public {
// require(msg.sender == owner, "Only Owner can call this fxn!");
_mint(msg.sender, _amount);
}
}

We inherited an ERC-20 interface from Open Zeppelin, and the main function we included was one that allowed us to mint tokens. Compile this contract

Compilation

Save all your contracts. Now, go to your raisemoney.sol contract and compile on the terminal with npx hardhat compile. Other things being equal, your contract should compile.

Testing of the Contract

We have to test the functions in our contract to ensure everything runs well. On this note, create a file and name it test.js. Paste this test script:

const {
loadFixture, time
} = require("@nomicfoundation/hardhat-network-helpers");
const { anyValue } = require("@nomicfoundation/hardhat-chai-matchers/withArgs");
const { expect, assert } = require("chai");
const { ethers } = require("hardhat")

describe("Deploy Raisemoney", function () {
// We define a fixture to reuse the same setup in every test. or use a beforeEach function
// We use loadFixture to run this setup once, snapshot that state,
// and reset Hardhat Network to that snapshot in every test.
async function deployRaiseMoney() {

// const ONE_ETH = 1_000_000_000_000_000_000_000;

const getThousand = ethers.utils.parseUnits("1000", "ether");
const getFiveHundred = ethers.utils.parseUnits("500", "ether");
const getTwoHundred = ethers.utils.parseUnits("200", "ether");
const getHundred = ethers.utils.parseUnits("100", "ether");
const getFifty = ethers.utils.parseUnits("50", "ether");


// Contracts are deployed using the first signer/account by default
const [owner, user1, user2, user3] = await ethers.getSigners();

const raise_Money = await ethers.getContractFactory("RaiseMoney");
const test_ERC = await ethers.getContractFactory("MobiCoin");

const testERC = await test_ERC.deploy(getThousand);
const raiseMoney = await raise_Money.deploy(testERC.address);


return { testERC, raiseMoney, owner, user1, user2, user3, getThousand, getFiveHundred, getTwoHundred, getHundred, getFifty };
}

describe("Deployment", function () {

async function loadTokenfeatures() {
const { testERC, raiseMoney, owner, user1, user2, user3, getThousand, getFiveHundred, getTwoHundred, getHundred, getFifty } = await loadFixture(deployRaiseMoney);
let tx1 = await testERC.connect(user1).mintMore(getFiveHundred);
let tx2 = await testERC.connect(user2).mintMore(getFiveHundred);
let tx3 = await testERC.connect(user3).mintMore(getFiveHundred);

let ownerBalance = await testERC.balanceOf(owner.address);
let user1Bal = await testERC.balanceOf(user1.address);
let user2Bal = await testERC.balanceOf(user2.address);
let user3Bal = await testERC.balanceOf(user3.address);

return { ownerBalance, user1Bal, user2Bal, user3Bal, testERC, raiseMoney, owner, user1, user2, user3, getThousand, getFiveHundred, getTwoHundred, getHundred, getFifty };
}


it("Should mint more coins for other users", async function () {
// To load state from fixtures
const { ownerBalance, user1Bal, user2Bal, user3Bal, user4Bal, getThousand, getFiveHundred } = await loadFixture(loadTokenfeatures);


expect(ownerBalance).to.equal(getThousand);
expect(user1Bal).to.equal(getFiveHundred);
expect(user2Bal).to.equal(getFiveHundred);
expect(user3Bal).to.equal(getFiveHundred);

});

it("Should set the right token address", async function () {
// loading from fixtures...
const { testERC, raiseMoney } = await loadFixture(loadTokenfeatures);

expect(await raiseMoney.token()).to.equal(testERC.address);
});

it("should be able to kickoff properly", async function () {
const { owner, user1, user2, raiseMoney, getTwoHundred } = await loadFixture(deployRaiseMoney);

let tx1 = await raiseMoney.connect(owner).kickOff(user1.address, getTwoHundred, 29);
let campaignCount = await raiseMoney.campaignCount();
expect(campaignCount).to.equal(1);

// To test an error due to the condition "Campaign must end in 30 days"
await expect(raiseMoney.connect(user2).kickOff(user2.address, getTwoHundred, 31)).to.be.reverted;
});
// console.log("Kickoff works as expected!");

it("Should be able to give properly", async function () {
const { owner, user1, user2, user3, raiseMoney, testERC, getFiveHundred, getTwoHundred, getFifty } = await loadFixture(
loadTokenfeatures
);
await raiseMoney.connect(owner).kickOff(user1.address, getTwoHundred, 29);
// To approve the contract
await testERC.connect(user1).approve(raiseMoney.address, getFiveHundred)
await testERC.connect(user2).approve(raiseMoney.address, getFiveHundred)
await testERC.connect(user3).approve(raiseMoney.address, getFiveHundred)

await raiseMoney.connect(user1).give(1, getFifty);
await raiseMoney.connect(user2).give(1, getTwoHundred);

let [beneficiary, moneyraised] = await raiseMoney.campaigns(1);

expect(moneyraised).to.be.greaterThanOrEqual(
getTwoHundred
);

// To track the raised money of an address
let user2contributions = await raiseMoney.connect(user2).trackRaisedMoney(1, user2.address)
expect(user2contributions).to.equal(getTwoHundred);

// To test that a user can't give after target is reached
await expect(raiseMoney.connect(user2).give(1, getTwoHundred)).to.be.reverted;

// console.log(await raiseMoney.getBenefactors(1));
});

it("Should Undo giving", async function () {
const { owner, user1, user2, raiseMoney, testERC, getFiveHundred, getTwoHundred, getFifty } = await loadFixture(
loadTokenfeatures
);
await raiseMoney.connect(owner).kickOff(user1.address, getTwoHundred, 20);
// To approve the contract
await testERC.connect(user1).approve(raiseMoney.address, getFiveHundred)
await testERC.connect(user2).approve(raiseMoney.address, getFiveHundred)

// to give
await raiseMoney.connect(user1).give(1, getFifty);
await raiseMoney.connect(user2).give(1, getTwoHundred);

// to ungive
await raiseMoney.connect(user1).undoGiving(1, getFifty);

let user1contributions = await raiseMoney.trackRaisedMoney(1, user1.address);
expect(user1contributions).to.equal(0);

// It shoud fail when a user attempts to ungive beyond his balance
await expect(raiseMoney.connect(user1).undoGiving(1, getFiveHundred)).to.be.reverted;
});

it("Should Check Success after campaign target is reached", async function () {
const { owner, user1, user2, raiseMoney, testERC, getFiveHundred, getTwoHundred, getFifty } = await loadFixture(
loadTokenfeatures
);
await raiseMoney.connect(owner).kickOff(user1.address, getTwoHundred, 20);
// To approve the contract
await testERC.connect(user1).approve(raiseMoney.address, getFiveHundred)
await testERC.connect(user2).approve(raiseMoney.address, getFiveHundred)

// to give
await raiseMoney.connect(user1).give(1, getFifty);
await raiseMoney.connect(user2).give(1, getTwoHundred);
let checkSuccessBool = await raiseMoney.checkSuccess(1);
expect(checkSuccessBool).to.equal(true);
});
});

describe("Withdrawals", function () {

// Using fixures again
async function loadTokenfeatures() {
const { testERC, raiseMoney, owner, user1, user2, user3, getThousand, getFiveHundred, getTwoHundred, getHundred, getFifty } = await loadFixture(deployRaiseMoney);
let tx1 = await testERC.connect(user1).mintMore(getFiveHundred);
let tx2 = await testERC.connect(user2).mintMore(getFiveHundred);
let tx3 = await testERC.connect(user3).mintMore(getFiveHundred);

let ownerBalance = await testERC.balanceOf(owner.address);
let user1Bal = await testERC.balanceOf(user1.address);
let user2Bal = await testERC.balanceOf(user2.address);
let user3Bal = await testERC.balanceOf(user3.address);

return { ownerBalance, user1Bal, user2Bal, user3Bal, testERC, raiseMoney, owner, user1, user2, user3, getThousand, getFiveHundred, getTwoHundred, getHundred, getFifty };
}

it("Should revert with the right error if called too soon", async function () {
const { owner, user1, user2, raiseMoney, testERC, getFiveHundred, getTwoHundred, getFifty } = await loadFixture(
loadTokenfeatures
);
await raiseMoney.connect(owner).kickOff(user1.address, getTwoHundred, 20);
// To approve the contract
await testERC.connect(user1).approve(raiseMoney.address, getFiveHundred)
await testERC.connect(user2).approve(raiseMoney.address, getFiveHundred)

// to give
await raiseMoney.connect(user1).give(1, getFifty);
await raiseMoney.connect(user2).give(1, getTwoHundred);

await expect(raiseMoney.connect(user1).withdrawal(1)).to.be.revertedWith(
"cannot withdraw before ending"
);

});

it("Should revert with the right error if called from another account", async function () {
const { owner, user1, user2, raiseMoney, testERC, getFiveHundred, getTwoHundred, getFifty } = await loadFixture(
loadTokenfeatures
);
await raiseMoney.connect(owner).kickOff(user1.address, getTwoHundred, 20);
// To approve the contract
await testERC.connect(user1).approve(raiseMoney.address, getFiveHundred)
await testERC.connect(user2).approve(raiseMoney.address, getFiveHundred)

// to give
await raiseMoney.connect(user1).give(1, getFifty);
await raiseMoney.connect(user2).give(1, getTwoHundred);

await expect(raiseMoney.connect(user2).withdrawal(1)).to.be.revertedWith(
"Error, only the beneficiary can withdraw!"
);
});

it("Shouldn't fail if the unlockTime has arrived and the owner calls it", async function () {
const { owner, user1, user2, raiseMoney, testERC, getFiveHundred, getTwoHundred, getFifty } = await loadFixture(
loadTokenfeatures
);
await raiseMoney.connect(owner).kickOff(user1.address, getTwoHundred, 20);
// To approve the contract
await testERC.connect(user1).approve(raiseMoney.address, getFiveHundred)
await testERC.connect(user2).approve(raiseMoney.address, getFiveHundred)

// to give
await raiseMoney.connect(user1).give(1, getFifty);
await raiseMoney.connect(user2).give(1, getTwoHundred);

const ONE_MONTH_IN_SECS = 31 * 24 * 60 * 60;
const unlockTime = (await time.latest()) + ONE_MONTH_IN_SECS;

// Transactions are sent using the first signer by default
await time.increaseTo(unlockTime);

await expect(raiseMoney.connect(user1).withdrawal(1)).not.to.be.reverted;
});

it("Refund Benefactors tokens if target isn't met", async function () {
const { owner, user1, user2, raiseMoney, testERC, getFiveHundred, getTwoHundred, getFifty } = await loadFixture(
loadTokenfeatures
);
await raiseMoney.connect(owner).kickOff(user1.address, getFiveHundred, 20);
// To approve the contract
await testERC.connect(user1).approve(raiseMoney.address, getFiveHundred)
await testERC.connect(user2).approve(raiseMoney.address, getFiveHundred)

// to give
await raiseMoney.connect(user1).give(1, getFifty);
await raiseMoney.connect(user2).give(1, getTwoHundred);

const ONE_MONTH_IN_SECS = 31 * 24 * 60 * 60;
const unlockTime = (await time.latest()) + ONE_MONTH_IN_SECS;

// Transactions are sent using the first signer by default
await time.increaseTo(unlockTime);

await expect(raiseMoney.connect(user1).refund(1)).not.to.be.reverted;
});

});
});

Once you have compiled it, this should show on your terminal: compile and test

Now, we can move to the next phase, which is deployment to the Celo Alfajores Testnet.

Deployment of the Crowdfunding Contract to Celo

Before you plan to deploy, ensure you delete the configuration file–which is the Hardhat.config.js in your Hardhat and replace it with this:

require("@nomiclabs/hardhat-waffle");
require("dotenv").config({ path: ".env" });
require("hardhat-deploy");

// Prints the Celo accounts associated with the mnemonic in .env
task("accounts", "Prints the list of accounts", async (taskArgs, hre) => {
const accounts = await hre.ethers.getSigners();

for (const account of accounts) {
console.log(account.address);
}
});

/**
* @type import('hardhat/config').HardhatUserConfig
*/
module.exports = {
defaultNetwork: "alfajores",
networks: {
localhost: {
url: "http://127.0.0.1:7545",
},
alfajores: {
gasPrice: 1500000000,
gas: 4100000,
url: "https://alfajores-forno.celo-testnet.org",
accounts: {
mnemonic: process.env.MNEMONIC,
path: "m/44'/52752'/0'/0",
},
//chainId: 44787
},
celo: {
url: "https://forno.celo.org",
accounts: {
mnemonic: process.env.MNEMONIC,
path: "m/44'/52752'/0'/0",
},
chainId: 42220,
},
},
solidity: "0.8.0",
};

Then deploy the contract with hardhat run scripts/deploy.js --network alfajores. This was the contract address of our contract: 0xEF87eDE6F4713De16d83Ca08cf0f8A6a263Db60A.

deploy

You can always verify your contracts at Celoscan

Conclusion

Congratulations, you just learned how to build and deploy a crowdfunding contract on the Celo blockchain. This project should have enhanced your contract of not only smart contract development with Solidity but also the intricacies of successfully building on Celo.

Ensure you take your time to code along in this tutorial, and push your code to GitHub when you are done.

Next Steps

For more real-time visual testing, I recommend you test this contract on Remix, using up to 3 addresses to contribute to the crowdfunding contract.

About the Author

John Fawole is a blockchain technical writer and Solidity dev; ; connect with him on LinkedIn.

Go back

· 12 min read

header

Introduction​

In this article, we'll cover how to build an on-chain DAO with Hardhat, Solidity, and JavaScript. If you're unaware, many crypto projects are attempting to utilize DAOs (decentralized autonomous organizations) for project governance. The decentralized nature of cryptocurrency makes DAOs a popular governance model in the blockchain industry.

With this one, we're getting right into the code. The DAO's governance token will be an ERC20 token. This ERC20 token will be used to create and vote on proposals.

Prerequisites​

You must be familiar with the following to fully understand this tutorial:

  • HardHat: Hardhat is an extensible developer tool that helps smart contract developers.
  • Solidity: A high-level programming language.
  • Javascript: You should be familiar with the language's fundamentals.
  • Chai: To test smart contracts, we'll use a javascript testing package.

Requirements​

To follow along, your computer must have the most recent version of Node.js installed. Ensure Node is at least version 16.0.0 or higher if it is already installed.

Hardhat Project Setup

Create a folder called DAO and open it in VS Code. Run the following command to set up a new hardhat project in the terminal:

npx hardhat .

Remove all the files from the contracts and test folder once the hardhat setup is complete. Create the following three files: DAO.sol, Token.sol, and dao.test.js. The contracts folder should have the .sol files, and the test folder should contain the .js file.

We will need to install some dev dependencies so that the hardhat project will work properly. To install those, run the following command in the terminal:

npm install --save-dev "hardhat@^2.12.2" "@nomicfoundation/hardhat-toolbox@^2.0.0" "solidity-coverage"

We will also need to install OpenZeppelin Library as dependency for our project:

npm install "@openzeppelin/contracts"

Let’s see what these can help us with:

  • hardhat: Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.
  • @nomicfoundation/hardhat-toolbox: It bundles all the commonly used packages and Hardhat plugins.
  • solidity-coverage: It is a solidity code coverage plugin for Hardhat.
  • @openzeppelin/contracts: It is a secure smart contract library for solidity

Governance Token Smart Contract

Using the ERC20 token standard, we will develop a governance token for our DAO. Open Token.sol and insert the following code:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract Token is ERC20 {

constructor() ERC20("Token", "TOKEN") {
_mint(msg.sender, 1000000);
}

}

Please go here to read more about how the above snippet works. We can now go to the smart contract for the DAO.

DAO Smart Contract

To get things going, we'll construct an empty smart contract called DAO in the DAO.sol file and import IERC20.sol from the openzeppelin library.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract DAO {

}

Constructor Definition

Now that we have a blank smart contract, we can declare a variable inside of it with the name token of type IERC20.

The next step is creating the smart contract constructor, which takes the token's address as an argument. We keep the token address as a variable because we want our DAO contract to work with any ERC20 token. We will initialize the Token variable inside the constructor by assigning a value using the IERC20 interface and handing it an argument of the contract address.

contract DAO {

IERC20 public token;

constructor(address _token) {
token = IERC20(_token);
}
}

Defining Proposal

The option to propose and vote on proposals will be available to anyone who has a governance token and chooses to participate in the governance process. We must outline the structure of those proposals. A struct will define the format for each proposal we will create named Proposal.

    struct Proposal {
uint256 accept;
uint256 reject;
uint256 abstain;
uint256 deadline;
bytes32 title;
mapping(address => bool) voted;
}

Note: Everything other than functions goes above the constructor, and all the functions go below the constructor. Just a tip on organizing smart contracts

Storing Created Proposals

We need to store any proposal that is made. By creating a mapping called proposals, we will achieve this. The proposal will serve as the value, and the proposal's index, which we will track using the proposalIndex variable, will be the key in the mapping.

    uint256 public proposalIndex;
mapping(uint256 => Proposal) public proposals;

Create Proposal

After putting up everything related to proposals, there is just a function left that allows us to construct proposals. Starting off, let's define the function createProposal with visibility as public. This function returns the index of the created proposal (type uint256) and accepts the title (type bytes32) argument.

To access the proposals mapping with the current value of proposalIndex, we will first create a variable named proposal (with type Proposal and data location of storage). Then, by setting the value of the title (the function argument) and the current block timestamp plus one day, we will set the title and the deadline for the proposal we had gotten from the mapping. Finally, add 1 to proposalIndex's value and return the index of the proposal you just created.

    function createProposal(bytes32 _proposal) public returns(uint256) {
Proposal storage proposal = proposals[proposalIndex];

proposal.title = _proposal;
proposal.deadline = block.timestamp + 1 days;

proposalIndex++;

return proposalIndex - 1;
}

There is a problem; we only want governance token holders to have access to this service; we don't want anyone else to be able to create proposals. This can be resolved by introducing a modifier called onlyTokenHolders that checks to see if the caller's balance of their governance token is greater than zero and rejects the call otherwise. We can check the balance by using the token variable's balanceOf method.

    modifier onlyTokenHolders() {
require(
token.balanceOf(msg.sender) > 0,
"NOT_A_TOKEN_HOLDER"
);
_;
}

This modifier can be added to the createProposal function.

function createProposal(bytes32 _proposal) public onlyTokenHolders returns(uint256)

Vote on the Proposal

We've made it possible to propose, and now we can write a function so that token holders can vote on proposals. Only token holders who hadn't already voted before the deadline should be allowed to cast a vote.

The index of the proposal and the type of vote the token holder must cast are the two arguments this function should accept. We must limit the type of vote to just three options with an enum named Vote: accept, reject, and abstain.

    enum Vote {
accept,
reject,
abstain
}

We will fetch the proposal with the index supplied as an argument to the voteOnProposal function. Then, using the require statement, we'll see if the deadline has not passed or if the token holder has not already cast a vote. We shall set the voted flag for that token holder to true if all conditions are met. Following that, we will increase the number of votes cast by token holders' balance of governance tokens in accordance with their votes.

    function voteOnProposal(uint256 _index, Vote vote) public onlyTokenHolders {

Proposal storage proposal = proposals[_index];

require(block.timestamp < proposal.deadline, "INACTIVE_PROPOSAL");
require(proposal.voted[msg.sender] == false, "ALREADY_VOTED");

proposal.voted[msg.sender] = true;

if (vote == Vote.accept) {
proposal.accept += token.balanceOf(msg.sender);
} else if (vote == Vote.reject) {
proposal.reject += token.balanceOf(msg.sender);
} else {
proposal.abstain += token.balanceOf(msg.sender);
}

}

Execute Proposal

When the time has passed, the proposal will be executed by emitting an event. First, let's define the event called the winner. This event will take arguments, the proposal's index, its title, and the number of votes it received.

    event winner(uint256 _index, bytes32 proposal, Vote winningVote);

We must first determine whether the proposal is active; if it is, we cannot execute it. If not, we'll look at which votes received the most and run the event appropriately.

    function executeProposal(uint256 _index)  public {

Proposal storage proposal = proposals[_index];

require(block.timestamp > proposal.deadline, "ACTIVE_PROPOSAL");

if (proposal.accept >= proposal.reject) {
if (proposal.accept >= proposal.abstain) {
emit winner(_index, proposal.title, Vote.accept);
} else {
emit winner(_index, proposal.title, Vote.abstain);
}
}
else {
if (proposal.reject >= proposal.abstain){
emit winner(_index, proposal.title, Vote.reject);
} else{
emit winner(_index, proposal.title, Vote.abstain);
}
}
}

Testing Smart Contract

To test the contract, we're going to a chai and mocha library. To ascertain how many components of the smart contract have been tested, we will also use the hardhat coverage plugin. First, let's configure the hardhat coverage plugin. The only thing left to do is add the following to hardhat.config.js, as we have already installed hardhat coverage in the beginning:

require("solidity-coverage");

To determine how much of the part has been tested, enter the following command into the terminal:

npx hardhat coverage

This is the intended result:

Test result with zero coverage of the smart contract

Write some tests right away. You should feel comfortable running tests with chai and mocha. Some basics are as follows: Tests are created within it function and are arranged using describe. Please read this if you want to learn more about testing.

We'll use the AAA writing format, which stands for Arrange, Action, and Assert. We first create the necessary conditions for the test (arrange), then we perform the activity we are testing (act), and last, we evaluate if we are getting the results we were expecting (assert).

We need to do imports from chai, ethers, and @nomicfoundation/hardhat-network-helpers in the dao.test.js file.

const {
time,
loadFixture,
} = require("@nomicfoundation/hardhat-network-helpers");
const { expect } = require("chai");
const { ethers } = require("hardhat");

Create a describe block with the title DAO in the dao.test.js file. We will put all of our test cases, and fixtures in this describe block. contractFixture, a fixture we'll make, will deploy the contracts and generate test accounts. We'll also make our first proposal and transfer some tokens to various addresses for testing.

describe("DAO", function () {

async function contractFixture() {

const accounts = await ethers.getSigners();

const TOKEN = await ethers.getContractFactory('Token');
const token = await TOKEN.deploy();

await token.deployed();

const DAO = await ethers.getContractFactory("DAO");
const dao = await DAO.deploy(token.address);

await dao.deployed();


await token.transfer(accounts[1].address, 13000);
await token.transfer(accounts[2].address, 32300);

await dao.createProposal(ethers.utils.formatBytes32String("first proposal"))

return { accounts, token, dao };
}
// ALL TESTS SHOULD GO BETWEEN START AND END LINE
// ---------START---------


// ---------END---------
})

The deployment of contracts will be tested in the first test. To test this, we will see if the DAO contract has our governance token address properly set and if 1 million governance tokens are available.

    it("Should set right contract address", async function () {
const { token, dao } = await loadFixture(contractFixture);

expect(await dao.token()).to.equal(token.address);
expect(await token.totalSupply()).to.equal(1000000);
});

Now that we can test proposal-related functions. Let's first see if the proposal is being created correctly. To do this, we'll get the first proposal and then check that all the fields are filled in with the right data.

    it('Should create correct proposal', async function () {
const { accounts, dao } = await loadFixture(contractFixture);

const proposal = await dao.proposals(0);

expect(proposal.title).to.eq(ethers.utils.formatBytes32String("first proposal"));
expect(proposal.accept).to.eq(0);
expect(proposal.reject).to.eq(0);
expect(proposal.abstain).to.eq(0);
})

We can go ahead and check the voting function. We will load the fixture, make a proposal, and then vote on it to see if the number of votes increases due to our voting.

    it('Should have correct accept votes', async function () {
const { accounts, dao } = await loadFixture(contractFixture);

await dao.connect(accounts[1]).voteOnProposal(0, 0);
await dao.connect(accounts[2]).voteOnProposal(0, 0);

const proposal = await dao.proposals(0);

expect(proposal.accept).to.eq(45300);
})

We can check to see if someone is voting twice because it is not permitted. Voting twice should result in an error.

    it('Should revert with already voted', async function () {
const { accounts, dao } = await loadFixture(contractFixture);

await dao.voteOnProposal(0, 0);

await expect(dao.voteOnProposal(0, 0)).to.be.rejectedWith("ALREADY_VOTED");
})

The contract specifically forbids voting past the deadline, so now is a good time to put that condition to the test. With the help of the time module we imported from @nomicfoundation/hardhat-network-helpers, we may increase the block timestamp. We will vote after increasing the timestamp, which should result in an error.

    it('Should revert with inactive proposal', async function () {
const { accounts, dao } = await loadFixture(contractFixture);

await time.increase(time.duration.days(1));

await expect(dao.connect(accounts[1]).voteOnProposal(0, 0)).to.be.revertedWith("INACTIVE_PROPOSAL");
})

We can now go on to the testing executeProposal function. The timestamp must be larger than the deadline to run the executeProposal function. To test this, we will vote on the proposal, increase the timestamp, and then test to see which event is being emitted.

    it('Should emit event for first proposal on execution with accept vote as winner', async function () {
const { dao } = await loadFixture(contractFixture);

await dao.voteOnProposal(0, 0);
await time.increase(time.duration.days(1));

await expect(dao.executeProposal(0)).to.emit(dao, "winner").withArgs(0, ethers.utils.formatBytes32String("first proposal"), 0);
})

We can use the coverage command once more to run all of the tests and see how much of the smart contract we have tested. Output should resemble the following:

Test Result for partial coverage of the smart contract

Conclusion

You have successfully learned the following things from this article:

  • The on-chain DAO's workings
  • Interaction between ERC20 tokens from other contracts
  • Using Chai and Mocha to test solidity contracts
  • Use of the solidity coverage plugin

Next Steps​

We have tested the contract, but as you can see, we didn't test it completely. As a next step, apply what you learned in this article and work to reach as close to 100% test coverage as you can.

About the Author​

Nikhil Bhintade is the author of the article. Nikhil works as a product manager. He enjoys writing about cutting-edge technology and the things he is learning. You can see his most recent work on GitHub.

References​

Here is a reference to a project that was finished with tests that had 100% coverage. To understand more about using Hardhat, you may also refer to this page.

Go back

· 20 min read
Isaac Jesse

header

Introduction

As a web3 developer, you know you move up the ladder when your productivity speed increases. Being productive is not only measured in how fast you can deliver but in conjunction with how secure and usable your application is, which largely depends on how well you are familiar with using sophisticated tools. While Celo unpacks and unfolds the web3 mysteries enabling you to focus on logic to build user-centric applications, your part as the developer is knowing how to use these tools to increase your productivity level.

Prerequisites​

This tutorial focuses on advance practical methods of configuring and using Hardhat. so you should:

  • Know how to set up a basic Hardhat project. Please complete the previous example tutorial here as we’ll also make use of the previous example here. Clone the project, and navigate to the project directory.
git clone https://github.com/bobeu/advanced-hardhat-for-celo-dev.git
cd advanced-hardhat-for-celo-dev

Requirements​

To get started, ensure you have the following tools installed on the machine:

  • Code editor. VSCode recommended.
  • NodeJs version >=14.0.0.

Note: I am using Node version "18.12.1"

Hardhat plugins

Continuing from the last tutorial, we will install a few plugins to improve the deployment of the Lock contract to Celo networks and solidify testing.

An important highlight of Hardhat is the plugins that are external to Hardhat itself. Even though Hardhat comes with some default plugins, you can always override them to use what’s best for your needs. We will discuss some of the external programs - relevant plugins, or the plugins you most likely need. However, this does not include plugins that weren't built before this article was written. During Hardhat installation, If you choose to install the sample project to enable support for waffle tests, “hardhat-waffle” and “hardhat-ethers” are installed alongside “@nomicfoundation/hardhat-toolbox”. As a Celo developer using hardhat, you’ll mostly be running tasks which is a function of the “Hardhat Runner” - a CLI for interacting with Hardhat. Each time you run a Hardhat command from the CLI e.g. “npx hardhat compile”, you’re simply running a task.

Using plugins with Hardhat is one flexibility advantage over Truffle, which simply extends the Hardhat Runtime Environment - HRE . Some of them are in-built, while others are built by the community. Now, let’s examine some of the higher-level libraries rather called plugins, and how to integrate and use them.

  • @nomicfoundation/hardhat-toolbox
  • @nomicfoundation/hardhat-chai-matchers
  • @openzeppelin/hardhat-upgrades
  • @nomiclabs/hardhat-ethers
  • @typechain/hardhat
  • Hardhat-deploy
  • Hardhat-react

Under the hood, these plugins extend Hardhat functionalities by extending the config file. Example:

extendEnvironment((hre) => {
const Web3 = require("ethers");
hre.Web3 = Web3;

hre.web3 = new Web3(hre.network.provider);
});

Hardhat lets you add instances of these tools to its environment object by installing and initializing them once in "hardhatconfig.js". Soon as they feature in the Hardhat global variables, they’ll be available for use across every Hardhat component. Firstly, we will install and configure the plugins we need, then we will apply the usage.

Note: Hardhat components are files/folders within Hardhat context i.e. within the scope of its reach, such as “test” folder.

@nomicfoundation/hardhat-toolbox

A set of coordinated tools developed and recommended by the hardhat officials that bundles the commonly used packages and plugins. When you initialize a hardhat project, “@nomicfoundation/hardhat-toolbox” will be the default umbrella package that houses other tools such as “@nomiclabs/hardhat-etherscan”, “@nomiclabs/hardhat-ethers” etc. Importing the library alongside the pre-installed tools will only make them redundant.

Even though you can do a lot with this plugin, yet you’re limited to the extent of its scope. When you need to make use of other plugins, it is advisable to remove “@nomicfoundation/hardhat-toolbox” from the configuration before proceeding to avoid dependency conflicts.

@nomicfoundation/hardhat-chai-matchers

This is a Hardhat component that blends Ethereum-specific functionalities with the Chai assertion library to make testing smart contracts more easy, readable, and concise. The recent version of Hardhat comes with “@nomicfoundation/hardhat-toolbox” so you do not need to install it. If you like to set things up independently, please continue with this section.

This plugin depends on other libraries or packages to function as intended. You will install it as a peer/dev dependency along with the peer packages if you’re using an older version of Node.

Peer dependencies

  • chai
  • @nomiclabs/hardhat-ethers
  • ethers
npm install --save-dev @nomicfoundation/hardhat-chai-matchers chai @nomiclabs/hardhat-ethers ethers

Or

yarn add --dev @nomicfoundation/hardhat-chai-matchers chai @nomiclabs/hardhat-ethers ethers

Include it in the Hardhat config file:

require("@nomicfoundation/hardhat-chai-matchers")
import "@nomicfoundation/hardhat-chai-matchers";

@openzeppelin/hardhat-upgrades

This tool reduces your tasks when writing upgradeable contracts by modifying Hardhat scripts adding new functions that enable you to deploy and manage proxied contracts, so anytime you release a new version of your dApp, you only need a minimal command to update it, and your users can start interacting with the newer version. If you’re looking for a tutorial on how to write upgradeable contracts, this blogpost might be helpful.

Openzeppelin/hardhat-upgrades depend on etherjs library as peer dependency to function properly. We will install it as a dev dependency. alongside the peer.

npm install --save-dev @nomiclabs/hardhat-ethers ethers

Activate it in "hardhatconfig.js":

require("@nomicfoundation/hardhat-chai-matchers");
require("@openzeppelin/hardhat-upgrades"); /* < ======== */

@nomiclab/hardhat-ethers

This is an official plugin from the Nomic Foundation that wraps the Ethereum library (Etherjs), abstracts the complexities and provides you with a hardhat-configured version for easy interaction with Ethereum-compatible blockchains without creating additional tasks. It depends on the "ether.js" library and works very well with versions from v5.0.0 and above, it adds ethers objects to the Hardhat environment using the same APIs as ether.js .

It adds helpful functionalities to the ethers object which you need to be aware of, such as

  • Libraries interface,
  • FactoryOptions interface,
  • deployContract()
  • getContractFactory(),
  • getSigners() etc.

The return value of each of the helper functions (Contracts and ContractFactory) are by default connected to the first signer returned by the "getSigners()" API.

npm install --save-dev "@nomiclabs/hardhat-ethers" "ethers@^5.0.0"

Include it in the configuration file:

require('@nomicfoundation/hardhat-chai-matchers')
require('@openzeppelin/hardhat-upgrades')
require('@nomiclabs/hardhat-ethers') /* < ======== */

@typechain/hardhat

Of all mentioned plugins, this is the easiest as it adds Typescript support for smart contracts at compile time. It generates Typescript binding you will need in your test and/or frontend if you use Typescript in your project. This might be very helpful if your project features Typescript, but since this tutorial is based on Javascript, we will only jump over it.

npm install --save-dev typechain "@typechain/hardhat" "@typechain/ethers-v5"

Hardhat-deploy

If you need a more elegant deployment method (aside from using the scripts/<somefile> method) that keeps track of all the networks your contracts are deployed to, can also replicate same in your testing environment, “Hardhat-deploy” is the tool you need. In fact, it is one of the plugins I love using. It has interesting features that make your work much easier by adding and modifying existing hardhat tasks. An example task is “deploy”. Before installing hardhat-deploy, if you try to run “npx hardhat deploy”, you get a warning that there is no such task. By adding the tool, the task becomes available.

With this tool, you can write more straightforward tests, assign custom names to addresses to make tracking the address deployed that contract easier, track networks, etc. Here are some of its features, all for a better developer experience as itemized in the documentation:

  • Chain configuration export.
  • It enables deterministic deployments across selected networks.
  • It enhances library linking at the time of deployment.
  • Developers are able to create specific deploy scripts per network.
  • Deployment retrying (by saving pending transactions): and you can feel confident when making deployment since you can always recover them.
  • It lists deployed contracts’ information i.e. contracts’ addresses and abis. This is useful for web applications.
  • It allows you to only deploy what is needed.
  • Support for openzeppelin transparent proxies. You are able to make proxy deployments with the ability to upgrade them transparently, only if code changes.

For more of the features, please refer to the documentation.

The following command installs "hardhat-deploy" together with peer dependencies - "ethers.js" and “hardhat-ethers” and ensures compatibility:

npm install --save-dev  @nomiclabs/hardhat-ethers@npm:hardhat-deploy-ethers ethers

If you use the above command, in your config file, you will need to write require("@nomiclabs/hardhat-ethers") instead of require("hardhat-deploy-ethers")

image

Since we’d already installed “ether” and “nomiclabs/hardhat-ethers”, we only need to install “hardhat-deploy-ethers”.

npm install -D hardhat-deploy

Or

yarn add --dev hardhat-deploy

Then activate in hardhat file:

require("@nomicfoundation/hardhat-chai-matchers");
require('@openzeppelin/hardhat-upgrades');
require("@nomiclabs/hardhat-ethers");
require("hardhat-deploy"); /* < ======== */

Hardhat-react

A hardhat plugin for react application. When run, it generates react hook components from your smart contract that is hot-loaded into your React application. It could also be helpful where your application uses Typescript, as everything is typed and initialized for you. It automatically hooks into “hardhat-deploy” when you run “npx hardhat node --watch“ and you can alway run it manually with “npx hardhat react” .

It uses the following peer plugins to maximize results:

  • hardhat
  • hardhat-deploy
  • hardhat-deploy-ethers
  • hardhat-typechain
  • ts-morph
  • ts-node
  • typescript
  • ts-generator
  • typechain@4.0.0
  • @typechain/ethers-v5

Interestingly, you can install all of the dependencies with just one command.

npm install --save-dev hardhat hardhat-deploy hardhat-deploy-ethers hardhat-typechain hardhat-typechain ts-morph ts-node typescript ts-generator typechain@4.0.0 @typechain/ethers-v5

Yarn

yarn add --dev hardhat hardhat-deploy hardhat-deploy-ethers hardhat-typechain hardhat-typechain ts-morph ts-node typescript ts-generator typechain@4.0.0 @typechain/ethers-v5

Import them to hardhat.

require("@nomicfoundation/hardhat-chai-matchers");
require('@openzeppelin/hardhat-upgrades');
require("@nomiclabs/hardhat-ethers");
require('hardhat-deploy');
require("@nomiclabs/hardhat-ethers");
require("hardhat-deploy-ethers");
require("hardhat-deploy");
require("@symfoni/hardhat-react");
require("hardhat-typechain");
require("@typechain/ethers-v5");

Now that we have installed all the tools we need, let’s discuss how we can use them in our Celo application. At the top of your "hardhatconfig.js" file, you should have the following set of imports and configurations.

require("@nomicfoundation/hardhat-toolbox");
require("@nomicfoundation/hardhat-chai-matchers");
require("@openzeppelin/hardhat-upgrades");
require("@nomiclabs/hardhat-ethers");
require("hardhat-deploy");
require("@nomiclabs/hardhat-ethers");
require("hardhat-deploy-ethers");
require("hardhat-deploy");
require("@symfoni/hardhat-react");
require("hardhat-typechain");
require("@typechain/ethers-v5");
const {config} = require("dotenv");
const { extendEnvironment } = require("hardhat/config");

config();

/** @type import("hardhat/config").HardhatUserConfig */
module.exports = {
networks: {
localhost: {
url: "http://127.0.0.1:8545",
},
alfajores: {
url: "https://alfajores-forno.celo-testnet.org",
accounts: [`${process.env.PRIVATE_KEY}`],
chainId: 44787,
},

celo: {
url: "https://forno.celo.org",
accounts: [`${process.env.PRIVATE_KEY}`],
chainId: 42220,
},

},

solidity: {
version: "0.8.9",
settings: { // See the solidity docs for advice about optimization and evmVersion
optimizer: {
enabled: true,
runs: 200
},
evmVersion: "byzantium"
}
},

};

To make a more advanced deployment and testing, we will leverage some of the tools we have installed.

Modifying "Lock.sol" For this task, we will modify the Lock contract to make it look more advanced with another dependency contract, and use “hardhat-deploy” to deploy the contracts to the “alfajores” network and save the deployment information anywhere we want for later use which is not available to us in the previous deployment method. Secondly, we want the deployment to run only if changes are made to the contracts. In the contract folder, create a new “.sol” file and name it “Beneficiary.sol”. Make a new folder as “interface”. Add a new file named “Beneficiary.sol”. Rename “Lock.sol” to “Bank.sol”, then paste the following code to their respective destination.

Bank.sol This is a simple time-lock contract that allows only approved child address to withdraw from the bank. The Bank makes an external gasless call to the “Beneficiaries” contract to verify if the caller is an approved beneficiary. The former depends on the latter.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;

import "./interfaces/IBeneficiary.sol";

contract Bank {
address public immutable beneficiary;
event Withdrawal(uint amount, address indexed who);

constructor(address _beneficiary) payable {
require(
_beneficiary != address(0),
"Beneficiary is zero address"
);
beneficiary = _beneficiary;
}

function withdraw() external {
require(IBeneficiary(beneficiary).getApproval(msg.sender), "You're not a beneficiary");

emit Withdrawal(address(this).balance, msg.sender);

payable(msg.sender).transfer(address(this).balance);
}

function getBalance(address who) external view returns(uint256) {
return address(who).balance;
}

}

Beneficiary.sol An independent contract that returns whether the queried address is a beneficiary or not. Owner is able to approve or disapprove the beneficiary.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;

contract Beneficiaries {
address payable immutable owner;

address payable immutable child_1;
address payable immutable child_2;

mapping (address => bool) public approval;

constructor (address _child_1, address _child_2) {
require(_child_1 != address(0) && _child_2 != address(0), "Children addresses are empty");
child_1 = _child_2;
child_2 = _child_2;
}

function getApproval(address child) external view returns(bool) {
return approval[child];
}

function approve(address childToApprove, bool _approval) public {
require(msg.sender == owner, "Caller not owner");
require(childToApprove == child_1 || childToApprove == child_2, "Child not recognized");
approval[child] = _approval;
}

}

IBeneficiary.sol path: "interface/IBeneficiary.sol"

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

interface IBeneficiary {
function getApproval(address) external view returns(bool);
}

Note: The above contracts are for testing purposes and may contain potential bugs. Do not use in production.

In the project directory, make a new folder called "deploy", navigate into it and create a new file "00_deploy.js". You could as well name it whatever you want as long as it is ".js" extended and inside the "deploy" folder, Hardhat-deploy will find it. Inside this file, we will write a script that executes the task we want.

mkdir deploy
cd deploy
touch 00_deploy.js

Output => “deploy/00_deploy.js”

image

Run compile: Before you compile, comment out these dependencies.

// require("@nomicfoundation/hardhat-toolbox");
// require("hardhat-typechain");

With “require("@nomicfoundation/hardhat-toolbox")” active you may get “unresolved dependency error” and may be required to install the following dependencies:

npm install --save-dev "@nomicfoundation/hardhat-network-helpers@^1.0.0" "@nomicfoundation/hardhat-chai-matchers@^1.0.0" "@nomiclabs/hardhat-ethers@^2.0.0" "@nomiclabs/hardhat-etherscan@^3.0.0" "@types/chai@^4.2.0" "@types/mocha@^9.1.0" "@typechain/ethers-v5@^10.1.0" "@typechain/hardhat@^6.1.2" "chai@^4.2.0" "hardhat-gas-reporter@^1.0.8" "solidity-coverage@^0.8.1" "ts-node@>=8.0.0" "typechain@^8.1.0"

If you get a prompt in the terminal to install the above dependencies, install them using Yarn. With Yarn, you do not have to worry about incompatible dependencies. Yarn’s installation is smooth.

yarn add --dev "@nomicfoundation/hardhat-network-helpers@^1.0.0" "@nomicfoundation/hardhat-chai-matchers@^1.0.0" "@nomiclabs/hardhat-ethers@^2.0.0" "@nomiclabs/hardhat-etherscan@^3.0.0" "@types/chai@^4.2.0" "@types/mocha@^9.1.0" "@typechain/ethers-v5@^10.1.0" "@typechain/hardhat@^6.1.2" "chai@^4.2.0" "hardhat-gas-reporter@^1.0.8" "solidity-coverage@^0.8.1" "ts-node@>=8.0.0" "typechain@^8.1.0"

Also, you get the following error if you try to compile while “@nomicfoundation/hardhat-toolbox” is imported alongside previous dependencies. This is because we had manually installed some of the dependencies which also exist in the toolbox. Most times you get typechain error.

image

There is more than one copy of the Typechain plugin all competing for usage hence the naming conflict. To resolve this issue, we simply remove “@nomicfoundation/hardhat-toolbox” from the hardhatconfig.js so we can have only the manually installed dependencies.

You will also get an error like this if “hardhat-typechain” is present because it works plainly with Typescript. Since we are not using Typescript we simply remove it.

image

Now we can compile

npx hardhat compile

image

Deploy to Alfajores:

Remember we had earlier created a file inside the deploy folder - “00_deploy.js”. Copy/paste this code'

module.exports = async ({ getNamedAccounts, deployments}) => {
const {deploy} = deployments;
const {deployer, child_1, child_2} = await getNamedAccounts();

console.log(child_1, child_2)

const beneficiaries = await deploy('Beneficiaries', {
from: deployer,
gasLimit: 4000000,
args: [child_1, child_2],
});

const bank = await deploy('Bank', {
from: deployer,
gasLimit: 4000000,
value: "1000000000000000000",
args: [beneficiaries.address],
});

console.log("Beneficiaries address:", beneficiaries.address)
console.log("Bank address", bank.address,)
console.log("Deployer", deployer)
console.log("Child_1 ", child_1)
console.log("Child_2 ", child_2)

};

module.exports.tags = ['Beneficiaries', 'Bank'];

The “00_deploy.js” is a module that accepts two parameters from the hardhat environment (hardhatconfig) when invoked:

deployments (line 2): This function returns a couple of utility functions, but we only need the "deploy()" so we extract by destructuring.

getNamedAccount : A function that returns predefined named accounts from the “hardhatconfig.js”. For our contract, we are required to supply two arguments (a)child_1 (b) child_2 and a deployer address. By default, hardhat generates 10 default accounts with signers that enable us to make transactions without providing private keys or mnemonic phrases. We extracted the first 3 signers by setting the key/value pair in the “hardhatconfig.js” as below.

image

Line 9: "deploy()" function accepts two arguments - Contract name and an optional argument of type object where we specified “from”, “gaslimit” and “args”. The function resolves to an object from which we extracted the address of the deployed contracts. What we just did is a local deployment. Now let’s deploy to a live network, i.e Alfajores (Celo’s testnet). We will need to make some changes in the config file under the "namedAcccounts" object so we can perform deployment either to “Hardhat” or “Testnet”. However, we will need accounts with signing capability to deploy to Alfajores since the default 10 accounts are meant to be used locally, and an account to sign transaction during testing. To achieve this, one thing we could do is to make a reference to the “env” file. Perhaps we need accounts with private key access. Since public keys are often derived from private keys, we can set the private key in the secret file i.e “.env”, then import it to the point it is needed. We will as well keep the rest two accounts we need as parameters in the same “.env” file. Add "CHILD_1" and "CHILD_2" private keys.

image

In the “hardhatconfig.js”, modify the “namedAccount” object as shown below. Pay attention to how I configured each of the accounts. The “default” key points to accounts generated by Hardhat. To set accounts for Alfajores, we must use the network’s chainId as the key otherwise we get errors. The same method applies if you are deploying to the Celo mainnet.

Notice I prefixed the value for the deployer under “44787” with “privatekey://”. This is to enable Hardhat to know that we are using a full signer account since the deployer will need to sign transactions while deploying to the live network. Lastly, we loaded the values programmatically using “process.env” .

image

We can then run:

npx hardhat deploy --network alfajores

image

You should see the above messages printed to the console. If you get any error, please go over it again to ensure you follow all the steps as itemized. If you perhaps would need deployment information, paste the following in the “hardhatconfig.js” and make a folder called “deployments”. It should be in the project’s root.

paths: {
deploy: "deploy",
deployments: "deployments",
imports: "imports"
},

When the deployment is done, “hardhat-deploy” generates the following outputs.

image

If you check the content of the .chainId file, you’ll see 44787 which represents the network we just deployed to i.e Alfajores. The code in your “hardhatconfig.js” file should look like this:

require("@nomicfoundation/hardhat-chai-matchers");
require("@openzeppelin/hardhat-upgrades");
require("@nomiclabs/hardhat-ethers");
require("hardhat-deploy");
require("@nomiclabs/hardhat-ethers");
require("hardhat-deploy-ethers");
require("hardhat-deploy");
require("@typechain/ethers-v5");
const {config} = require("dotenv");

config();

/** @type import("hardhat/config").HardhatUserConfig */
module.exports = {

networks: {
localhost: {
url: "http://127.0.0.1:8545",
live: false,
saveDeployments: true,
tags: ["local"]
},

hardhat: {
live: false,
saveDeployments: true,
tags: ["test", "local"]
},

alfajores: {
url: "https://alfajores-forno.celo-testnet.org",
accounts: [`${process.env.PRIVATE_KEY}`],
chainId: 44787,
},

celo: {
url: "https://forno.celo.org",
accounts: [`${process.env.PRIVATE_KEY}`],
chainId: 42220,
},

},

solidity: {
version: "0.8.9",
settings: { // See the solidity docs for advice about optimization and evmVersion
optimizer: {
enabled: true,
runs: 200
},
}
},

paths: {
deploy: 'deploy',
deployments: 'deployments',
imports: 'imports'
},

namedAccounts: {
deployer: {
default: 0,
44787: `privatekey://${process.env.PRIVATE_KEY}`,
},

child_1: {
default: 1,
44787: process.env.CHILD_1,
},

child_2: {
default: 2,
44787 : process.env.CHILD_2,
}
}
};

Testing

Testing smart contracts enables us to be sure our solidity code is working as intended. We have 2 contracts to test: “Beneficiaries.sol” and ”Bank.sol”. Remove the existing “Lock.js” test file. Make a new file under the test folder and name it ”Beneficiaries.js”. We will write a short, concise, simple unit test using the script we wrote in “deploy/00_deploy.js”. Since we are testing locally, we will require deployments to be done locally. Here, we are leveraging “hardhat-deploy” and “hardhat-deploy-ethers”.

In the new test file, paste the following code:

const { expect } = require("chai");
const { BigNumber } = require("bignumber.js");
const {ethers, getNamedAccounts} = require("hardhat");


const toBN = (x) => {
return new BigNumber(x);
}

describe('Beneficiaries', () => {

it("Test1: Should confirm child_1 status as false", async function () {
await deployments.fixture(["Beneficiaries"]);
const { deployer, child_1 } = await getNamedAccounts();
const Instance = await ethers.getContract("Beneficiaries", deployer);

// Note: For approval, it used default account i.e account[0] which is also the deployer
await Instance.approval(child_1).then((tx) =>
expect(tx).to.equal(false)
);
});

it("Test2: Should approve child_2", async function () {
await deployments.fixture(["Beneficiaries"]);
const { deployer, child_2 } = await getNamedAccounts();
const Instance = await ethers.getContract("Beneficiaries", deployer);

// At this point, child_2 is not approved
await Instance.approval(child_2).then((tx) =>
expect(tx).to.equal(false)
);

// Here the owner approve child_2
await Instance.approve(child_2, true);

// And we can verify if child_2 is truly approved.
await Instance.approval(child_2).then((tx) =>
expect(tx).to.equal(true)
);
});

it("Test3: Should withdraw successfully", async function () {
await deployments.fixture(["Beneficiaries", "Bank"]);
const { deployer, child_1, child_2 } = await getNamedAccounts();
const Beneficiaries = await ethers.getContract("Beneficiaries", deployer, child_1, child_2);
const Bank = await ethers.getContract("Bank", deployer);
const initialBalance = await Bank.getBalance(child_2);
const bankBalance = await Bank.getBalance(Bank.address);

expect(initialBalance).to.be.gt(BigNumber(0));

// Here the owner approve child_2
await Beneficiaries.approve(child_2, true);
const signer = await ethers.getSigner(child_2);
await Bank.connect(signer).withdraw();

expect(await Bank.getBalance(child_2)).to.be.gt(bankBalance);
expect(await Bank.getBalance(Bank.address)).to.equal(BigNumber(0));

});
});

The above set of tests cover for “Beneficiaries.sol” and “Bank.sol” contracts. Let’s examine the code so you can better understand the structure. We are using the deployments information from the “00_deploy.js” when we call “await deployments.fixtures([‘Beneficiaries’])”, the script is run and it returns deployment information for which of the valid tags we specify. In this case ‘Beneficiaries’. We also have access to the "namedAccount" we used earlier by destructuring the returned values.

const { deployer, child_1, child_2 } = await getNamedAccounts();

Thereafter, we get a single contract object by calling the “getContract” method provided by the plugin.

const Instance = await ethers.getContract("Beneficiaries", deployer);

Notice how we generate a signer for the “child_2” account?

const signer = await ethers.getSigner(child_2);

Since we had set names to reference different accounts in the “hardhatconfig.js” by setting default keys, we get the same set of accounts while “npx hardhat test” is invoked The default private key/paired accounts generated by Hardhat are not compatible with “signers” of “ethers” library. So we will encounter an error while trying to connect “child_2” to sign the transaction in “Test 3”. To fix this, we’ll use the “ethers” library since we want to make the “Child_2” address compatible with ethers’ signers. “ethers” exposes a method called “getSigner()” that accepts optional string arguments and creates a signing capacity for it. This is exactly what we did to make child_2 ethers-compatible-signer.

const signer = await ethers.getSigner(child_2);
await Bank.connect(signer).withdraw();

We can now run the test. npx hardhat test

image

Boom! We made it and our test passed as expected. The complete code for this tutorial can be found here.

_Deployments folder

If you expand the deployment folder, you should have the following structure generated by the plugin.

image

Conclusion​

Congratulations on completing this tutorial. What we have learned so far:

  • Hardhat plugins
    • What they are.
    • How they work under the hood.
    • How we can manipulate them to achieve desired results.
  • Advanced hardhat configuration using plugins with examples.
  • Lastly, we learn how to harmonize deployment to write clear and concise tests.

What next?​

You can go over this tutorial as many times as you can to fully understand all of the concepts we have discussed so far. Practice makes perfect. Make some time to try out new things on your own and you will be able to deploy your own dApp on Celo. If you’re looking for related tutorials, I recommend to browse through the Celo blog;

About the Author​

Isaac Jesse , aka Bobelr is a smart contract/Web3 developer. He has been in the field since 2018, worked as an ambassador with several projects like Algorand and so on as content producer. He has also contributed to Web3 projects as a developer.

References​

Go back

· 12 min read
Isaac Jesse

header

Introduction

Since the bubbling part of the Web3-learning stream features more about tooling than writing code, many aspiring web3 developers often are exposed to boilerplate code where most of the things happening are abstracted. Picking the right choice of tools and knowing how to use them effectively will give you a better experience as a web3 developer and increase your efficiency. This is one of the goals of Celo as a platform.

Prerequisites

This tutorial exposes you to Hardhat (basic setup and configuration) as one of the many tools you need to successfully deploy smart programs on Celo. You need to be familiar with the following:

  • Prior knowledge of Javascript programming language.
  • Familiar with using the command line.
  • Foundational programming know-how.

If you need some headstarts, there are a couple of tutorials on Javascript documentation that prepare your way.

Requirements

To get started, you will need to have the following technologies installed to make the journey smooth:

  • Code Editor. To follow along with me, I recommend that you install Visual Studio Code.
  • Nodejs version 12.0.1 or later . With Nodejs, you have access to Node Package Manager - NPM or Yarn .
  • Download and install Git bash, and a tutorial to help you get started.

Please note that this tutorial will not cover smart contracts or frontend code.

Basic Hardhat Configuration

What is Hardhat?

As defined on the official website, Hardhat is a development environment for Ethereum software. It consists of different components for editing, compiling, debugging and deploying your smart contracts and dApps, all of which work together to create a complete development environment.

Basically, Hardhat was built to target blockchain platforms that are compatible with Ethereum Virtual Machine - EVM, of which Celo happens to be one. Its flexibility of use and extendability makes it one of a kind. Hardhat potentially allows the installation of plugins, and it can even be configured to use other competitive tools, such as the Truffle framework. Let’s do a basic Hardhat installation and set up; to automate connection to Celo networks. As a reminder, you should have the aforementioned prerequisites set up before attempting this section.

Open up a git bash terminal and the system terminal side by side. We don’t need two command lines, but I want to show you a common challenge most web3 developers often encounter and a simple hack that saves you the headache.

image

Make a new directory in the "git bash" terminal for this tutorial. And name it celo-hardhat-example .

Run:

mkdir celo-hardhat-example && cd celo-hardhat-example

These simple commands create a new project folder with the name tag “celo-hardhat-example” and navigate into it. As one of the git bash features, it allows nesting commands by using the && operator.

image

Still in the project directory, run: "yarn add hardhat" or "npm install hardhat", whichever package manager you prefer. Be sure to have an internet connection as the command downloads hardhat set-up scripts in the project’s root directory. You may experience delays with Git bash as sometimes it takes longer to install hardhat successfully. If this is the case, switch to using the system’s command prompt or that of VSCode but split the commands instead by removing the “&&” operator. After that, run:

npm install --save-dev hardhat

Here, we are installing the Hardhat setup script as a dependency. When the installation is complete, we will create an instance of a hardhat boilerplate project. To do this, run:

npx hardhat

This command initializes a new instance of the Hardhat development environment. Note: If you run 'npx hardhat' using Bash CLI, you get a weird error like this.

image

This is a known compatibility issue with Git bash to create an instance of hardhat. To solve it, simply navigate to the project’s folder in a new terminal other than Git bash (one you opened earlier), run the same command in the command prompt or VSCode terminal, and “Viola” works fine.

image

A warm display showing installation success and a dialogue section will pop up in the terminal asking for your preferences. Select Javascript as the preferred language (for this tutorial) by clicking “Enter” on your keyboard. Next, you will be asked to install nomiclabs tools. There are a couple of dependencies/packages we need for our project. They’re compressed in “@nomicfoundation/hardhat-toolbox” so you do not have to download them separately such as “@nomiclabs/hardhat-waffle”, “@etherproject” plus a few others. Watch out for it in the terminal. Copy-paste it back to the terminal and run it. In a few minutes, the installation should be complete. Open the boilerplate code from the current CLI. Run "code ." (i.e code space dot).

npm install --save-dev "hardhat@^2.12.2" "@nomicfoundation/hardhat-toolbox@^2.0.0"

If all goes well, Your project folder/file structure should look like this:

  • Celo-hardhat-example (Project/root directory)
  • contracts (folder)
  • scripts (folder)
  • test (folder)
  • .gitIgnore (file)
  • hardhat.config.js (file)
  • package.json (file)
  • README.md (file)

Let’s examine the structure and how we can configure it to make the best use of them.

contracts:

This folder contains smart contract files written in solidity. When you invoke hardhat to compile your contracts i.e, "npx hardhat compile", The instruction is passed to the default solidity compiler - “solc” to execute compilation instruction on every file with ".sol" extension inside the contacts folder. In other words, contracts are an array of solidity files. You can override the compiler version by explicitly defining the version of "solc" that will be used to compile all of the solidity files by explicitly speecifying a preferred list of "solc" versions in the "hardhatconfig.js". Don't worry much about this. I will show you as we progress.

As a good practice, you could separate concerns among the different ".sol" files by filtering them into groups.

Example: Under the contract folder, you could have:

  • contracts
  • interfaces
  • libraries
  • utilities

Let’s take a look at the example contract (Lock.sol) under the contracts folder. The contract is written for example purposes. So you should not try to deploy to production. The first thing we want to do is compile the contract.

npx hardhat compile

An artifact folder is created that contains the compile contracts from which we can fetch the contract’s JSONInterface i.e ABI and other information.

scripts:

This folder contains Javascript files used for special tasks such as deployments. “scripts/deploy.js” is a deployment file that fetches and deploy compiled contracts from the artifacts that were generated when npx hardhat compile was run. To actively run any ".js" file under the scripts, for example, “deploy.js”, use the command in the format:

npx hardhat run scripts/deploy.js

If you have files other than "deploy.js" and want to specifically point to it, you only need to substitute “deploy.js” for it.

Let’s try to deploy our Lock.sol contract to the Celo testnet. We will use the same command except for additional arguments to show we want to deploy to a specific network.

npx hardhat run scripts/deploy.js –network alfajores

The above command starts by invoking node package manager - npm, which goes into the node_modules (a folder containing all of the dependencies we need to run the program successfully), looks for hardhat, checks for “run” in its command list, invoke it on “deploy.js” file and instruct hardhat to deploy the outcome of the file to the selected network “alfajores” which is Celo testnet.

image

Note: Hardhat will always perform a new compilation instruction if changes to previously compiled contract (s) exist. This is done using the cache file that will be generated soon as hardhat command is invoked. To deploy to Celo mainnet, we point the command to Celo as in:

npx hardhat run scripts/deploy.js –network celo

Alternatively, you may need to first deploy your contracts locally before sending it to a live network. This enables you to test extensively, debug and catch potential errors ahead of time. Hardhat provides you with two hardhat-based network options:

An In-memory instance of Hardhat Network created by default on start-up. When you run “npx hardhat deploy” without using the “--network” flag, the in-memory network is used.

The second option is a standalone hardhat network, that allows external clients, such as web3 wallets e.g. Metamask, to connect to it. To use this option, you have to run it as a node.

npx hardhat node

image

It uses a JSON RPC Websocket server that runs on port http://127.0.0.1:8545. A set of pre-funded accounts is generated, and you can connect external clients like Metamask. In the following tutorial, we will learn how to do that. To deploy to the server, run:

npx hardhat run scripts/deploy.js --network localhost

image

test:

As the name implies, the test holds all of the test files we have written for our smart programs. To run test files, we invoke the command:

npx hardhat test

To run a specific test file, use: npx hardhat test/somefileName.js where ‘somefileName’ is the name of the target file.

hardhat.config.js:

This is the heart of hardhat which connect and serve every other part. From smart contract compilation to deployment to testing, Hardhat manages the processes by injecting some variables globally via the hardhat.config.js. It allows us to define however we want to interact with our DApp. Let’s set up our configuration to use either Celo testnet or mainnet each time we run designated commands.

The network parameters we will use are defined in Celo’s documentation.

Hardhat config file is a module that exports a JSON object with which hardhat exposes an object called networks - a JSON object that contains network information. Since we could connect to Celo’s testnet or mainnet, let’s populate the network field. Copy the following and replace the module.exports:

  /** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
solidity: "0.8.17",
networks: {
localhost: {
url: "http://127.0.0.1:8545",
accounts: {
mnemonic: DEVCHAIN_MNEMONIC,
},
},
alfajores: {
url: "https://alfajores-forno.celo-testnet.org",
accounts: [`${process.env.PRIVATE_KEY}`],
chainId: 44787,
},
celo: {
url: "https://forno.celo.org",
accounts: [`${process.env.PRIVATE_KEY}`],
chainId: 42220,
},
solidity: {
version: "0.8.9",
settings: {
optimizer: {
enabled: true,
runs: 200
},
}
},

From the image, we defined just after the network parameter a configuration for the solidity compiler. Let’s understand what this means.

  • version: Defines the compiler version that should be used for compiling solidity files.

  • settings/optimizer/enabled: If enabled, it optimizes our contract by pruning unnecessary and unused code to reduce contract size. If the contract size exceeds a certain range, their deployment may be rejected by the target network.

.gitignore:

Some folders are too big to commit to the repository. An example of such is the “node_modules” that houses all of our dependencies. Another example is the .env that holds sentitive information such as the environment variables we do not wish to commit to source control.

package.json:

This serves as the heart of most projects that use node js. It records the functional attributes of a project that npm uses to install dependencies, run scripts, and identify the entry point of our application.

README.md:

Use this file to describe your project : how to run and test it.

Common Mistakes Or Errors You Might likely Encounter

Invalid account Error

image

Most errors like this are triggered as a result of one or more issues in the “hardhat.config.js”. The message complains of an invalid account because hardhat could not find any private key from which to generate an account and send a transaction. If you have defined something like “process.env.PRIVATE_KEY” under the “account” field in the network object, you will need to provide where to find this variable. Such variables are sensitive and should be kept private as much as possible. Often, we use a .env file to manage it. Create a .env file in the project directory, and add the private key

Solution

This should keep the error away.

image

Invalid value undefined for HardhatConfig.networks.solidity.url

image

This error is prompted when you add a solidity object as part of network field object. The terminating curly brace for the networks was after the solidity object. From the image below, the terminating curly brace was omitted and hardhat sees “solidity” as part of the network hence it expects that you include a URL which is an expected format for network objects.

image

Solution

Simply terminate the network object before defining a solidity object. Add closing curly brace between the network object and the solidity object.

Connection timeout

When you are not connected to the network or your network is bad, you get errors like below. All you need is ensure you are connected to the internet when trying to deploy to the testnet or mainnet.

image

Conclusion

We have learned:

  • How to start a new hardhat project.
  • How each of the files and folders work, their uses and how to configure them to match our needs.
  • We have also gained an understanding of how things work under the hood.
  • And lastly, we learned about few errors or mistakes that might likely arise during development.

What next?

I hope that you will try this on your own. Create a new project. Try it several times and you will get used to it. As soon as you understand how things work, you will increase in speed and knowledge of web3 development.

This is foundational knowledge you need to work with hardhat as a Celo developer. In my next tutorial, I will show you more advanced interesting stuff you can do while developing DApps on Celo.

Isaac Jesse , aka Bobelr is a smart contract/Web3 developer. He has been in the field since 2018, worked as an ambassador with several projects like Algorand and so on as content producer. He has also contributed to Web3 projects as a developer.

References

Go back

· 31 min read

header

Introduction

In this tutorial, we will be building a frontend for an NFT Auction dApp that runs on the Celo blockchain using Angular.

How the dApp works

Our dApp will allow NFT owners to create Auctions for NFTs they own, and bidders can make bids on the NFTs. Successful bidders will get the NFTs they bidded for transferred to them, and the sellers will get the CELO paid by the bidders sent to their wallets.

  • During Auction
    • The seller of NFT deploys this contract.
    • Auction lasts for seven days.
    • Participants can bid by depositing ETH greater than the current highest bidder.
    • All bidders can withdraw their bid if it is not the highest bid.
  • After the auction
    1. The highest bidder becomes the new owner of NFT.
    2. The seller receives the highest bid of ETH.

You can see a sample of what we will be building here.

image

Prerequisites

This tutorial assumes that you are already familiar with solidity and understand how smart contracts work. It will be assumed that you already know how to deploy smart contracts to a network using your favorite web3 tooling (Hardhat, Truffle, etc). Celo is an Ethereum Virtual Machine (EVM) compatible chain, so all the languages and workflows you learned while building for any EVM chain apply to Celo also.

I also assume that you already know the basics of using Angular, i.e., you can bootstrap an application in Angular and understand routing, services, binding, etc.

You will also need the following

  • Solidity
  • Hardhat/Truffle
  • Node
  • Typescript
  • Angular Basics

Getting Started

I assume that anyone going through this tutorial already understands and use Angular, so I will skip the setup involved in getting Angular to work on your development computer. That means I assume you already have Node and Angular setup on your PC.

*If you are entirely new to Angular, here is a good tutorial you can learn from.

To bootstrap our Angular dApp, we will be using Celo Composer.

Celo Composer is a set of tools and starter templates that makes it easy for you to start your Celo-based web3 projects.

You can check out Celo Composer here https://github.com/celo-org/celo-composer/tree/main/packages/angular-app.

To start our Angular App Project, we will use the Celo Composer CLI; the CLI makes it easy for us to select the options that we want when bootstrapping our dApp.

  • Run this on your terminal
    npx @celo/celo-composer create
  • Choose Angular when asked for the framework

  • Choose hardhat (Only Hardhat is available at the time of writing this tutorial)

  • Skip subgraph, we won't use it in this tutorial.

Your Project will now be created; you can check to make sure it has the following folders

  • packages/hardhat - Your Hardhat Folder - Where you can keep your Contracts
  • packages/angular-app - Your Angular project

Setup the Smart Contract

  • Open your Hardhat project folder (packages/hardhat)

  • Copy the .envexample to a new file called .env. This is where the private key you use to deploy your contracts will be saved.

  • Fill in the private key with your Celo wallet private key. You might want to get some Alfajores (Testnet) coins from the Celo Faucet

  • Install the needed packages

  npm i
  • Open your Contracts folder (packages/hardhat/contracts)

  • Add a new contract in the folder called NFTAuctionManager.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "./NFTAuction.sol";

contract NFTAuctionManager {

    event AuctionCreated(address indexed creator, uint indexed auctionId, address indexed auctionAddress);


    uint public auctionCount;



    mapping(uint => address) public auctions;

    constructor() {

    }

    function createAuction(address _nft,
        uint _nftId,
        uint _startingBid) external {

        auctionCount++;

        address n = address(new NFTAuction(_nft,_nftId,_startingBid,msg.sender, auctionCount) );

        auctions[auctionCount]=n;

        emit AuctionCreated(msg.sender, auctionCount, n);
    }



    function getAuctionAddress(uint id) public view returns (address)
{
        return auctions[id];
    }


}

The NFTAuctionManager Contract has two functions

CreateAuction: allows you to create a new Auction; it stores the created Auction in the auctions mapping

GetAuctionAddress: an helper method to access existing auctions through their index

  • Add another Contract called NFTAuction.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import "@openzeppelin/contracts/token/ERC721/IERC721.sol";

contract NFTAuction {
    event Started();
    event Bidded(address indexed sender, uint amount);
    event Withdrew(address indexed bidder, uint amount);
    event Ended(address winner, uint amount);

    IERC721 public nft;
    uint public nftId;

    address payable public seller;
    uint public endAt;
    bool public started;
    bool public ended;

    address public highestBidder;
    uint public highestBid;
    mapping(address => uint) public bids;

    uint public auctionId;

    constructor(
        address _nft,
        uint _nftId,
        uint _startingBid,
        address _seller,
        uint _auctionId
    ) {
        nft = IERC721(_nft);
        nftId = _nftId;

        seller = payable(_seller); // payable(msg.sender);
        highestBid = _startingBid;
        auctionId=_auctionId;
    }

    function start() external {
        require(!started, "started");
        require(msg.sender == seller, "not seller");

        nft.transferFrom(msg.sender, address(this), nftId);
        started = true;
        endAt = block.timestamp + 7 days;

        emit Started();
    }

    function bid() external payable {
        require(started, "not started");
        require(block.timestamp < endAt, "ended");
        require(msg.value > highestBid, "value < highest");


        highestBidder = msg.sender;
        highestBid = msg.value;

        bids[highestBidder] = highestBid;

        emit Bidded(msg.sender, msg.value);
    }

    function withdraw() external {
        uint bal = bids[msg.sender];
        bids[msg.sender] = 0;
        payable(msg.sender).transfer(bal);

        emit Withdrew(msg.sender, bal);
    }

    function end() external {
        require(started, "not started");
        require(block.timestamp >= endAt, "not ended");
        require(!ended, "ended");

        ended = true;
        if (highestBidder != address(0)) {
            nft.safeTransferFrom(address(this), highestBidder, nftId);
            seller.transfer(highestBid);
        } else {
            nft.safeTransferFrom(address(this), seller, nftId);
        }

        emit Ended(highestBidder, highestBid);
    }

    function details() public view returns (address _nft,uint _nftId,address _seller,uint _endAt, bool _started,bool _ended, address _highestBidder,uint _highestBid){
        return (address(nft), nftId,seller,endAt, started,ended,highestBidder,highestBid );

    }
}

The NFTAuction contract implements the Auction Sale. It has the following functions

Start: The start function allows you to transfer the NFT you want to sell to this contract and begin the sale. Only the seller can call this function.

Bid: Buyers can submit bids for the NFT through the bid function. Bids are only accepted if they are higher than the previous highest bid. The amount bidded is transferred to the contract when this function is called.

Withdraw: Bidders can withdraw their Celo in the event their bids were not successful.

End: This function ends the auction sale once the auction deadline has been reached. If the auction was successful, the NFT will be transferred to the highest bidder and the Celos transferred to the seller.

These are the contracts we will be interacting with via our Angular dApp Frontend.

Deploy Your Smart contract

Your hardhat project was set up with the hardhat-deploy plugin which makes deployments very easy.

To deploy, go to the deploy folder, open the 00-deploy.js file, and you will see an example deployment function for the existing Greeter contract.

Copy the 00-deploy.js file and paste it to a new file called 01-deploy-NFTAuction.js.

Your hardhat-deploy plugin deploys your contracts serially using the naming of the file. So, when you run the deploy command, it will run the 00-deploy file first, then run the 01-deploy-nftauction.js file next.

Now open the 01-deploy-NFTAuction.js file.

Update the code to deploy the NFTAuctionManager Contract.

Your code should look like this


await deploy("NFTAuctionManager", {
from: deployer,
args: [],
log: true,
})

module.exports.tags = ["NFTAuctionManager"];

Deploy the Contracts by running the following commands on any terminal (make sure you are in the packages/hardhat directory)

npx hardhat deploy –network alfajores

If all is well, you should see a message from hardhat with the transaction hash of your Contract deployment and the address of your new Contract

You can now view your contract on the CELO explorer (Alfajores) using the address.

Now that we have deployed our contract let's build the dApp.

Before we go on, Our dApp will be running in the browser, so you will need a wallet to test within the browser. If you have not yet installed Metamask, this will be a good point to do that.

Also, once you have Metamask installed, ensure you add the CELO Networks - Mainnet and Alfajores ( TestNet ) - to it. If you are unsure how to go about this, You can use this tutorial ( https://developers.celo.org/3-simple-steps-to-connect-your-metamask-wallet-to-celo-732d4a139587 ) as a guide.

DAPP Frontend

Switch to the angular project in your terminal

cd packages/angular-app

Now install the needed packages

npm i

Once the packages install, let's run the code to see how it looks first with the base Celo composer code

image

Click on the connect button, and you will see how it works for now.

You can now connect to a crypto wallet, and the response from your connection is also displayed back to you. Now, let's go through the code that produced that.

Open src/app/core/web3.ts

The first thing we need to do is figure out a way to get access to our Web3 provider.

This Web3 provider allows your application to communicate with an Ethereum Node. Providers take JSON-RPC requests and return the response.

The /core/web3 file contains an injection token that allows you to get access to the web provider.


import { InjectionToken } from '@angular/core';
import Web3 from 'web3';

export const WEB3 = new InjectionToken<Web3>('web3', {
providedIn: 'root',
factory: () => new Web3(Web3.givenProvider)
});

Now that we know how to get access to the web3 provider, Most of your interactions with the blockchain are going to be happening through the web3Service so let's go through that also.

Open the packages\angular-app\src\app\services\contract\web3.services.ts file

Now let's go through the code

First, we import the libraries we will need. There are three important libraries to take note of.

  1. WalletConnect: This is an open-source protocol that helps implement established connections between various crypto wallets anddecentralized finance (DeFi) DAPPs. The protocol establishes a remote, encrypted connection between the wallets and apps. Simply put, WalletConnect forms a bridge connecting any mobile wallet to any decentralized web application. With WalletConnect, users can connect over 170WalletConnect-compatible wallets, such asMetaMask, Valora, andTrust Wallet, and several DAPPs

  2. Web3 Modal: This is an easy-to-use library to help developers add support for multiple providers in their apps with a simple, customizable configuration. With Web3modal, you can easily support injected providers like (Metamask, Brave Wallet, Dapper, Frame, Gnosis Safe, Tally, Web3 Browsers, etc) and WalletConnect.

  3. Web3 Js: This is a collection of JS libraries that lets you interact with an Ethereum node remotely or locally. CELO Composer comes preinstalled with web3 js.

Now, let's have a look at some of the class variables

All the Web3 Wallet accounts our DAPP is connected to are saved in the accounts array.

In Web3, a DAPP might be connected to different wallets at the same time; requesting for all connected accounts will always return an array. Most times, though, most DAPPS simply use the first member of the array as the active account.

The provider variable will store a reference to our web3 provider as provided by the Wallet we are connected to.

balance shows the current Native Coin (CELO) Balance of our connected wallet.

In lines 27 - 53 (web3.services.ts), we configure our options for web3modal.

The walletconnect option allows us to set up the wallet that we would like to support in our dApp. You can connect through different types of connections

  • Injected Wallets:- e.g, Metamask - supports wallets that injects an Ethereum Provider into the browser or window
  • Mobile Wallets: - e.g Valora - Wallet connect allows users to connect via scanning QR codes that deep links to the apps.
  • Remote Nodes - e.g, Infura, Alchemy, etc - These -allow you to "plug in" to the blockchain via nodes managed by some trusted third party. Wallet Connect allows you to specify an InfuraId in the options. If you don't have one, go to https://www.infura.io/ now to create an account

Let's look at the next lines


async connectAccount() {
this.provider = await this.web3Modal.connect(); // set provider
if (this.provider) {
this.web3js = new Web3(this.provider);
} // create web3 instance
this.accounts = await this.web3js.eth.getAccounts();
return this.accounts;
}

In connectAccount, we call the Web3modal's connect method to display our wallet connection dialog. This will display a dialog with all the options of wallets we configured earlier in WalletConnect.

After you select a wallet and get connected to an account, web3modal returns a provider. We will need this provider to create our web3js instance. Once we have our web3js instance, we can request our account.

Lastly, in the web3.service, the accountInfo method allows you to get the balance of the currently connected account.

Ok, let's change the web3service a little to make it easier to use with type checking

Replace the content with this

import {Inject, Injectable} from '@angular/core';
import { WEB3 } from '../../core/web3';
import { Subject } from 'rxjs';
import Web3 from 'web3';
import Web3Modal from "web3modal";
import WalletConnectProvider from "@walletconnect/web3-provider";
import { provider } from 'web3-core';

@Injectable({
providedIn: 'root'
})

@Injectable({
providedIn: 'root'
})
export class Web3Service {
web3Modal;
web3js: Web3| undefined;
provider: provider | undefined;
accounts: string[] | undefined;
balance: string | undefined;

constructor(@Inject(WEB3) private web3: Web3) {
const providerOptions = {
walletconnect: {
package: WalletConnectProvider, // required
options: {
infuraId: 'env', // required change this with your own infura id
description: 'Scan the qr code and sign in',
qrcodeModalOptions: {
mobileLinks: [
'rainbow',
'metamask',
'argent',
'trust',
'imtoken',
'pillar'
]
}
}
},
injected: {
display: {
logo: 'https://upload.wikimedia.org/wikipedia/commons/3/36/MetaMask_Fox.svg',
name: 'metamask',
description: "Connect with the provider in your Browser"
},
package: null
},
};

this.web3Modal = new Web3Modal({
network: "mainnet", // optional change this with the net you want to use like rinkeby etc
cacheProvider: true, // optional
providerOptions, // required
theme: {
background: "rgb(39, 49, 56)",
main: "rgb(199, 199, 199)",
secondary: "rgb(136, 136, 136)",
border: "rgba(195, 195, 195, 0.14)",
hover: "rgb(16, 26, 32)"
}
});
}


async connectAccount() {
this.provider = await this.web3Modal.connect(); // set provider
if (this.provider) {
this.web3js = new Web3(this.provider);
} // create web3 instance
this.accounts = await this.web3js!.eth.getAccounts();
return this.accounts;
}

async accountInfo(account: string){
const initialvalue = await this.web3js!.eth.getBalance(account);
this.balance = this.web3js!.utils.fromWei(initialvalue , 'ether');
return this.balance;
}

}

Now let's visit the AppComponent (/src/app/app.component.ts) to see how our web3service is used.

In the Connect method, we call the web3service connectAccount method to connect through our web3modal and then we log the response.

The response from the Connect call is bound to the header in our view, so you can try to connect a wallet to see what the response is.

If all goes well, you should see your connected wallet address on the page.

Congrats, you have made your first web3 interaction

Add Bootstrap UI Framework

We are going to be modifying our DAPPs UI a little, and we will find it better to use a UI framework to keep our UI looking a little reasonable. I find it more straightforward to use bootstrap but you can use any framework you prefer.

Tailwind is installed, so I will disable it, and add bootstrap instead. So go to styles.css and remove all the stylings there.

There are two ways to install bootstrap.

  • Via a CDN or
  • Via an Angular plugin

While using an angular plugin is preferable, we won't be doing that as that's outside the scope of this tutorial, you can look up this tutorial to see how to setup bootstrap via the angular plugin. https://www.freecodecamp.org/news/how-to-add-bootstrap-css-framework-to-an-angular-application

For now, go to https://getbootstrap.com/docs/5.2/getting-started/introduction/ and copy the bootstrap css files and scripts and add it to the head and body section in your index.html(/src/index.html)

We can now change the code in our app-component to use a more "bootstrappy" theme and we will also add support for routing by adding a Router Outlet.

<nav class="navbar navbar-expand-sm navbar-dark bg-primary">
<div class="container-fluid">
<a class="navbar-brand" href="#">
<h1>Angular DAPP</h1>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="/">Home</a>
</li>

<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Auctions
</a>
<ul class="dropdown-menu" aria-labelledby="navbarDropdown">
<li><a class="dropdown-item" href="/create-auction">Create Auction</a></li>
<li><a class="dropdown-item" href="/list-auction">All Auctions</a></li>
</ul>
</li>

</ul>

</div>
</div>
</nav>
<div class="container-fluid py-3">
<div class="row my-3">
<div class="col d-flex justify-content-center">
<h3>My Angular DAPP</h3>

</div>
</div>
<div class="row my-3">
<div class="col d-flex justify-content-center">
<div *ngIf="accounts "> <span>{{balance}} CELO </span> {{accounts[0]}}</div>
<button *ngIf="!accounts " class="btn btn-primary py-2 px-4 rounded" (click)="Connect()" >
Connect your wallet
</button>
</div>
</div>

<div class="row">
<div class="col my-3 ">
<router-outlet></router-outlet>
</div>
</div>
</div>

Now open the app.module file(src/app/app.module.ts), let's configure our app module to support routing. Add this import to the file (under existing imports)

import {RouterModule, Routes } from '@angular/router';

Also create an array for storing our routes

const routes: Routes = [];

Finally, in your NgModule, add the router module to the imports

@NgModule({
...
imports: [
....
RouterModule.forRoot(routes)
],
...
})
export class AppModule {}

Your App Module should look like this now

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

import { AppComponent } from './app.component';
import { RouterModule, Routes } from '@angular/router'

const routes: Routes = [];

@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
BrowserAnimationsModule,
RouterModule