Integrating The Web-Frontend With Smart Contracts

Integrating The Web-Frontend With Smart Contracts

In this article, we will discuss how to create our first DApp and integrate its frontend with a smart contract

This article is part of a series of articles around blockchain, web3 and terminologies around it. In this article, we will create a front-end web application that will interact with our smart contract. If you haven't read our previous article, where we wrote our first smart contract, please read it here before proceeding with this one or check out the code.
In the previous article we covered:

  • Writing your first smart contract.
  • How to test smart contracts through test cases?
  • Deploying the contract to a local test net.

We are all set to create our first decentralized app. In this article, we will go step by step in order to integrate our web frontend with the counter smart contract which we deployed earlier. Why counter contract? Because this article aims to inspire everyone to dig deeper into the world of blockchain and smart contracts even if they are not aware of all its terminologies and concepts. However, if you are looking for quick code access, check it out here. Let's get started!!!

Web setup

Initialise an empty React project

To begin with, you have to initialise an empty React project using the code snippet given below:

npx create-react-app my-app
cd my-app

Counter app UI

The counter smart contract, which we deployed earlier requires very minimal UI to interact with the blockchain. All we need is a button to update the count and show the updated state variable value in the UI. A typical counter app can be as simple as the below code snippet:

import React, { useState } from "react";

export default function App() {
  const [count, setCount] = useState(0);

  let incrementCount = () => {
    setCount(count + 1);
  };

  return (
    <div class="root">
      <div>
        <h1 class="text">{count}</h1>
        <button onClick={incrementCount} class="button">
          +
        </button>
      </div>
    </div>
  );
}

ABI

Now that we have a minimal UI ready, let's place the ABI file which we got as an artifact as a part of code compilation and place it under src, the resource for thesame can be found here. In case you want to recompile it and are looking for the steps around it, refer to our previous article.

An abi file acts as an interface for the DApps to communicate to the smart contract as it consists of information about the methods which are available to call outside the contract. We will load the abi object as a constant along with the contract address as shown below:

const contractABI = require("./Counter.json");
const YOUR_CONTRACT_ADDRESS = "0xf9e08779375Be47E3109ED8bbC700619B82361dc";

Wallet Provider

In order for DApps to interact with smart contracts and perform a transaction, the user needs to connect to a wallet provider, which will be a meta mask in our case.

The below method checks for the metatask and if metamask is installed or not, and then further requests for account details and store it locally can be placed. If the metamask does not exist, the user will be prompted to install the metamask plugin and try again using the following code snippet:

const connectWalletProvider = async () => {
    try {
      const { ethereum } = window;
      if (!ethereum) {
        // Wallet not installed
        alert("Get MetaMask!");
        return;
      }

      // Change network to rinkeby
      await ethereum.enable();
      const accounts = await ethereum.request({
        method: "eth_requestAccounts",
      });
      await ethereum.request({
        method: "wallet_switchEthereumChain",
        params: [{ chainId: `0x${Number(4).toString(16)}` }],
        // I have used Rinkeby, so switching to network ID 4
      });
      console.log("Connected", accounts[0]);
      localStorage.setItem("walletAddress", accounts[0]);
    } catch (error) {
      console.log(error);
    }
  };

  useEffect(() => {
    connectWalletProvider();
    if (window.ethereum) {
      // Listeners
      window.ethereum.on("chainChanged", () => {
        window.location.reload();
      });
      window.ethereum.on("accountsChanged", () => {
        checkedWallet();
      });
    }
  }, []);

Ether for smart contract interaction

Now that we have successfully connected to our wallet, we are all set to start interacting with the smart contract. We have two methods to do this, one is to get the value of the counter state variable and another one is a transaction to increment the counter. As a part of dev dependencies, we need an ether object injected globally and an ether module contains functions that allow a user to interact with the Ethereum blockchain. We will create a method that in turn returns a contract object. The contract object contains contract pieces of information such as ABI, address of the contract, and the instance of the signer ie. who is accessing the contract. Run the following code to proceed:

let getContract = () => {
  const provider = new ethers.providers.Web3Provider(window.ethereum);
  const signer = provider.getSigner();
  let contract = new ethers.Contract(
    YOUR_CONTRACT_ADDRESS,
    contractABI.abi,
    signer
  );
  return contract;
};

Get Count Value

The Get Count method returns the value of the count on the blockchain. Use the following snippet to proceed:

let fetchCurrentValue = async () => {
  let count_ = await getContract().getCount();
  console.log(+count_.toString());
  setCount(count_.toString());
};

Increment count

An Increment count is a transaction on the blockchain, that is going to cost us gas. Run the following code to execute this

let incrementCount = async () => {
  const tx = await getContract().increment();
  alert("Once block is mined, Value will be auto updated");
  await tx.wait();
  fetchCurrentValue();
};

Events on blockchain

