Building Full stack Dapps with Solidity, Hardhat, React, EthersJs, Mocha and Chai.
Building decentralized applications on the Ethereum blockchain and other EVM-compatible blockchain networks such as Polygon, Avalanche, Celo and many others comes handy in this tutorial.
At the conclusion of this comprehensive guide, you will understand every step required for the concise development and effective deployment of smart contracts, as well as the proper integration of the client-side with React.
After considering numerous options for the tutorial's heading, I decided to include the year of publishing to raise awareness about the tutorial's goals and objectives. Blockchain is a nascent and rapidly expanding technology with several modifications being made to the development method on a daily basis. I'm sure some developers have seen various tutorials that depict comparable teaching, but the methodologies may have altered over time as a result of the changes brought to blockchain development every now and then.
Based on my knowledge and experience as a blockchain developer, the most popular stack being utilized by Web3 developers for building a full stack decentralized application with Solidity includes:
- Ethereum Development Environment - Hardhat
- Ethereum Web Client Library - Ethers.js
- Javascript Test Framework - Mocha
- Client-side framework - React
- Oracle network - Chainlink
- API Layer - The Graph Protocol
To further understand the concept behind the stack mentioned above, I recommend you check out this tutorial.
As I previously stated, the Web3 industry is always growing, with new updates being provided on a daily basis. As a result, I have compiled the most recent complete strategy to simplify blockchain development and will hopefully update this guide as needed.
The code for this tutorial is located here:
azeezabidoye / messenger-dapp
Full Stack Ethereum and Dapp Development. A comprehensive guide: 2024
Full Stack Ethereum and Dapp Development. A comprehensive guide: 2024
Click here to read the tutorial
What we intend to achieve
In this tutorial, we are going to learn how to:
- Create Dapp using Hardhat framework
- Write Solidity smart contract
- Deploy to Celo Alfajores testnet
- Test our Dapp using Mocha and Chai
- Create UI components with React
- Interact with our Dapp
The Stack breakdown
Hardhat: To build smart contracts, you must compile your Solidity code into code that can be easily read and run by the client-side application, deploy your contracts, perform tests, and debug Solidity code without dealing with a live environment.
Hardhat is an Ethereum development environment and framework created specifically for this purpose. We will build our full stack Dapp using the Hardhat framework.React: React is an excellent frontend Javascript library for developing web applications, user interfaces, and UI components.
React and its extensive ecosystem of metaframeworks, including Next.Js, Gatsby, Blitz.Js and others support a wide range of deployments, including classic Single Page Applications (SPAs), static site generators, server-side rendering, and a combination of the three.
We will construct our Dapp by combining React with Hardhat as the client-side library.Ethers.js: The Ethers.js library will serve the purpose of the Ethereum web client library in the development of our Dapp.
In our React application, we'll need to interact with the deployed smart contracts. We'll need a means to read the data and send new transactions.
Ethers.js intends to provide a comprehensive library for the Ethereum blockchain and its ecosystem, covering client-side Javascript applications such as React, Vue, and Angular.Mocha: According to the Mocha website, it is a Javascript framework that makes asynchronous testing simple and fun.
Before we deploy our Dapp to the blockchain network, we need to execute a series of tests to ensure that the smart contracts function properly.
Mocha tests run serially, allowing for flexible and reliable reporting; this is the library we will use in our projects.Metamask is a Google Chrome extension that injects itself into Javascript code anytime your Dapp frontend is loaded.
This Chrome extension assists with account administration and connects the current user to the blockchain.
Once a user has connected their Metamask wallet, you as a developer can interact with the globally available Ethereum API (windows.ethereum
), which identifies users of web3-compatible browsers (such as Metamask), and whenever you request a transaction signature, Metamask will prompt the user to confirm it immediately.Chainlink: Chainlink bridges a major gap in the blockchain ecosystem by enabling smart contracts to safely communicate with real-world apps, increasing their use cases and effectiveness.
Chainlink is a significant decentralized oracle network that enables data interchange between on-chain and off-chain applications. It is essential for providing real-world information to blockchain networks.The Graph: Because most blockchain applications, such as Ethereum, are difficult and time-consuming to read data from the chain, both companies and individuals create their own centralized indexing servers and service API requests from them. Unfortunately, this requires a significant investment in engineering and hardware, as well as a compromise of the security features essential for decentralization.
The Graph Protocol is an indexing protocol for searching blockchain data that allows for the development of completely decentralized apps while also offering a rich GraphQL query layer for application consumption.
The tutorial is absolutely beginner-friendly thereby for some reasons we will not discuss Chainlink and The Graph. However, there will be reasons to reference these tools in tutorials in the future.
Prerequisites
- NodeJs
- Metamask
- Testnet ethers
Dev tool
- Yarn
npm install -g yarn
Let's start hacking...
Step #1: Create new React project
To get started, create a new React application
npm create vite@latest project_name --template react
In our case, we will name the application messenger-dapp
. Therefore, you can run:
npm create vite@latest messenger-dapp --template react
Follow the prompt, choose React and finally choose Javascript.
✍️ Your React app is created with the project-name specified.
Navigate to the new project directory.
cd messenger-dapp
Step #2: Install Hardhat package as a dependency
You can use either Yarn or NPM but for the purpose of this tutorial, I will recommend using Yarn
yarn add hardhat
Step #3: Configure Hardhat Ethereum Development environment
npx hardhat init
Follow the prompt and select Create a Javascript project
.
Press “y
" to agree to other options and continue.
✍️ Hardhat will automatically install all necessary packages for your project.
Hardhat folder structure
- Hardhat.config.cjs file: Serves as the entry point of our development; includes every configuration, plugins, and custom tasks.
- Contracts directory: Directory for all Solidity contract codes.
- Test: Directory for test scripts.
Delete all the files in both Contracts and Test directories. This is to ensure that we have a clean slate for our code and development.
Step #4: Setup environment variables
Environment variables are predetermined values that are typically used to provide the ability to configure the way programs, applications and services will behave.
For this tutorial, there are two basic environment variables we will be needing for our development. They are; an Infura API key which will help us to run our node during deployment and our Private key.
Getting started with Infura API key
Let me quickly walk you through the process of getting your first API key on the Infura platform
- Visit infura.io
- Get started by setting up an Infura account
- Select "Create new API key" on the dashboard. Follow the first two steps in this documentation for guidance
- Click on the API key name to see all endpoints provided
- Check your desired network endpoints and "Save Changes"
- Select "Active Endpoints" in the navigation.
- Copy the HTTPS URL provided for your testnet.
BONUS: How to Integrate Celo Alfajores Testnet with Metamask
- Open Metamask
- Select the network dropdown at the top left corner
- Turn on the “Show testnet” option to see test networks
- To add Celo Alfajores testnet to the list; visit chainlist.org
- Endeavour to check the “Include Testnets” checkbox
- Search for “Celo Alfajores” in the search field
- Click “Add to Metamask” on the network grid
- Follow the prompt and finally “Switch network”
- Get free tokens from Celo faucet; visit faucet.celo.org
- Paste wallet address into the “Account address” field
- Click on “Faucet” button
- Wait for process to be completed > 🎉 Boom: Your account has been funded with 0.5 Celo token
Configure Private key
- Open Metamask
- Click the stacked dots at the top right corner
- Select "Account details"
- Select "Show private key"
- Enter your password to continue
- Click and hold "Hold to reveal Private Key" button to reveal your Private key
- Copy your Private key for configuration
Step #5: Configure Hardhat for Dapp development
Navigate to the hardhat.config.cjs
file and configure the network for the testnet.
networks: {
alfajores: {
chainId: 44787,
url: "https://celo-alfajores.infura.io/v3/1644a8878efe4a5e8b1eabc92564e725", // Insert Infura Celo Url here
accounts: [a77868cba9d67ed2854547cdebb3e30c52cabaa1c4646beefing786c811c5fc], // Insert Metamask Private key here
}
}
Endeavour to prefix your Private key with an
0x
and wrap it in quotes to avoid errors.
networks: {
alfajores: {
// Code here
accounts: ["0xa77868cba9d67ed2854547cdebb3e30c52cabaa1c4646beef008e786c811c5fc"], // Insert Metamask Private key here
}
}
Step #6: Create the smart contract file
Navigate to the Contracts
directory and create a new file for the Solidity code as Messenger.sol
and update the file with the following code:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "hardhat/console.sol";
contract Messenger {
string message;
constructor(string memory _message) {
console.log("Deploying Messenger with message:", _message);
message = _message;
}
function getMessage() public view returns (string memory) {
return message;
}
function setMessage(string memory _message) public {
console.log("Changing message from '%s' to '%s'", message, _message);
message = _message;
}
}
About the contract
This smart contract is simple. The contract has a variable that was defined in the global scope but assigned a value in the function constructor.
The function constructor is only called when the contract is deployed, therefore it sets the message
variable. It also exposes a function (getMessage
) that can be used to retrieve the message.
Additionally, another function called (setMessage
) is available, which allows users to modify the message variable. When this contract gets deployed on the Ethereum blockchain, users will be able to interact with these methods.
Reading and writing to the Ethereum blockchain
There are two main ways to interact with an Ethereum smart contract: reading and writing. Reading is a non-transactional activity, whereas writing is transactional. In the smart contract shown above, the (getMessage
) function is considered reading, whereas the (setMessage
) method is considered writing or transactional.
You do not need to carry out a transaction if you are merely reading from the blockchain and not modifying or updating anything; there will be no gas or cost involved. The function you request is then carried out exclusively by the node to which you are connected, thus you do not have to pay for gas and reading is free.
However, while writing or initializing a transaction, you must pay for it to be included in the blockchain. To make this work, you must pay gas, which is the cost or price necessary to properly complete a transaction or execute a contract on the Ethereum blockchain.
From our client-side application, we will communicate with the smart contract using the ethers.js library, the contract address, and the ABI generated by hardhat from the contract.
Step #7: Compile the contract
Compiling a smart contract involves using the contract's source code to generate its bytecode and the contract Application Binary Interface (ABI). The Ethereum Virtual Machine (EVM) executes the bytecode to understand and execute the smart contract.
After we run the command to compile the smart contract, Hardhat generates a directory named artifacts
in our root directory.
We can specify where the artifacts
directory should be simply by adding a few options to the hardhat.config.cjs
file.
paths: {
artifacts: "./src/artifacts",
}
⚠️ Specifying a directory for the auto-generated ABI has no bearing on the smart contract compilation process. It's just a good practice to place the ABI in the
src
folder because that's where our client-side code will be created.
As of now, your Hardhat configuration should look like this:
module.exports = {
solidity: "0.8.24",
paths: {
artifacts: "./src/artifacts"
},
networks: {
alfajores: {
chainId: 44787,
url: "https://celo-alfajores.infura.io/v3/1644a8878efe4a5e8b1eabc92564e725", // Insert Infura Celo Url here
accounts: ["0xa77868cba9d67ed2854547cdebb3e30c52cabaa1c4646beefinge786c811c5fc"], // Insert Metamask Private key here
}
}
};
What is an ABI?
ABI stands for application binary interface. You might consider it as an interface between your client-side application and the Ethereum blockchain, where the smart contract with which you will be interacting with is created.
Now that we've covered the fundamentals of smart contracts and are familiar with ABIs, let's create one for our project.
yarn hardhat compile
✍️ Watch out for the
artifacts
which is automatically added to thesrc
directory of your project.
Step #8: Configure the Dapp for deployment
This is the most important stage of Dapp development; a few settings must be completed for a timely and effective deployment.
Create a folder for deployment scripts in the root directory
mkdir deploy
Create a file for the deployment scripts in the deploy
directory with a numbered naming structure. e.g 00-deploy-messenger.cjs
Install an Hardhat plugin as a package for deployment
yarn add hardhat-deploy --dev
Import hardhat-deploy
package to the hardhat-config.cjs
file
require("hardhat-deploy")
Install hardhat-deploy-ethers
to override the @nomiclabs/hardhat-ethers
package
yarn add --dev @nomiclabs/hardhat-ethers@npm:hardhat-deploy-ethers
✍️ The command above allows Ethers to keep track of and remember all of the multiple deployments that we perform within our contract.
Set up a deployer account in the hardhat-config.cjs
file
networks: {
// Code Here
},
namedAccounts: {
deployer: {
default: 0,
}
}
Update the 00-deploy-messenger.cjs
file with the following code to deploy the Messenger
contract
module.exports = async ({ getNamedAccounts, deployments }) => {
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
await deploy("Messenger", {
contract: "Messenger",
from: deployer,
args: ["Hello Devs...this is simple and fun"], // The message value in the function constructor
log: true, // Logs statements to console
});
};
module.exports.tags = ["Messenger"];
Step #9: Deploy contract to testnet
Now, we can execute the deploy script and instruct the CLI that we want to deploy to Celo Alfajores test network.
yarn hardhat deploy --network alfajores
Once this script is executed, the smart contract should be deployed to the Celo Alfajores test network, enabling us to interact with it.
The output of the CLI should look like this:
deploying "Messenger" (tx: 0x59ef946ed3b481f78ea04929e4a1724aeccf7a3598fe7900e269e05ed90d4385)...: deployed at 0xAf22bD61d2206D22050C524003017817DebE61e4 with 633051 gas
✨ Done in 30.37s.
And here is the contract address:
deployed at 0xAf22bD61d2206D22050C524003017817DebE61e4
✍️ Observe how the token balance has changed. Gas fee for the Dapp's deployment have been deducted from the token balance. We would encounter more of this anytime we made transactional requests to the blockchain.
Step #10: Create and connect UI components with React
In this tutorial, we will create a basic UI component using React. To get you started, we'll focus on essential functions and a few CSS styles.
Let us briefly explore the two major goals of our React application:
- Retrieve the current value of
message
from the smart contract. - Allow a user to update the value of the
message
.
Here are the few steps we need to take to achieve these goals:
- Create an input field and some local state to handle the input value (to update the message).
- Allow the application to connect to the user's MetaMask account for signing transactions.
- Create functions that read and write to the smart contract.
Open the src/App.jsx
file and update it with the following code, set the value of messengerContractAddress
to the address of your smart contract:
import "./App.css";
import React from "react";
import { useState } from "react";
import { ethers, BrowserProvider } from "ethers";
// Import the json-file from the ABI
import Messenger from "./artifacts/contracts/Messenger.sol/Messenger.json";
// Store the contract address in a variable
const messengerContractAddress = "your-contract-address"; // Deployed to testnet
const App = () => {
// Store message in a local state
const [message, setMessageValue] = useState();
// Request access to User's MetaMask account
const requestAccount = async () => {
await window.ethereum.request({ method: "eth_requestAccounts" });
};
// Function for retrieving message value from smart contract.
const getMessage = async () => {
if (typeof window.ethereum !== "undefined") {
const web3Provider = new ethers.BrowserProvider(window.ethereum);
const contract = new ethers.Contract(
messengerContractAddress,
Messenger.abi,
web3Provider
);
try {
const data = await contract.getMessage();
console.log(`Data: ${data}`);
} catch (error) {
console.error(error);
}
}
};
// Function for updating message value on smart contract
const setMessage = async () => {
if (!message) return;
if (typeof window.ethereum !== "undefined") {
await requestAccount();
const web3Provider = new ethers.BrowserProvider(window.ethereum);
const signer = await web3Provider.getSigner();
const contract = new ethers.Contract(
messengerContractAddress,
Messenger.abi,
signer
);
const transaction = await contract.setMessage(message);
await transaction.wait();
setMessageValue("");
getMessage();
}
};
return (
<div className="container">
<button onClick={getMessage}>Message</button>
<button onClick={setMessage}>Send message</button>
<input
onChange={(e) => setMessageValue(e.target.value)}
placeholder="Write your message here..."
/>
</div>
);
};
export default App;
Let's add some styles...
Update the App.css
file with the following CSS code:
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
/* styles.css */
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
}
button {
background-color: #4caf50; /* Green */
border: none;
color: white;
padding: 10px 20px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin: 10px;
cursor: pointer;
border-radius: 5px;
transition: background-color 0.3s ease;
}
button:hover {
background-color: #45a049;
}
input {
padding: 10px;
margin: 10px;
width: 300px;
border: 1px solid #ccc;
border-radius: 5px;
font-size: 16px;
transition: border-color 0.3s ease;
}
input:focus {
border-color: #4caf50;
outline: none;
}
Next: run the React app:
yarn run dev
Now, you can interact with your Dapp. Ensure to keep your console
open to see results.
Step #11: Writing unit tests with Mocha and Chai
Unit testing applies to everything we want to test, whether it's a class, a function, or a single line of code.
In this article, we will look at unit testing our Solidity code using Mocha, a lightweight Nodejs framework, and Chai, a Test-driven development (TDD) assertion library for Node.
Both Mocha and Chai use NodeJs, and the browser supports asynchronous testing. Although Mocha may be used with any assertion library, it is most usually combined with Chai.
Let's initiate some tests for the deployment of the smart contract and the two functions it contains.
First, ensure you have the necessary dependencies installed:
yarn add mocha chai@4.3.7 --dev
Navigate to the test
directory and create a new file as messenger-test.cjs
.
Add the following code to test/messenger-test.cjs
:
const { ethers } = require("hardhat");
const { expect } = require("chai");
describe("Messenger", function () {
let MessengerFactory, messenger;
beforeEach(async function () {
MessengerFactory = await ethers.getContractFactory("Messenger");
messenger = await MessengerFactory.deploy(
"Hello devs...we love EVM development"
);
await messenger.waitForDeployment();
});
describe("Deployment", function () {
it("Should set the correct initial message", async function () {
expect(await messenger.getMessage()).to.equal(
"Hello devs...we love EVM development"
);
});
});
describe("SetMessage", function () {
it("Should change the message when called", async function () {
await messenger.setMessage("We love building Dapps");
expect(await messenger.getMessage()).to.equal("We love building Dapps");
});
it("Should emit console log on message change", async function () {
const transaction = await messenger.setMessage("Happy hacking...");
await transaction.wait();
});
});
});
Navigate to your terminal and run:
yarn hardhat test
The result of your test should pass like this:
Messenger
Deployment
Deploying Messenger with message: Hello devs...we love EVM development
✔ Should set the correct initial message
SetMessage
Deploying Messenger with message: Hello devs...we love EVM development
Changing message from 'Hello devs...we love EVM development' to 'We love building Dapps'
✔ Should change the message when called
Deploying Messenger with message: Hello devs...we love EVM development
Changing message from 'Hello devs...we love EVM development' to 'Happy hacking...'
✔ Should emit console log on message change
3 passing (412ms)
✨ Done in 1.78s.
Big ups to you 👍
Congratulations if you've made it this far, and I commend your determination and desire to learn Web3 development. This is only a basic approach to the development, and I hope you use these techniques whenever you create a decentralized application for Ethereum and other EVM-based blockchain networks.
Although we have addressed certain elements required for effective development, there is something more we must do as professionals.
BONUS: Secure your environment variables
Remember, in Step #4 of this tutorial, we set up our environment variables, which are third-party components required for the creation and deployment of our smart contract.
It is critical that we safeguard them; if the codebase is accidentally shared or made public, the embedded secrets are revealed, resulting in a possible security compromise. Environment variables, on the other hand, are kept on the server and not exposed in the code, minimizing the danger of exposure.
We must protect the API endpoint
we got from Infura and our Metamask Private key
.
Install the dependency module that loads environment variables from a .env
file:
yarn add dotenv --dev
Create a new file in the root directory of the project as .env
.
Add two new variables to the .env
file with their values as follows:
PRIVATE_KEY="a77868cba9d67ed2854547cdebb3e30c52cabaa1c4646beefinge786c811c5fc"
INFURA_ALFAJORES_URL="https://celo-alfajores.infura.io/v3/1644a8878efe4a5e8b1eabc92564e725"
Import the dotenv
module to hardhat-config.cjs
for configuration
require("dotenv").config()
Replicate the two environment variables in hardhat-config.cjs
file
const { PRIVATE_KEY, INFURA_ALFAJORES_URL } = process.env;
Finally, your hardhat-config.cjs
should be detailed as follows:
require("@nomicfoundation/hardhat-toolbox");
require("hardhat-deploy");
require("dotenv").config();
const { PRIVATE_KEY, INFURA_ALFAJORES_URL } = process.env;
/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
solidity: "0.8.24",
paths: {
artifacts: "./src/artifacts",
},
networks: {
alfajores: {
chainId: 44787,
url: INFURA_ALFAJORES_URL,
accounts: [PRIVATE_KEY],
},
},
namedAccounts: {
deployer: {
default: 0,
},
},
};
Conclusion
Finally, we covered the fundamentals of Ethereum and Dapp development in great depth. Let's take a brief look at what we've learnt thus far;
- ✅ Built Dapp using Hardhat framework
- ✅ Developed Solidity smart contract.
- ✅ Deployed to Celo Alfajores testnet.
- ✅ We tested our DApp.
- ✅ Built a client-side application with React.
- ✅ Interacted with the Dapp.
- ✅ Additionally, secure the environment variables.
In my future tutorials, I'll go over more complex smart contract development, as well as how to use Chainlink to bridge communication between on-chain and off-chain networks, as well as how to deploy smart contracts as subgraphs to expose a GraphQL API and implement things like pagination and full text search.
If you find this lesson useful, please upvote and share it so that others can benefit as well. If you encounter any issues or have recommendations for future tutorials, please leave a comment and let me know.
Top comments (2)
Wonderful. What a comprehensive post!
This was extremely helpful. Thank you so much for posting this!