paint-brush
How to Integrate Smart Contracts to the Frontendby@ileolami
1,500 reads
1,500 reads

How to Integrate Smart Contracts to the Frontend

by IleolamiSeptember 13th, 2024
Read on Terminal Reader
Read this story w/o Javascript

Too Long; Didn't Read

Learn how to build the user interface(UI) for the Coffee Payment using, React.js, Typescript and Web3.js. The last article taught you how to write, compile, test and deploy smart contracts using Solidity, Javascript, dRPC endpoint, and API key.
featured image - How to Integrate Smart Contracts to the Frontend
Ileolami HackerNoon profile picture

Introduction

The last article taught you how to write, compile, test, and deploy smart contracts using Solidity, Javascript, dRPC endpoint, and API key.

Here, you'll learn how to build the user interface(UI) for the Coffee Payment using, React.js, Typescript, and Web3.js.

Tool and Technology

  1. React.js and Typescript
  2. Web3.js
  3. TailwindCSS for styling
  4. ThirdWeb SDK and API Key
  5. Contract Address
  6. Contract ABI
  7. MetaMask

Prerequisites

You know how to use CSS frameworks, React.js, and typescript.

Creating the ConnectWallet Button Using Thirdweb ConnectWallet SDk

Thirdweb is a web3 development platform that provides developers with SDKs, tools, and resources to simplify the process. For this article, you will be using the ConnectWallet SDK.


Before you get your hands dirty, it's important to understand why this process is crucial. A wallet must be connected before a user can interact with any dApp because the wallet serves as the user's identity and holds the necessary funds for transactions. This connection ensures that the user can securely and seamlessly use the smart contract features provided by the dApp.


Remember, you used the private key from your wallet to deploy the smart contract.


The role of thirdweb here is to simplify the writing of a long bunch of code; all you need is just to import some functions and a few lines of code.


  1. Install thirdweb using this command prompt.
npm i thirdweb  


  1. Wrap import The Thirdweb provider around your application inside the main.tsx
import { StrictMode } from 'react'  
import { createRoot } from 'react-dom/client'  
import App from './App.tsx'  
import './index.css'  
import { ThirdwebProvider } from "thirdweb/react"; //import ThirdwebProvider  
createRoot(document.getElementById('root')!).render(  
  <StrictMode>  
     <ThirdwebProvider>  
      <App />  
    </ThirdwebProvider>  
  </StrictMode>  
)  
  1. Create a new file, and name it connectWallet.tsx under src folder.
  2. Add the following code to the connectWallet.tsx
import { createThirdwebClient } from "thirdweb";

import { ConnectButton } from "thirdweb/react";

const client = createThirdwebClient({ clientId: import.meta.env.VITE_CLIENT_ID});

export default function ConnectBtn() {
  return (
  
      <ConnectButton client={client} />
   
  );
}

You will notice I use a client ID stored in .env file, the is because you need an API key to use the connectWallet SDK.


  1. Import the connectBtn function to the App.tsx file to view it

You will be able to connect any wallet of choice to any dApp.

Congratulations, you’ve successfully created a Connect Wallet button. Learn more on Thirdweb.

Creating State Variable and Functions to Load web3, dRPC, and Contract

Here, you will be creating the state variable needed and functions to load web3 etc.

For now all your action will be carry out inside the App.tsx, you should have clear out the file by now.

State Variables

  1. Import the ABI, web3(js framework for interacting with smart contract), useState, and useEffect.

if you have been following through from the deployment article, you’ve already installed web3.js, check your package.json for confirmation

import { useState, useEffect } from "react";  
import Web3 from "web3";  
import ABI from "../artifacts/contracts/coffee.sol/Coffee.json";  
import ConnectBtn from "./connectWallet";  
  1. Declaring the State variables
  // State variables for amount, totalCoffeesSold, totalEtherReceived, coffeePrice and ethToUsdRate  
  const [amount, setAmount] = useState(0);  
  const [totalCoffeesSold, setTotalCoffeesSold] = useState<number | null>(null);  
  const [totalEtherReceived, setTotalEtherReceived] = useState<number | null>(null);  
  const [coffeePrice, setCoffeePrice] = useState(0);  
  const [ethToUsdRate, setEthToUsdRate] = useState(0);  
  const [accountBalance, setAccountBalance] = useState(0);  
  • amount: used to store the number of coffees being purchased.
  • totalCoffeesSold: Stores the total number of coffees sold.
  • totalEtherReceived: Stores the total amount of Ether received from coffee sales.
  • coffeePrice: Stores the price of the coffee in Ether.
  • ethToUsdRate: Stores the current exchange rate from Ether to USD.
  • accountBalance: Stores the user's account balance in Ether.