In the above method, we waited for the transaction to be completed. Now this might provide for an optimal user experience as the time it takes for a transaction to process depends on the traffic on the network, hence we will do a small modification to our contract so that every time a state change happens on the blockchain, ie. the value of the counter is incremented, we will emit an event named CounterIncremented. This event trigger can be listened on the frontend using an on-event listener as shown below:

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Counter {
    uint256 count; // persistent contract storage

    event counterIncremented(address sender, uint256 value);

    constructor(uint256 _count) {
        count = _count;
    }

    function increment() public {
        count += 1;
        emit counterIncremented(msg.sender, count);
    }

    function getCount() public view returns (uint256) {
        return count;
    }
}

Event listener on the Frontend

With the event being emitted on the blockchain, we will listen to it on the frontend every time we initiate an incrementCounter transaction. In the below code snippet, we no longer have to wait for the transaction to get completed, rather we can update the counter value on event changes using the code given below:

let listenToEvent = async () => {
  getContract().on("counterIncremented", async (sender, value, event) => {
    // Called when anyone changes the value
    setCount(+value.toString());
  });
};

let incrementCount = async () => {
  const tx = await getContract().increment();
  alert("Once block is mined, Value will be auto updated");

};

Final touch

We are now all set to run our first Dapp locally. However, we will add some beautification to the code, such as relevant loaders when fetching the data and proper messages when the wallet provider is not present in the browser or when it is disconnected and so on. Here's how the complete code would come out as. In case looking for the CSS, find the same here. Run the following code:

import React, { useState, useEffect } from "react";
import { ethers } from "ethers";
import "./style.css";

const contractABI = require("./Counter.json");
const YOUR_CONTRACT_ADDRESS = "0xf9e08779375Be47E3109ED8bbC700619B82361dc";

export default function App() {
  const [count, setCount] = useState(0);
  const [loading, setLoading] = useState(true);
  const [metaMaskEnabled, setMetaMaskEnabled] = useState(false);

  let getContract = () => {
    const provider = new ethers.providers.Web3Provider(window.ethereum);
    const signer = provider.getSigner();
    let contract = new ethers.Contract(
      YOUR_CONTRACT_ADDRESS,
      contractABI.abi,
      signer
    );
    return contract;
  };

  let incrementCount = async () => {
    const tx = await getContract().increment();
    alert("Once block is mined, Value will be auto updated");
    // await tx.wait();
    // fetchCurrentValue();
  };

  let fetchCurrentValue = async () => {
    let count_ = await getContract().getCount();
    console.log(+count_.toString());
    setCount(count_.toString());
    setLoading(false);
  };

  const checkedWallet = async () => {
    try {
      const { ethereum } = window;
      if (!ethereum) {
        alert("Get MetaMask!");
        setMetaMaskEnabled(false);
        return;
      }

      // Change network to rinkeby
      await ethereum.enable();
      const accounts = await ethereum.request({
        method: "eth_requestAccounts",
      });
      await ethereum.request({
        method: "wallet_switchEthereumChain",
        params: [{ chainId: `0x${Number(4).toString(16)}` }],
      });
      console.log("Connected", accounts[0]);
      localStorage.setItem("walletAddress", accounts[0]);
      setMetaMaskEnabled(true);

      // Listen to event
      listenToEvent();

      // Fetch the current counter value
      fetchCurrentValue();
    } catch (error) {
      console.log(error);
      setMetaMaskEnabled(false);
    }
  };

  useEffect(() => {
    checkedWallet();
    if (window.ethereum) {
      window.ethereum.on("chainChanged", () => {
        window.location.reload();
      });
      window.ethereum.on("accountsChanged", () => {
        checkedWallet();
      });
    }
  }, []);

  let listenToEvent = async () => {
    getContract().on("counterIncremented", async (sender, value, event) => {
      // Called when anyone changes the value
      setCount(+value.toString());
    });
  };

  return (
    <div class="root">
      {!metaMaskEnabled && <h1>Connect to Metamask</h1>}
      {metaMaskEnabled && (
        <div>
          {!loading && (
            <div>
              <h1 class="text">{count}</h1>
              <button onClick={incrementCount} class="button">
                +
              </button>
            </div>
          )}
          {loading && <div class="loader"></div>}
        </div>
      )}
    </div>
  );
}

Run the code

That's it!!! In order to run your DApp, just execute the following command:

npm start

You can find the full source code here

What next?

We started with blockchain and the underlying mechanism of how it works, wrote our first smart contract and then we also saw how to interact with a smart contract on the web frontend. In the next article, we will start with real-life use cases of blockchain through a case study.

This article is part of Research & Development work being done by Pushkar Kumar, Suresh Konakanchi, and Ruchika Gupta. We will be covering a series of articles along with open source projects around blockchain, smart contracts, and web3 in general. Here is the list of all the articles that you can follow to start with Blockchain and write your first contract: