Integrating your Smart Contract with Frontend

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.

Table of Content

  • Prerequisites
  • Approach to integrating your Smart Contract with Frontend
  • Implementation
  • Conclusion
  • FAQs related to Integrating your Smart Contract with Frontend

Prerequisites

  1. Having Metamask extension and knowing the basic workings of Metamask wallet, like connecting accounts, confirming transactions, etc.
  2. Some experience with building React or Next JS apps.
  3. Being familiar with the folder structure of the Next application.
  4. 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.

  1. Initialize Next JS App.
  2. Then install the required dependencies for web3 development using the given command.
  3. Initialize the hardhat environment.
  4. Writing the smart contract.
  5. Writing the deploy script.
  6. Deploying the contract.
  7. Making the Context folder to integrate the smart contract and the frontend.
  8. Making the frontend for our app.
  9. Wrapping the layout in the Web3Context provider.
  10. 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 App

Step 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 Environment

Step 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 node

3. 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 Folder

4. It will also print the address on which our contract is deployed at.

Contract Address

Copy 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 folder

This 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:

  1. fetchContract(signerOrProvider): Returns an instance of the CrowdFunding smart contract.
  2. createCampaign(campaign): Creates a new campaign on the blockchain.
  3. getCampaigns(): Retrieves all existing campaigns from the blockchain.
  4. ifWalletConnected(): Checks if the user’s wallet is connected.
  5. 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 App

3. 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 Displayed

4. Fill in the form fields.

Create Campaign

5. 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.

Step 11: Importing Accounts in Metamask Wallet

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 Import

After you paste the private key and click ‘Import’, a new account will be created with 1000 ETH balance for development purposes.

Accounts Imported

Multiple 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.

FAQs related to Integrating your Smart Contract with Frontend

1. What is the purpose of using Hardhat in this tutorial?

Hardhat is used as the Ethereum development environment to compile, deploy, and test the smart contract. It provides tools and features that make it easier to develop and manage smart contracts on a local blockchain.

2. Why do we need a separate file to connect the smart contract with the frontend?

A separate file, such as CrowdFunding.js in the Context folder, acts as a bridge between the smart contract and the frontend. This separation helps organize the code, making it easier to manage and maintain the integration logic.

3. What should I do if I encounter dependency conflicts during installation?

Dependency conflicts can arise due to version mismatches. To avoid these, use the specific versions of dependencies provided in the tutorial. If issues persist, consider checking for updated compatibility notes or seeking help from the package documentation or community forums.

4. How can I ensure my wallet is properly connected to the application?

Use the connectWallet function in your CrowdFunding.js file to connect the wallet. Ensure you have the MetaMask extension installed and configured. When you click the “Connect Wallet” button, MetaMask should prompt you to select an account. Once connected, the wallet address will be displayed on the frontend.

5. What steps are necessary if I redeploy my smart contract?

After redeploying your smart contract, you need to update the contract address in the Constants.js file. Additionally, you should delete the old Contract.json file from the Context folder and replace it with the new one from the artifacts folder generated by the deployment process. This ensures your frontend is interacting with the correct contract instance.



Contact Us