Functions to Load web3, RPC, and Contract

// Function to load web3 and contract
  const RPC = new Web3(`https://lb.drpc.org/ogrpc?network=sepolia&dkey=${import.meta.env.VITE_dRPC_API_KEY}`);
  const web3 = new Web3(window.ethereum)
  const contractAddress = "0xC8644fA354D7c2209cB6a9DFd9c6d18e899B8D97";
  const contract = new web3.eth.Contract(ABI.abi, contractAddress);

Breakdown:

  1. const RPC = creates a new Web3 instance using a dPRC endpoint which contains URL and API key.
  2. const web3 = creates another Web3 instance using metamask's provider.window.ethereum: This allows the application to interact with the user's wallet.
  3. const contractAddress = the Ethereum address where the smart contract is deployed.
  4. const contract = creates a new contract instance to interact with the smart contract.

You will notice that you used two Web3 instance RPC and web3. This is because RPC Provider can only be used to make calls while “Window.ethereum” can be used for both calls and make transaction.

Creating the Pay Coffee Function

  // Function to pay coffee from buyer account
  const buyCoffee = async () => {
    const accounts = await web3.eth.getAccounts();
    await contract.methods.buyCoffee(amount).send({ from: accounts[0] });
  };

This asynchronous function allows the user to pay for the number of coffees bought.

  • const accounts Retrieved all the Ethereum accounts available in the user's wallet.
  • await contract.methods.buyCoffee(amount).send({ from: accounts[0] }): calls the buyCoffeemethod of the smart contract to purchase coffee.contract.methods.buyCoffee(amount): Accesses the buyCoffee function of the smart contract set it argument to the value of the input field where the user will enter the number of coffee bought.
  • .send({ from: accounts[0] }): Sends a transaction from the first account in the accounts array to execute the buyCoffee method on the smart contract.

The HTML syntax will look like this:

 <input
            type="number"
            value={amount}
            onChange={(e) => setAmount(parseInt(e.target.value))}
            className="border bg-transparent rounded p-2 mb-4 w-full outline-none [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
          />
          <button
            onClick={buyCoffee}
            className="bg-yellow-900 bg-opacity-35 text-white font-bold py-2 px-4 rounded w-full hover:bg-yellow-700 transition duration-300"
          >
            Buy Coffee
          </button>

Getting the Coffee Price, Total Coffee Sold and Ether Received


   // Function to fetch total coffees sold
  useEffect(() => {
    const fetchTotalCoffeesSold = async () => {
      try {
        const total = (await contract.methods.getTotalCoffeesSold().call()) as number;
        setTotalCoffeesSold(Number(total));
      } catch (error) {
        console.error('Error fetching total coffees sold:', error);
      }
    };
    fetchTotalCoffeesSold();
  }, []);
  
  
  // Function to fetch total ether received
  useEffect(() => {
    const getTotalEther = async () => {
      try {
        const total = (await contract.methods.getTotalEtherReceived().call()) as number;
        setTotalEtherReceived(Number(web3.utils.fromWei(total, 'ether')));
      } catch (error) {
        console.error('Error fetching total ether received:', error);
      }
    }; getTotalEther();
  })
  
  // Function to fetch coffee price
  useEffect(() => {
    const fetchCoffeePrice = async () => {
      try {
        const price = await contract.methods.coffeePrice().call();
        const priceInEther = web3.utils.fromWei(Number(price), 'ether');
        setCoffeePrice(Number(priceInEther));
      } catch (error) {
        console.error('Error fetching coffee price:', error);
      }
    };
    fetchCoffeePrice();
  }, []);

