In this tutorial, we are going to utilize the power of creating smart contracts using Solidity and building on a platform like a web application using React JS and Ether JS for users to interact.
Introduction
We will first build a simple smart contract like an address book, which will allow us to add and store contacts as well as remove contacts from our address book and then, we'll build a front-end web application that will interact with this smart contract allowing us to again add and remove contacts.
Create a Hardhat Project
Start by creating a Hardhat project by using the following commands,
Go to the terminal and write,
// Create a folder mkdir book-contract // Move your terminal to the folder directory you created cd book-contract
Next, create a package.json file in your project by using the following commands,
npm init -y
Then, open your code editor, go to the terminal and use these commands to add the required npm packages to your project folder.
// Install Hardhat npm install --save-dev hardhat // Install Hardhat Toolbox npm install @nomicfoundation/hardhat-toolbox // Install dotenv package npm install dotenv
After you are done installing the above-mentioned packages into your project, initialize the project using Hardhat,
npx hardhat init
You will see a dialogue box appears which says to name your project, select a root directory and choose a JavaScript project.
The initialized project has the following structure:
contracts/
scripts/
test/
hardhat.config.js
These are the default paths for a Hardhat project.
contracts/
is where the source files for your contracts should be.test/
is where your tests should go.scripts/
is where simple automation scripts go.
Create a Smart Contract
Now go to the contracts folder and create a new smart contract with the name ContactBook.sol and paste the below-given code.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;
contract ContactBook {
// Declare a private variable 'owner' to store the contract owner's address.
address private owner;
// Define a struct 'Contact' to store contact information with 'name' and 'wallet' fields.
struct Contact {
string name;
address wallet;
}
// Declare a private array 'contacts' to store an array of contacts.
Contact[] private contacts;
constructor() {
// The constructor, executed once when the contract is deployed, setting the owner to the deployer's address.
owner = msg.sender;
}
modifier onlyOwner() {
// Modifier ensures only the contract owner can call certain functions.
require(msg.sender == owner, "Only owner can call this function.");
_;
}
// This function allows the owner to add a contact to the 'contacts' array.
function addContact(string memory _name, address _wallet) public onlyOwner {
contacts.push(Contact(_name, _wallet));
}
function removeContact(uint _index) public onlyOwner {
// To check if the provided index is valid.
require(_index < contacts.length, "Index out of bounds.");
for (uint i = _index; i < contacts.length - 1; i++) {
// This loop shifts the contacts to fill the gap created by removing a contact.
contacts[i] = contacts[i + 1];
}
// This removes the last contact to maintain the correct array length.
contacts.pop();
}
// This function allows anyone to view the list of contacts.
function getContacts() public view returns (Contact[] memory) {
return contacts;
}
}
Test your Smart Contract
After you complete writing the smart contract, go to the test folder, create a new file with the name ContactBook.test.js and write the following code below.
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("ContactBook", function () {
// Declare variables to store the contract factory, contract instance, and deployer's address and another wallet address.
let ContactBook;
let contactBook;
let owner;
let addr1;
// 'beforeEach' hook is executed before each test case.
beforeEach(async function () {
// Get two Ethereum signers (accounts) for testing.
[owner, addr1] = await ethers.getSigners();
// Get the ContactBook contract factory, then deploy the ContactBook contract and ensure the contract is deployed and ready for testing.
ContactBook = await ethers.getContractFactory("ContactBook");
contactBook = await ContactBook.deploy();
});
// First test case, which checks the functionality of adding a contact.
it("Should add a contact", async function () {
const contactName = "Mrinmoy Porel";
const contactWallet = addr1.address;
// Call the 'addContact' function on the ContactBook contract, passing the contactName and contactWallet.
await contactBook.connect(owner).addContact(contactName, contactWallet);
// Get the list of contacts from the ContactBook contract.
const contacts = await contactBook.getContacts();
// Use Chai's 'expect' function to make assertions about the contract's behavior.
// Expect there to be one contact in the list, the contact's name to match the sample name and the contact's wallet address to match addr1's address.
expect(contacts.length).to.equal(1);
expect(contacts[0].name).to.equal(contactName);
expect(contacts[0].wallet).to.equal(contactWallet);
});
// Second test case, which checks the functionality of removing a contact.
it("Should remove a contact", async function () {
const contactName = "Mrinmoy Porel";
const contactWallet = addr1.address;
// Add a contact to the ContactBook contract, similar to the first test case.
await contactBook.connect(owner).addContact(contactName, contactWallet);
// Get the list of contacts before removing a contact.
const initialContacts = await contactBook.getContacts();
// Call the 'removeContact' function on the ContactBook contract to remove the contact at index 0.
await contactBook.connect(owner).removeContact(0);
// Get the list of contacts after removing the contact.
const updatedContacts = await contactBook.getContacts();
// Use Chai's 'expect' function to make assertions about the contract's behavior.
// Expect the number of contacts to decrease by 1.
expect(updatedContacts.length).to.equal(initialContacts.length - 1);
});
});
After you are done writing the test cases for your smart contract, go to the terminal of your project directory and type the following command to check whether your smart contract is working accordingly.
// Command to run test using Hardhat
npx hardhat test
Once all the test cases are cleared, you will be greeted with this dialogue box.
Deploy your Smart Contract
Now this is going to be one of the most important parts of building this project.
Go to the scripts folder and you will find a deploy.js file, if you don't find one just create one. Write the below-given deploy code inside the deploy.js file.
const hre = require("hardhat");
const { ethers } = require("hardhat");
async function main() {
const ContactBook = await hre.ethers.getContractFactory("ContactBook");
const contactbook = await ContactBook.deploy();
await contactbook.deployed();
console.log(`Contract deployed at: ${contactbook.address}`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
After writing the deployment script you also need to go to the hardhat.config.js file and write the hardhat configuration script.
require('dotenv').config();
require("@nomicfoundation/hardhat-toolbox");
const ALCHEMY_API_KEY_URL = process.env.ALCHEMY_API_KEY_URL;
const MUMBAI_PRIVATE_KEY = process.env.MUMBAI_PRIVATE_KEY;
/**
* @type import('hardhat/config').HardhatUserConfig
*/
module.exports = {
defaultNetwork: "mumbai",
networks: {
hardhat: {
},
mumbai: {
url: ALCHEMY_API_KEY_URL,
chainID: 80001,
accounts: [MUMBAI_PRIVATE_KEY],
}
},
solidity: {
version: "0.8.19",
settings: {
optimizer: {
enabled: true,
runs: 200
}
}
},
paths: {
sources: "./contracts",
tests: "./test",
cache: "./cache",
artifacts: "./artifacts",
},
mocha: {
timeout: 40000,
},
};
Create a .env file in your project directory and write the private key from your metamask wallet and Alchemy URL key from alchemy.com by creating a project on the Polygon Mumbai testnet for hardhat to interact with an Alchemy node to deploy the smart contract.
We use dotenv package and a .env file so that your wallet's private key doesn't get uploaded online and people use it to drain funds from your wallet.
// Name the file as ".env"
ALCHEMY_API_KEY_URL= /* Put the Alchemy URL key here */
MUMBAI_PRIVATE_KEY= /* Put your wallet Private key here */
Now after completing all the above-mentioned steps, it's time to deploy your smart contract.
Go to the terminal of your project directory and type,
// Command to deploy smart contract using Hardhat
npx hardhat run scripts/deploy.js --network mumbai
After successfully deploying the smart contract you will be able to see its address.
Now go to Mumbai PolygonScan and paste the contract address, you will be able to see the contract details there.
If you run into any problems, here's the link to the GitHub repository for your convenience, ContactBook Smart contract.
Finally, you have completed half of your work, now let's get into building the front end of the project so users can interact with your project.
Create a Frontend Application
It's great to hear that you've deployed your smart contract on the Polygon Mumbai testnet! To build a contact book using React, Ether.js, Solidity, and Hardhat, you'll need to create a front-end interface for users to interact with your smart contract. Here's a basic outline of the steps you'll need to follow:
Create a React Application: You can create a React application using Create React App or any other method you prefer, here I used Vite.js
npx create vite@latest contact-book cd contact-book
Set up Ether.js: Install Ether.js to interact with your deployed smart contract. You can install it using npm or yarn.
npm install ethers // React-icons library to add icons to your App npm i react-icons
Set Up Web3 and Ether.js: In your React app, set up Web3.js and Ether.js to interact with the Ethereum network through MetaMask. Create a new file
web3.js
in src folder:// Import the ethers.js library to work with Ethereum. import { ethers } from "ethers"; // Define global variables for Web3 provider and Ethereum provider. let web3; let provider; // Define an asynchronous function to set up the Web3 provider. async function setupWeb3() { if (window.ethereum) { // If the browser has the Ethereum object (MetaMask is available): web3 = new ethers.providers.Web3Provider(window.ethereum); // Initialize the Web3 provider using MetaMask. // Check if the connected network is Polygon Mumbai (chain ID: 80001) const networkId = (await web3.getNetwork()).chainId; if (networkId != 80001) { window.alert("Please switch to Polygon Mumbai Testnet"); } // Request user account access from MetaMask. // This will prompt the user to connect their wallet if it's not already connected. await window.ethereum.request({ method: "eth_requestAccounts" }); // After this, 'web3' will be set up and connected to the user's Ethereum wallet. } else { // If MetaMask is not detected, show an alert to the user. alert("MetaMask not detected! Please install MetaMask."); } } // Call the 'setupWeb3' function to set up the Web3 provider when the script is loaded. setupWeb3(); // Export a function to retrieve the Web3 provider. export function getWeb3() { return web3; }
App Component: In your
src/App.jsx
, create your React components and render them in your contact book:// Import necessary libraries and dependencies. import { useState, useEffect } from "react"; import { ethers } from "ethers"; import { GiCancel } from "react-icons/gi"; import "./App.css"; // Import the getWeb3 function. import { getWeb3 } from "./web3"; // Import your contract's ABI JSON. import ContactBook from "../constants/ContactBook.json"; const App = () => { // Define state variables to manage user input and contact list. // Name input const [name, setName] = useState(""); // Wallet Address input. const [walletAddress, setWalletAddress] = useState(""); // List of contacts. const [contacts, setContacts] = useState([]); // Web3 provider. const [web3, setWeb3] = useState(null); // Contract instance. const [contract, setContract] = useState(null); // Initialize Web3 and the contract when the component mounts. useEffect(() => { async function initialize() { const web3Instance = getWeb3(); // Initialize Web3 provider. const provider = new ethers.providers.Web3Provider(window.ethereum); setWeb3(web3Instance); // Paste your deployed-contract address const contractAddress = "0x7eA70E947C0E8b8832ABCb86a35b64809183Fd30"; const signer = provider.getSigner(); const contractInstance = new ethers.Contract(contractAddress, ContactBook.abi, signer); setContract(contractInstance); // Fetch the initial contacts as soon as the component mounts. refreshContacts(); } initialize(); }, []); // Function to add a new contact to the contract. const addContact = async () => { if (web3 && contract) { try { const tx = await contract.addContact(name, walletAddress); await tx.wait(); window.alert("Contact added successfully."); // Refresh the list of contacts after adding. refreshContacts(); // Clear the name and walletAddress input fields. setName(""); setWalletAddress(""); } catch (error) { console.error("Error adding contact: ", error); } } }; // Function to remove a contact from the contract. const removeContact = async (index) => { if (web3 && contract) { try { const tx = await contract.removeContact(index); await tx.wait(); window.alert("Contact removed successfully."); // Refresh the list of contacts after removal. refreshContacts(); } catch (error) { console.error("Error removing contact: ", error); } } }; // Function to retrieve and display the list of contacts from the contract. const refreshContacts = async () => { if (contract) { const allContacts = await contract.getContacts(); setContacts(allContacts); } }; return ( <div className="App"> <h1>Contact Book</h1> <div className="input-field"> <input type="text" placeholder="Enter Name" value={name} onChange={(e) => setName(e.target.value)} /> <input type="text" placeholder="Enter Wallet Address" value={walletAddress} onChange={(e) => setWalletAddress(e.target.value)} /> <button onClick={addContact}>Add Contact</button> </div> <h2>Contacts</h2> <div className="contacts"> <ul> {contacts.map((contact, index) => ( <li key={index}> <div className="contact-details"> <span className="name">{contact.name}</span> <span className="wallet">Wallet Address:</span> <span className="address">{contact.wallet}</span> </div> <button onClick={() => removeContact(index)}><GiCancel /></button> </li> ))} </ul> </div> </div> ); }; export default App;
Style Your App: Go to your App.css file and style your app using CSS.
::-webkit-scrollbar {
background: #fff;
border-radius: 5px;
width: 7px;
}
::-webkit-scrollbar-thumb {
background: #737373;
border-radius: 5px;
width: 6px;
}
body {
background: #000;
display: flex;
justify-content: center;
padding: 7.5rem;
}
.App {
width: 550px;
height: 475px;
display: flex;
padding: 0rem 2rem;
flex-direction: column;
border: 3px solid #737373;
border-radius: 15px;
}
h1 {
color: #fff;
font-weight: 600;
font-size: 2.5rem;
}
.input-field {
display: flex;
flex-direction: row;
}
input:nth-child(1) {
width: 140px;
height: 30px;
background: #ff914d;
border: none;
margin-right: 1rem;
}
input:nth-child(1)::placeholder {
color: #000;
font-weight: bold;
text-align: center;
}
input:nth-child(2) {
width: 250px;
height: 30px;
background: #f06543;
border: none;
margin-right: 1.2rem;
}
input:nth-child(2)::placeholder {
color: #000;
font-weight: bold;
text-align: center;
}
.input-field button {
border: none;
background: #38b6ff;
color: #fff;
padding: 0 1rem;
border-radius: 20px;
font-weight: bold;
}
.input-field button:hover {
cursor: pointer;
}
h2 {
color: #fff;
font-size: 2rem;
font-weight: 600;
margin-bottom: 0;
}
.contacts {
height: 250px;
width: 100%;
padding: .25rem 0;
overflow: auto;
}
ul {
height: 100%;
padding: 1rem 0;
}
li {
list-style: none;
height: 70px;
width: 95%;
background: #545454;
border-radius: 10px;
border: transparent;
display: flex;
flex-direction: row;
padding: .5rem;
margin-bottom: 1rem;
justify-content: space-between;
}
.contact-details {
display: flex;
flex-direction: column;
justify-content: space-between;
}
.contact-details span {
color: #fff;
}
.name {
font-size: 1.75rem;
font-weight: bold;
}
.address {
font-weight: bold;
font-size: 1.15rem;
}
li button {
background: #ff5757;
border: none;
border-radius: 50px;
height: 45px;
width: 45px;
font-size: 2rem;
padding: .4rem;
color: #fff;
}
li button:hover {
cursor: pointer;
}
- Run the App: Start your React app:
npm run dev
Now your React app allows users to connect their MetaMask wallet, add and remove contacts, and view the list of contacts from your Solidity smart contract. Make sure your contract is deployed on the Polygon Mumbai testnet, and your app is running on a development server. Users will need MetaMask or a compatible Ethereum wallet installed to interact with your app.
Your app should look something like this,
If you get stuck anywhere in the code, you can take references from this Frontend repository.
Conclusion
In this article, we've demonstrated how to seamlessly integrate a smart contract with a front-end application. We've created a Contact Book smart contract and a user-friendly React.js app, connected to the Ethereum network via MetaMask. This project highlights the potential of blockchain technology for real-world applications, offering transparency and trust in digital interactions. The key takeaways include the power of smart contracts, user-friendly frontend design, network verification, and a commitment to enhancing the user experience. As blockchain technology continues to evolve, the integration of smart contracts and frontend applications holds promise for decentralized and innovative solutions, making our project a valuable foundation for your journey into blockchain development.
If you enjoyed this article ❤️, recommend sharing this article with your peers and don't forget to check my social-media handles.