In this article, we are going to learn how to connect a smart contract to the front end. To keep the process less complex and focus on the integration process, the smart contract in itself is kept simple. You can easily introduce multiple complex functions once you understand the basics.
For this tutorial, we are simply putting a campaign on the local blockchain, and once it has been mined it will be shown on the front end. I have taken this example because to take in the data from the front and put it back is the most basic thing, once you learn that, you can, of course, perform any required function on the data and then show it on the front page.
Prerequisites
- Having Metamask extension and knowing the basic workings of Metamask wallet, like connecting accounts, confirming transactions, etc.
- Some experience with building React or Next JS apps.
- Being familiar with the folder structure of the Next application.
- Basic knowledge about smart contracts- what they do and how they function.
We are working with Next js as the frontend and Hardhat as the Ethereum development environment for this tutorial.
Approach to integrating your Smart Contract with Frontend
We will be creating a simple web app to demonstrate the integration of the frontend with smart contracts.
- Initialize Next JS App.
- Then install the required dependencies for web3 development using the given command.
- Initialize the hardhat environment.
- Writing the smart contract.
- Writing the deploy script.
- Deploying the contract.
- Making the Context folder to integrate the smart contract and the frontend.
- Making the frontend for our app.
- Wrapping the layout in the Web3Context provider.
- Running the app.
Implementation
Step 1: Initialize Next App
Use the command to create a Next JS app and proceed with Javascript:
npm create next-app@latest
Initialize Next AppStep 2: Install Dependencies
Run this command to install the required and compatible dependencies:
npm install @headlessui/react@^1.7.19 @heroicons/react@^2.1.3 @next/font@^13.2.4 @nomicfoundation/hardhat-toolbox@^2.0.2 autoprefixer@^10.4.19 bufferutil@^4.0.8 ethers@^5.7.2 hardhat@^2.22.3 next@^13.2.4 react@^18.2.0 react-dom@^18.2.0 utf-8-validate@^5.0.10 web3modal@^1.9.12 postcss@^8.4.38 tailwindcss@^3.4.3 --save
Managing dependencies can be challenging, especially in the rapidly evolving landscape of web3 development. Conflicting versions can lead to frustration and annoyance among developers. To ensure compatibility and avoid conflicts, use the above command to install dependencies that work seamlessly together.
Step 3: Initialize the Hardhat Environment
Initialise the hardhat environment by running
npx hardhat init
Initialize HardHat EnvironmentStep 4: Writing Smart Contract
After initializing the hardhat env you will notice a 'Contracts' folder has been created. Create a file with the name of your smart contract. Here we are using CrowdFunding.sol.
CrowdFunding.sol:
Solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract CrowdFunding {
address public owner;
uint public totalFunds;
uint public target;
bool public campaignEnded;
struct Campaign {
address creator;
string title;
string description;
uint amount;
uint deadline;
uint amountCollected;
bool active;
}
mapping(address => uint) public contributors;
mapping(uint => Campaign) public campaigns;
uint public campaignCounter;
event FundsContributed(address indexed contributor, uint amount);
event CampaignCreated(
address indexed creator,
string title,
uint amount,
uint deadline
);
event CampaignEnded(uint totalFunds, bool succeeded);
constructor(uint _target) {
owner = msg.sender;
target = _target;
campaignEnded = false;
}
modifier onlyOwner() {
require(msg.sender == owner, "Only the owner can call this function");
_;
}
modifier campaignNotEnded() {
require(!campaignEnded, "Campaign has already ended");
_;
}
function createCampaign(
string memory _title,
string memory _description,
uint _amount,
uint _deadline
) public campaignNotEnded {
require(_amount > 0, "Contribution amount must be greater than 0");
require(_deadline > block.timestamp, "Deadline must be in the future");
campaigns[campaignCounter] = Campaign({
creator: msg.sender,
title: _title,
description: _description,
amount: _amount,
deadline: _deadline,
amountCollected: 0,
active: true
});
campaignCounter++;
emit CampaignCreated(msg.sender, _title, _amount, _deadline);
}
function contribute(uint _campaignId) public payable campaignNotEnded {
require(campaigns[_campaignId].active, "Campaign is not active");
require(msg.value > 0, "Contribution amount must be greater than 0");
require(
block.timestamp < campaigns[_campaignId].deadline,
"Campaign deadline has passed"
);
contributors[msg.sender] += msg.value;
campaigns[_campaignId].amountCollected += msg.value;
totalFunds += msg.value;
emit FundsContributed(msg.sender, msg.value);
checkGoalReached(_campaignId);
}
function checkGoalReached(uint _campaignId) private {
if (
campaigns[_campaignId].amountCollected >=
campaigns[_campaignId].amount
) {
campaigns[_campaignId].active = false;
campaignEnded = true;
emit CampaignEnded(totalFunds, true);
}
}
function withdrawFunds() public onlyOwner {
require(campaignEnded, "Campaign has not ended yet");
require(address(this).balance >= totalFunds, "Insufficient funds");
payable(owner).transfer(totalFunds);
}
function getBalance() public view returns (uint) {
return address(this).balance;
}
function getCampaigns() public view returns (Campaign[] memory) {
Campaign[] memory allCampaigns = new Campaign[](campaignCounter);
for (uint i = 0; i < campaignCounter; i++) {
allCampaigns[i] = campaigns[i];
}
return allCampaigns;
}
}
Step 5: Writing Deployment Script for the Contract
Create a folder Scripts and create a file named deploy.js. In this file the code for deploying the contract will be written.
JavaScript
const { ethers } = require("hardhat");
async function main() {
const CrowdFunding = await ethers.getContractFactory("CrowdFunding");
const crowdFunding = await CrowdFunding.deploy(1000); // Pass the target amount here (e.g., 1000 wei)
await crowdFunding.deployed();
console.log("CrowdFunding deployed to:", crowdFunding.address);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
Note:
The name of the Contract is passed in the getContractFactory function.
Step 6: Deploying the contract
1. Deploy the smart contract using the command:
npx hardhat node
This initialises a local blockchain and gives about 20 addresses with 1000 ETH for testing.
2. You can import any of these account in your Metamask wallet by choosing to import account and then entering the private key.
The accounts after running npx hardhat node3. Open another terminal and run the command:
npx hardhat run --network localhost scripts/deploy.js
This command will deploy your contract and create a folder called artifacts.
Artifacts Folder4. It will also print the address on which our contract is deployed at.
Contract AddressCopy this address for now.
Step 7: Create Context Folder
To connect smart contract with the frontend, we always need a file separate from the frontend file and the smart contract. This file acts like a bridge between the two.
1. Create a folder Context, and inside it create a file called CrowdFunding.js and Constants.js. Constants.js will contains the attributes of the deployed contract, that will be used in the CrowdFunding.js file.
Constants.js (in Context folder):
JavaScript
import crowdFunding from './CrowdFunding.json';
export const CROWDFUNDING_ADDRESS = '0x5FbDB2315678afecb367f032d93F642f64180aa3'; // Update this address
export const CROWDFUNDING_ABI = crowdFunding.abi;
Paste the copied address in the field of CROWDFUNDING_ADDRESS.
2. Now have a look at the artifacts folder that had been created after the deployment of the contract. You should see a contracts folder inside it and in the contracts folder, there will be a file called CrowdFunding.json. Move this file from this folder to Context folder (simply by dragging).
CrowdFunding.json file has to be moved from artifacts/Contracts to Context folderThis file will be used to get the ABI of the contract from the json file.
3. Write the code for another file inside Context i.e. Crowdfunding.js. This is the file that connects the smart contract with the frontend.
Crowdfunding.js:
JavaScript
'use client'
import React, { useState, useEffect } from "react";
import Web3Modal from "web3modal";
import { ethers } from "ethers";
// INTERNAL IMPORTS
import { CROWDFUNDING_ABI, CROWDFUNDING_ADDRESS } from "./Constants";
// FETCHING SMART CONTRACT
const fetchContract = (signerOrProvider) =>
new ethers.Contract(CROWDFUNDING_ADDRESS, CROWDFUNDING_ABI, signerOrProvider);
export const CrowdFundingContext = React.createContext();
export const CrowdFundingProvider = ({ children }) => {
const titleData = "Crowd Funding";
const [currentAccount, setCurrentAccount] = useState("");
const [error, setError] = useState(null);
const createCampaign = async (campaign) => {
const { title, description, amount, deadline } = campaign;
const web3Modal = new Web3Modal();
const connection = await web3Modal.connect();
const provider = new ethers.providers.Web3Provider(connection);
const signer = provider.getSigner();
const contract = fetchContract(signer);
try {
const transaction = await contract.createCampaign(
title,
description,
ethers.utils.parseUnits(amount, 18),
new Date(deadline).getTime()
);
await transaction.wait();
console.log("Transaction Mined", transaction);
} catch (error) {
console.log("Error", error);
}
};
const getCampaigns = async () => {
const provider = new ethers.providers.JsonRpcProvider();
const contract = fetchContract(provider);
const campaigns = await contract.getCampaigns();
const parsedCampaigns = campaigns.map((campaign, i) => ({
owner: campaign.owner,
title: campaign.title,
description: campaign.description,
target: ethers.utils.formatEther(campaign.amount.toString()), // Corrected line
deadline: campaign.deadline.toNumber(),
amountCollected: ethers.utils.formatEther(
campaign.amountCollected.toString()
),
pId: i,
}));
return parsedCampaigns;
};
const ifWalletConnected = async () => {
try {
if (!window.ethereum) {
setError("Install Metamask");
return false;
}
const accounts = await window.ethereum.request({
method: "eth_accounts",
});
if (accounts.length) {
setCurrentAccount(accounts[0]);
return true;
} else {
console.log("No account found");
}
} catch (error) {
console.log("Something wrong while connecting to the wallet ", error);
return false;
}
};
useEffect(() => {
ifWalletConnected();
}, []);
const connectWallet = async () => {
try {
if (!window.ethereum) {
setError("Install Metamask");
return;
}
const accounts = await window.ethereum.request({
method: "eth_requestAccounts",
});
setCurrentAccount(accounts[0]);
} catch (error) {
console.log("Something wrong while connecting to the wallet", error);
}
};
return (
<CrowdFundingContext.Provider
value={{
titleData,
currentAccount,
connectWallet,
createCampaign,
getCampaigns,
error,
}}
>
{children}
</CrowdFundingContext.Provider>
);
};
Explanation:
- fetchContract(signerOrProvider): Returns an instance of the CrowdFunding smart contract.
- createCampaign(campaign): Creates a new campaign on the blockchain.
- getCampaigns(): Retrieves all existing campaigns from the blockchain.
- ifWalletConnected(): Checks if the user's wallet is connected.
- connectWallet(): Connects the user's wallet to the application.
Step 8: Create Frontend for our Smart Contract
We will be writing some frontend in the page.js file in the src/app folder. This file will contain a button to connect wallet to Metamask wallet. After the connection is made, address of the connected account will be shown instead of the button. It has a form to fill the details of the campaign. After the Campaign has been made, it will be displayed on the page.
page.js (inside src/app folder):
JavaScript
'use client'
import Image from "next/image";
import React, { useState, useContext, useEffect } from 'react'
import { CrowdFundingContext } from '../../Context/CrowdFunding'
export default function Home() {
const { createCampaign, error, getCampaigns, currentAccount, connectWallet } = useContext(CrowdFundingContext)
const [title, setTitle] = useState('')
const [description, setDescription] = useState('')
const [amount, setAmount] = useState('')
const [deadline, setDeadline] = useState('')
const [errorMessage, setErrorMessage] = useState('')
const [campaigns, setCampaigns] = useState([])
const fetchCampaigns = async () => {
try {
const data = await getCampaigns()
setCampaigns(data)
} catch (error) {
console.error('Error while fetching campaigns:', error)
setErrorMessage('Error while fetching campaigns')
}
}
useEffect(() => {
fetchCampaigns()
}, [])
const handleSubmit = async (e) => {
e.preventDefault()
const campaign = {
title,
description,
amount,
deadline
}
try {
await createCampaign(campaign)
setErrorMessage('')
setTitle('')
setDescription('')
setAmount('')
setDeadline('')
fetchCampaigns()
} catch (error) {
console.error('Error while creating campaign:', error)
setErrorMessage(error.message || 'Error while creating campaign')
}
}
return (
<div style={{ maxWidth: '800px', margin: '0 auto', padding: '20px' }}>
<h1 style={{ fontSize: '24px', fontWeight: 'bold', marginBottom: '10px' }}>GFG on WEB3</h1>
{!currentAccount ? (
<button
style={{ backgroundColor: '#007bff', color: 'white', border: 'none', padding: '10px 20px', fontSize: '16px', cursor: 'pointer', marginBottom: '20px' }}
onClick={connectWallet}
>
Connect Wallet
</button>
) : (
<div style={{ marginBottom: '20px' }}>
<p style={{ marginTop: '10px' }}>Connected Wallet: {currentAccount}</p>
</div>
)}
<h2 style={{ fontSize: '20px', fontWeight: 'bold', marginTop: '20px' }}>Create Campaign</h2>
<form onSubmit={handleSubmit} style={{ marginTop: '20px' }}>
<div style={{ marginBottom: '10px' }}>
<label style={{ display: 'block', fontSize: '16px', marginBottom: '5px' }}>Title:</label>
<input
style={{ width: '100%', padding: '10px' }}
type='text'
value={title}
onChange={e => setTitle(e.target.value)}
/>
</div>
<div style={{ marginBottom: '10px' }}>
<label style={{ display: 'block', fontSize: '16px', marginBottom: '5px' }}>Description:</label>
<textarea
style={{ width: '100%', padding: '10px' }}
value={description}
onChange={e => setDescription(e.target.value)}
/>
</div>
<div style={{ marginBottom: '10px' }}>
<label style={{ display: 'block', fontSize: '16px', marginBottom: '5px' }}>Amount:</label>
<input
style={{ width: '100%', padding: '10px' }}
type='number'
value={amount}
onChange={e => setAmount(e.target.value)}
/>
</div>
<div style={{ marginBottom: '10px' }}>
<label style={{ display: 'block', fontSize: '16px', marginBottom: '5px' }}>Deadline:</label>
<input
style={{ width: '100%', padding: '10px' }}
type='date'
value={deadline}
onChange={e => setDeadline(e.target.value)}
/>
</div>
<button
type='submit'
style={{ backgroundColor: '#007bff', color: 'white', border: 'none', padding: '10px 20px', fontSize: '16px', cursor: 'pointer', marginTop: '10px' }}
>
Create Campaign
</button>
{errorMessage && <p style={{ color: 'red', marginTop: '10px' }}>{errorMessage}</p>}
</form>
<h2 style={{ fontSize: '20px', fontWeight: 'bold', marginTop: '20px' }}>Campaigns</h2>
<div>
{campaigns.map((campaign, index) => (
<div key={index} style={{ border: '1px solid #ccc', borderRadius: '5px', padding: '10px', marginTop: '20px' }}>
<h3 style={{ fontSize: '18px', fontWeight: 'bold' }}>{campaign.title}</h3>
<p>Description: {campaign.description}</p>
<p>Target Amount: {campaign.target}</p>
<p>Deadline: {new Date(campaign.deadline).toLocaleDateString()}</p>
</div>
))}
</div>
</div>
);
}
Step 9: Wrap the Layout Inside the Web3 Provider
Wrap the children inside the layout.js(in src) with CrowdFundingProvider that we made in CrowdFunding.js(Context).
src/layout.js:
JavaScript
import { Inter } from "next/font/google";
import "./globals.css";
import {CrowdFundingProvider} from '../../Context/CrowdFunding'
import Form from "../../Components/Form";
const inter = Inter({ subsets: ["latin"] });
export const metadata = {
title: "GFG App",
description: "Integrating front-end with smart contracts",
};
export default function RootLayout({ children }) {
return (
<>
<html lang="en">
<body className={inter.className}>
<CrowdFundingProvider>
<div>
</div>
{children}
</CrowdFundingProvider>
</body>
</html>
</>
);
}
Step 10: Execute the Application
1. Run the command:
npm run dev
If you get an error related to - The `app` directory is experimental, then modify the next.config.mjs in the following manner:
JavaScript
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
appDir: true,
},
};
export default nextConfig;
2. After Running npm run dev, you should be able to see this page:
Home Page for our App3. On clicking the Connect Wallet button a Metamask window will popup and after you select the account you want to connect with the contract address will be visible on the site in place of the button.
Account Connected and Wallet Address Displayed4. Fill in the form fields.
Create Campaign5. After you click on the Create Campaign button a Metamask notification should pop up, asking for confirmation for the payment. This payment is the gas fee. Click on confirm and a campaign will be added to the list.
Campaign is created and displayed!Note:
1. Every time you redeploy your contract you need to change the contract address in the constants.js file. Also you have to delete the Contract.json file from the Context and paste the new one from the artifacts.
2. You can add the accounts with 1000 ETH each, by copying the private keys that come on the terminal after running npx hardhat node.
1. Click on the Metamask extension. Login and click on the Account that comes on the top of pop up.
Click on the Account Name (Account8)2. Click on Add account or hardware wallet.
Click on Add Account or hardware wallet.3. Click on Import account.
Choose import account.4. Paste the private key from the terminal as the output of run the command:
npx hardhat node
Paste the Private Key and Click ImportAfter you paste the private key and click 'Import', a new account will be created with 1000 ETH balance for development purposes.
Accounts ImportedMultiple accounts can be imported using the discussed method for testing and development of web3 apps.
Conclusion
In this tutorial, we've walked through the process of connecting a smart contract to a frontend using Next.js for the frontend and Hardhat for the Ethereum development environment. By following the steps outlined, you should now have a basic understanding of how to deploy a simple smart contract, integrate it with your frontend, and interact with it via a user interface. This foundational knowledge will enable you to explore more complex functionalities and expand your decentralized applications with greater confidence and efficiency.
Similar Reads
Non-linear Components In electrical circuits, Non-linear Components are electronic devices that need an external power source to operate actively. Non-Linear Components are those that are changed with respect to the voltage and current. Elements that do not follow ohm's law are called Non-linear Components. Non-linear Co
11 min read
Spring Boot Tutorial Spring Boot is a Java framework that makes it easier to create and run Java applications. It simplifies the configuration and setup process, allowing developers to focus more on writing code for their applications. This Spring Boot Tutorial is a comprehensive guide that covers both basic and advance
10 min read
Class Diagram | Unified Modeling Language (UML) A UML class diagram is a visual tool that represents the structure of a system by showing its classes, attributes, methods, and the relationships between them. It helps everyone involved in a projectâlike developers and designersâunderstand how the system is organized and how its components interact
12 min read
Backpropagation in Neural Network Back Propagation is also known as "Backward Propagation of Errors" is a method used to train neural network . Its goal is to reduce the difference between the modelâs predicted output and the actual output by adjusting the weights and biases in the network.It works iteratively to adjust weights and
9 min read
3-Phase Inverter An inverter is a fundamental electrical device designed primarily for the conversion of direct current into alternating current . This versatile device , also known as a variable frequency drive , plays a vital role in a wide range of applications , including variable frequency drives and high power
13 min read
Polymorphism in Java Polymorphism in Java is one of the core concepts in object-oriented programming (OOP) that allows objects to behave differently based on their specific class type. The word polymorphism means having many forms, and it comes from the Greek words poly (many) and morph (forms), this means one entity ca
7 min read
CTE in SQL In SQL, a Common Table Expression (CTE) is an essential tool for simplifying complex queries and making them more readable. By defining temporary result sets that can be referenced multiple times, a CTE in SQL allows developers to break down complicated logic into manageable parts. CTEs help with hi
6 min read
What is Vacuum Circuit Breaker? A vacuum circuit breaker is a type of breaker that utilizes a vacuum as the medium to extinguish electrical arcs. Within this circuit breaker, there is a vacuum interrupter that houses the stationary and mobile contacts in a permanently sealed enclosure. When the contacts are separated in a high vac
13 min read
Python Variables In Python, variables are used to store data that can be referenced and manipulated during program execution. A variable is essentially a name that is assigned to a value. Unlike many other programming languages, Python variables do not require explicit declaration of type. The type of the variable i
6 min read
Spring Boot Interview Questions and Answers Spring Boot is a Java-based framework used to develop stand-alone, production-ready applications with minimal configuration. Introduced by Pivotal in 2014, it simplifies the development of Spring applications by offering embedded servers, auto-configuration, and fast startup. Many top companies, inc
15+ min read