Each function here is calling the right function from the contract using the contract.methods.FunctionName().call()and converting the return value price to ether using the web3.utils.fromWei(Number(price), ‘ether’.


The HTML will look like this:

            <p className="text-lg mb-2 flex justify-between">Amount of coffees sold: <span className="font-semibold">{totalCoffeesSold}</span></p>
            <p className="text-lg mb-2 flex justify-between">Total ether received: <span className="font-semibold">{totalEtherReceived} Eth </span></p>
            <p className="text-lg mb-4 flex justify-between">Coffee price: <span className="font-semibold">{coffeePrice} Eth </span></p>

Querying the User Account Balance

To get the user Account balance, you will be using the dRPC provider.

Can you remember why? if you know the answer, drop it in the comment section

useEffect(() => {
    const getAccountBalance = async () => {
      const accounts = await web3.eth.getAccounts();
      const balance = await RPC.eth.getBalance(accounts[0]);
      setAccountBalance(Number(Number(web3.utils.fromWei(balance, 'ether')).toFixed(4)));
    };
    getAccountBalance();
  })

This approach leverages web3 to interact with the Account for account retrieval and RPC for querying the balance because you need the web3 to query the account connected.


The HTML looks like this:

<p className="text-lg mb-2 flex justify-between">Your balance: <span className="font-semibold">{accountBalance} Eth  </span></p>

Extra Features

I decided to add two extra features here which are:

  1. Convert the ETH to USD using the CoinGecko API(the conversion rate will be in the current USD price).
  2. Ensuring the user's wallet is connected to Sepolia Network before interacting with the dApp.

Convert the ETH to USD Using the CoinGecko API

 // Function to fetch the current price of ETH in USD using Coingecko API
    useEffect(() => {
      const fetchEthToUsdRate = async () => {
        try {
          const response = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd');
          const data = await response.json();
          setEthToUsdRate(data.ethereum.usd);
        } catch (error) {
          console.error('Error fetching ETH to USD rate:', error);
        }
      };
      fetchEthToUsdRate();
    }, []);


 // variable` to display coffee price in USD
  const coffeePriceInUsd = (coffeePrice * ethToUsdRate).toFixed(2);

  // variable to display account balance in USD
  const accountBalanceInUsd = (accountBalance * ethToUsdRate).toFixed(2);

  // variable to display total ether received in USD
  const totalEtherReceivedInUsd = ((totalEtherReceived ?? 0) * Number(ethToUsdRate)).toFixed(2);

Required Connect Network Validity

  const checkNetwork = async () => {
    const networkId = await web3.eth.net.getId();
    if (BigInt(networkId) !== BigInt(11155111)) { // Sepolia network ID
      alert('Please switch to the Sepolia network');
    }
  };

  useEffect(() => {
    checkNetwork();
  }, []);

By doing so, the User will know that they need to connect their wallet to the Sepolia network before interacting with the dApp.

The Entire CodeBase

import { useState, useEffect } from "react";
import Web3 from "web3";
import ABI from "../artifacts/contracts/coffee.sol/Coffee.json";
import ConnectBtn from "./connectWallet";

const App = () => {

  // State variables for amount, totalCoffeesSold, totalEtherReceived, coffeePrice and ethToUsdRate
  const [amount, setAmount] = useState(0);
  const [totalCoffeesSold, setTotalCoffeesSold] = useState<number | null>(null);
  const [totalEtherReceived, setTotalEtherReceived] = useState<number | null>(null);
  const [coffeePrice, setCoffeePrice] = useState(0);
  const [ethToUsdRate, setEthToUsdRate] = useState(0);
  const [accountBalance, setAccountBalance] = useState(0);
  

// Function to load web3 and contract
  const RPC = new Web3(`https://lb.drpc.org/ogrpc?network=sepolia&dkey=${import.meta.env.VITE_dRPC_API_KEY}`);
  const web3 = new Web3(window.ethereum)
  const contractAddress = "0xC8644fA354D7c2209cB6a9DFd9c6d18e899B8D97";
  const contract = new web3.eth.Contract(ABI.abi, contractAddress);


// Function to check if connected to Sepolia network
  const checkNetwork = async () => {
    const networkId = await web3.eth.net.getId();
    if (BigInt(networkId) !== BigInt(11155111)) { // Sepolia network ID
      alert('Please switch to the Sepolia network');
    }
  };

  useEffect(() => {
    checkNetwork();
  }, []);

  
  // Function to fetch the current price of ETH in USD using Coingecko API
    useEffect(() => {
      const fetchEthToUsdRate = async () => {
        try {
          const response = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd');
          const data = await response.json();
          setEthToUsdRate(data.ethereum.usd);
        } catch (error) {
          console.error('Error fetching ETH to USD rate:', error);
        }
      };
      fetchEthToUsdRate();
    }, []);


  //Function to show user account balance
  useEffect(() => {
    const getAccountBalance = async () => {
      const accounts = await web3.eth.getAccounts();
      const balance = await RPC.eth.getBalance(accounts[0]);
      setAccountBalance(Number(Number(web3.utils.fromWei(balance, 'ether')).toFixed(4)));
    };
    getAccountBalance();
  })

  
  // Function to fetch total coffees sold
  useEffect(() => {
    const fetchTotalCoffeesSold = async () => {
      try {
        const total = (await contract.methods.getTotalCoffeesSold().call()) as number;
        setTotalCoffeesSold(Number(total));
      } catch (error) {
        console.error('Error fetching total coffees sold:', error);
      }
    };
    fetchTotalCoffeesSold();
  }, []);
  
  
  // Function to fetch total ether received
  useEffect(() => {
    const getTotalEther = async () => {
      try {
        const total = (await contract.methods.getTotalEtherReceived().call()) as number;
        setTotalEtherReceived(Number(web3.utils.fromWei(total, 'ether')));
      } catch (error) {
        console.error('Error fetching total ether received:', error);
      }
    }; getTotalEther();
  })
  
  // Function to fetch coffee price
  useEffect(() => {
    const fetchCoffeePrice = async () => {
      try {
        const price = await contract.methods.coffeePrice().call();
        const priceInEther = web3.utils.fromWei(Number(price), 'ether');
        setCoffeePrice(Number(priceInEther));
      } catch (error) {
        console.error('Error fetching coffee price:', error);
      }
    };
    fetchCoffeePrice();
  }, []);

  
  // Function to pay coffee from buyer account
  const buyCoffee = async () => {
    const accounts = await web3.eth.getAccounts();
    await contract.methods.buyCoffee(amount).send({ from: accounts[0] });
  };
  
  
  // variable` to display coffee price in USD
  const coffeePriceInUsd = (coffeePrice * ethToUsdRate).toFixed(2);

  // variable to display account balance in USD
  const accountBalanceInUsd = (accountBalance * ethToUsdRate).toFixed(2);

  // variable to display total ether received in USD
  const totalEtherReceivedInUsd = ((totalEtherReceived ?? 0) * Number(ethToUsdRate)).toFixed(2);

  return (
    <div
    className="min-h-screen flex flex-col"
    style={{
      backgroundImage: "url('https://thumbs.dreamstime.com/b/coffee-background-space-text-85121087.jpg')",
      backgroundSize: 'cover',
      backgroundPosition: 'center',
    }}
  >
      <header className=" text-white py-4 shadow-md flex justify-between items-center px-8">
        <h1 className="text-3xl font-bold">Coffee Store</h1>
        <ConnectBtn />
      </header>
      <main className="flex-grow flex items-center justify-center p-4">
        <div className="bg-white bg-opacity-50 shadow-lg rounded-lg p-8 max-w-md w-full transform transition duration-500 hover:scale-105">
          <div className="text-center mb-6">
            <h2 className="text-2xl font-semibold mb-2">Welcome to the Coffee Store</h2>
            <p className="text-lg">Pay for your favorite coffee with Ether</p>
          </div>
          <div className="mb-4">
            <p className="text-lg mb-2 flex justify-between">Your balance: <span className="font-semibold">{accountBalance} Eth (${accountBalanceInUsd}) </span></p>
            <p className="text-lg mb-2 flex justify-between">Amount of coffees sold: <span className="font-semibold">{totalCoffeesSold}</span></p>
            <p className="text-lg mb-2 flex justify-between">Total ether received: <span className="font-semibold">{totalEtherReceived} Eth (${totalEtherReceivedInUsd})</span></p>
            <p className="text-lg mb-4 flex justify-between">Coffee price: <span className="font-semibold">{coffeePrice} Eth (${coffeePriceInUsd})</span></p>
          </div>
          <input
            type="number"
            value={amount}
            onChange={(e) => setAmount(parseInt(e.target.value))}
            className="border bg-transparent rounded p-2 mb-4 w-full outline-none [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
          />
          <button
            onClick={buyCoffee}
            className="bg-yellow-900 bg-opacity-35 text-white font-bold py-2 px-4 rounded w-full hover:bg-yellow-700 transition duration-300"
          >
            Buy Coffee
          </button>
        </div>
      </main>
    </div>
  );
}

export default App;


Your UI should look like this

If you have been following from the first article to this, congratulations 🥳and well done 👍🏽.

Conclusion

From the previous articles, you have been able to understand the core tech stack for web3 development, how to deploy a smart contract and create a UI for it, so feel free to call yourself a WEB3 DEVELOPER.

You can access the live site here

Live demo - https://www.youtube.com/watch?v=ZZNIbLWckDY

The Github repo here


Previous Article