Full Stack Ethereum and Dapp Development. A comprehensive guide: 2024
Checkout the tutorial on Dev.to
Posted on May 27, 2024
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:
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
Checkout the tutorial on Dev.to
In this tutorial, we are going to learn how to:
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.
npm install -g yarn
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
You can use either Yarn or NPM but for the purpose of this tutorial, I will recommend using Yarn
yarn add hardhat
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.
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.
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.
Let me quickly walk you through the process of getting your first API key on the Infura platform
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
}
}
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;
}
}
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.
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.
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
}
}
};
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.
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"];
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.
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:
message
from the smart contract.message
. Here are the few steps we need to take to achieve these goals:
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;
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.
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.
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.
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,
},
},
};
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;
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.
Posted on May 27, 2024
Sign up to receive the latest update from our blog.