Building a frontend react app for Dapp from scratch: Part (3/4)

This part is the primary reason that motivated me to write this tutorial series in the first place. Tutorials on solidity were easy to find and they were good enough, but the problem was finding a good tutorial on how to build a frontend, how it interacts with the deployed smart contract and how to host it on cloud platforms like heroku.

This section requires you to have some basic understanding of react such as components, useState, useEffect, etc. There are a ton of good resources on react, so please check them out if you are not familiar with these concepts.

This project is hosted here.
Link to Github Repo of the project here. This will serve as a reference for you, if at some point you are not able to follow the tutorial.

Creating a react app

In the root directory, run

npx create-react-app frontend

This will create a react app named frontend.

npm install @alch/alchemy-web3
npm install react-bootstrap [email protected]
npm install dotenv
npm install react-icons --save

This will install all the necessary packages for our project.

Alchemy Web3 is a wrapper around Web3.js which provides some enhanced API methods.

Setup

create-react-app initiates with some boilerplate code when we can get rid off.

Delete App.test.js logo.svg reportWebVitals.js, and setupTests.js from the src folder.

And make changes to your index.js and App.js file to look like this.

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

App.js

import './App.css'

function App() {
  return (
    <>
    </>
  )
}

export default App

Now, go to frontend/public/index.html and add bootstrap CDN into the head tag.

<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">

Create a .env file in the frontend directory and add your Alchemy API Key which we used in the previous section.

REACT_APP_ALCHEMY_KEY = <YOUR_API_KEY>

Now, we are all set to building our project.

Getting artifacts

We need certain data about our deployed smart contract in the frontend in order to interact with it. In order to get that data we'll modify our deployment script at SimpleBank/scripts/deploy.js.

deploy.js

const { ethers, artifacts } = require('hardhat')

async function main() {
  const [deployer] = await ethers.getSigners()

  console.log('Deploying contracts with the account: ', deployer.address)

  const Bank = await ethers.getContractFactory('Bank')
  const bank = await Bank.deploy()

  console.log('Bank address: ', bank.address)

  saveArtifacts(bank)
}

// save the address and artifact of the deployed contract in the frontend
const saveArtifacts = (bank) => {
  const fs = require('fs')
  const artifactDir = __dirname + '/../frontend/src/artifacts'

  if (!fs.existsSync(artifactDir)) {
    fs.mkdirSync(artifactDir)
  }

  const bankArtifact = artifacts.readArtifactSync('Bank')

  const artifact = {
    address: bank.address,
    abi: bankArtifact.abi,
  }

  console.log('Saving artifacts to: ', artifactDir)

  fs.writeFileSync(artifactDir + '/Bank.json', JSON.stringify(artifact))
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error)
    process.exit(1)
  })

We have added a new function saveArtifacts() and calling it at the end of main will save the deployed contract address and abi to frontend/src/artifacts when the contract is deployed.

We'll need to deploy our contract once again to get this data.

npx hardhat run .\scripts\deploy.js --network rinkeby

This should create a artifacts folder in frontend/src with a Bank.json file.

Utility Functions

This is the part were we'll write the code to interact with our deployed smart contract.

Create a folder in frontend/src called utils and add the following files into it. This is where we'll write all our functions.

walletFunctions.js

export const connectWallet = async () => {
  if (window.ethereum) {
    try {
      const addresses = await window.ethereum.request({
        method: 'eth_requestAccounts',
      })
      const obj = {
        address: addresses[0],
        connected: true,
        status: '',
      }
      return obj
    } catch (error) {
      return {
        address: '',
        connected: false,
        status: error.message,
      }
    }
  } else {
    return {
      address: '',
      connected: false,
      status: (
        <a href="https://metamask.io/" target="_blank" rel="noreferrer">
          {' '}
          You need to install Metamask
        </a>
      ),
    }
  }
}

export const getWalletStatus = async () => {
  if (window.ethereum) {
    try {
      const addresses = await window.ethereum.request({
        method: 'eth_requestAccounts',
      })
      if (addresses.length > 0) {
        return {
          address: addresses[0],
          connected: true,
          status: '',
        }
      } else {
        return {
          address: '',
          connected: false,
          status: '🦊 Please connect to Metamask Wallet',
        }
      }
    } catch (error) {
      return {
        address: '',
        connected: false,
        status: error.message,
      }
    }
  } else {
    return {
      address: '',
      connected: false,
      status: (
        <a href="https://metamask.io/" target="_blank" rel="noreferrer">
          {' '}
          You need to install Metamask
        </a>
      ),
    }
  }
}

Here we have two functions, both are quite similar so I'll try to explain the key parts of both of them together.

if (window.ethereum) {
    //does someting
} else {
    return {
      address: '',
      connected: false,
      status: (
        <a href="https://metamask.io/" target="_blank" rel="noreferrer">
          {' '}
          You need to install Metamask
        </a>
      ),
    }
  }

Here, window.ethereum checks if the browser has a wallet. If yes, it proceeds else we return an instruction to install metamask wallet.

try {
      const addresses = await window.ethereum.request({
        method: 'eth_requestAccounts',
      })
      const obj = {
        address: addresses[0],
        connected: true,
        status: '',
      }
      return obj
    } catch (error) {
      return {
        address: '',
        connected: false,
        status: error.message,
      }
    }

Now, if there is a wallet available, we use const addresses = await window.ethereum.request({ method: 'eth_requestAccounts', }) to request the wallet to connect an account to the app. If the connection was successful we get all the connected addresses and we return the first one, else we return the error message.

The getWalletStatus function also works pretty much the same. It checks the accounts that are connected to the wallet, if none it responds with request to connect.

bankFunctions.js

const alchemyKey = process.env.REACT_APP_ALCHEMY_KEY
const { createAlchemyWeb3 } = require('@alch/alchemy-web3')
const web3 = createAlchemyWeb3(alchemyKey)

const { abi, address } = require('../artifacts/Bank.json')

export const depositEth = async (amount) => {
  if (parseFloat(amount) <= 0) {
    return {
      status: 'Please enter a valid amount',
    }
  }

  window.contract = await new web3.eth.Contract(abi, address)

  const WeiAmount = web3.utils.toHex(web3.utils.toWei(amount, 'ether'))

  const txParams = {
    to: address,
    from: window.ethereum.selectedAddress,
    value: WeiAmount,
    data: window.contract.methods.deposit().encodeABI(),
  }

  try {
    await window.ethereum.request({
      method: 'eth_sendTransaction',
      params: [txParams],
    })
    return {
      status: 'Transaction Successful. Refresh in a moment.',
    }
  } catch (error) {
    return {
      status: 'Transaction Failed' + error.message,
    }
  }
}

export const withdrawEth = async (amount) => {
  window.contract = await new web3.eth.Contract(abi, address)

  const WeiAmount = web3.utils.toWei(amount, 'ether')

  const txParams = {
    to: address,
    from: window.ethereum.selectedAddress,
    data: window.contract.methods.withdraw(WeiAmount).encodeABI(),
  }

  try {
    await window.ethereum.request({
      method: 'eth_sendTransaction',
      params: [txParams],
    })
    return {
      status: 'Transaction Successful. Refresh in a moment',
    }
  } catch (error) {
    return {
      status: 'Transaction Failed' + error.message,
    }
  }
}

export const getBalance = async () => {
  window.contract = await new web3.eth.Contract(abi, address)

  const reqParams = {
    to: address,
    from: window.ethereum.selectedAddress,
    data: window.contract.methods.getBalance().encodeABI(),
  }

  try {
    const response = await window.ethereum.request({
      method: 'eth_call',
      params: [reqParams],
    })
    const exhRate = await exchangeRate()
    const balance = web3.utils.fromWei(response, 'ether')
    return {
      inr: balance * exhRate,
      eth: balance,
      exhRate: exhRate,
    }
  } catch (error) {
    return {
      status: 'Check Failed ' + error.message,
    }
  }
}

export const exchangeRate = async () => {
  const response = await fetch(
    'https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=inr',
  )
  const data = await response.json()
  return data.ethereum.inr
}

These are the functions that will handle app the bank features.

const alchemyKey = process.env.REACT_APP_ALCHEMY_KEY
const { createAlchemyWeb3 } = require('@alch/alchemy-web3')
const web3 = createAlchemyWeb3(alchemyKey)

const { abi, address } = require('../artifacts/Bank.json')

Firstly, we'll use out API Key to initialize our web3 instance and import address and abi from artifacts/Bank.json.

if (parseFloat(amount) <= 0) {
    return {
      status: 'Please enter a valid amount',
    }
}

Then we have the depositEth function, which will be used to deposit amount. amount parameter will be in string format, so we convert it to float and ensure it is greater than 0.

window.contract = await new web3.eth.Contract(abi, address)

Here we get an instance of our contract using abi and address.

const WeiAmount = web3.utils.toHex(web3.utils.toWei(amount, 'ether'))

We convert the received parameter to Wei, which is the smallest denomination of ether ( 1 ether = 10^18 Wei). Then we convert it into Hex value which we'll use as a transaction parameter.

const txParams = {
    to: address,
    from: window.ethereum.selectedAddress,
    value: WeiAmount,
    data: window.contract.methods.deposit().encodeABI(),
  }

Transaction parameters include to: <contract_address>, from: <address_of_account_connected_to_app>, value: <amount_to_be_deposited>, data: <call_to_contract_function>.

try {
    await window.ethereum.request({
      method: 'eth_sendTransaction',
      params: [txParams],
    })
    return {
      status: 'Transaction Successful. Refresh in a moment.',
    }
  } catch (error) {
    return {
      status: 'Transaction Failed' + error.message,
    }
  }
}

Finally, we send the transaction. If successful we return a success message else we return the error message.

withdrawEth function as the name suggests is for transferring back the amount from bank to wallet. Its almost same are earlier, just instead of sending amount as a transaction parameter we'll send it as a parameter to the call function.

getBalance function return the balance available for the amount which calls it. The major difference here is that we use method: 'eth_call' since it is just a view function. We also use a exchangeRate function which is a simple fetch request to an api to get the current exchange rate of ETH to INR and return balance in both format.

Components & CSS

Create a components folder in frontend/src and add these components to it.

These components are build pretty basic with some bootstrap elements and CSS is minimal, so we won't into too much detail for them.

NavBar.jsx

import { Container, Navbar } from 'react-bootstrap'

export default function NavBar() {
  return (
    <div>
      <Navbar bg="dark" variant="dark">
        <Container>
          <Navbar.Brand>SimpleBank & Co.</Navbar.Brand>
        </Container>
      </Navbar>
    </div>
  )
}

This is a simple navbar with a brand which we'll use in our app.

StatusBox.jsx

import { Alert, Container, Row, Col } from 'react-bootstrap'

const StatusBox = ({ status }) => {
  return (
    <Container
      className={status.length === 0 ? 'status-box-null' : 'status-box'}
    >
      <Row className="justify-content-center">
        <Col lg="6">
          <Alert variant="danger">{status}</Alert>
        </Col>
      </Row>
    </Container>
  )
}

export default StatusBox

This status bar will be used to inform the user whenever anything goes wrong.

BankInfo.jsx

import { IoIosRefresh } from 'react-icons/io'
import {
  Button,
  Container,
  FormControl,
  InputGroup,
  Col,
  Row,
  Alert,
} from 'react-bootstrap'
import { useState, useEffect } from 'react'

import { getBalance, depositEth, withdrawEth } from '../utils/bankFunctions'

const BankInfo = ({ onAccoutChange }) => {
  const [balanceINR, setBalanceINR] = useState(0)
  const [balanceETH, setBalanceETH] = useState(0)
  const [showDeposit, setShowDeposit] = useState(false)
  const [showWithdraw, setShowWithdraw] = useState(false)
  const [exhRate, setExhRate] = useState(0)
  const [inputINR, setInputINR] = useState(null)
  const [inputETH, setInputETH] = useState(null)
  const [response, setResponse] = useState(null)

  const handleShowDeposit = () => {
    setShowDeposit(true)
  }

  const handleShowWithdraw = () => {
    setShowWithdraw(true)
  }

  const handleClose = () => {
    setShowDeposit(false)
    setShowWithdraw(false)
    setInputINR(null)
    setInputETH(null)
    setResponse(null)
  }

  const checkBalance = async () => {
    const balance = await getBalance()
    setBalanceETH(balance.eth)
    setBalanceINR(balance.inr)
    setExhRate(balance.exhRate)
  }

  const handleInoutINR = (e) => {
    setInputINR(e.target.value)
    setInputETH((e.target.value / exhRate).toFixed(18))
  }

  const handleDeposit = async () => {
    setResponse(null)
    const deposit = await depositEth(inputETH.toString())
    setInputETH(null)
    setInputINR(null)
    setResponse(deposit.status)
  }

  const handleWithdraw = async () => {
    if (inputINR > balanceINR) {
      setResponse('Insufficient Balance')
    } else {
      setResponse(null)
      const withdraw = await withdrawEth(inputETH.toString())
      setInputETH(null)
      setInputINR(null)
      setResponse(withdraw.status)
    }
  }

  useEffect(() => {
    checkBalance()
  }, [onAccoutChange])

  return (
    <>
      <div className="balance-card">
        <h1>
          Your Balance
          <IoIosRefresh className="refresh-icon" onClick={checkBalance} />
        </h1>
        <h3 className="balance-inr">{parseFloat(balanceINR).toFixed(2)} INR</h3>
        <h3 className="balance-eth">{parseFloat(balanceETH).toFixed(4)} ETH</h3>
        {!showDeposit && !showWithdraw && (
          <div className="btn-grp">
            <Button
              className="deposit-btn"
              variant="success"
              onClick={handleShowDeposit}
            >
              Deposit
            </Button>
            <Button
              className="withdraw-btn"
              variant="warning"
              onClick={handleShowWithdraw}
            >
              Withdraw
            </Button>
          </div>
        )}
        {showDeposit || showWithdraw ? (
          <>
            <Container>
              <Row className="justify-content-center ">
                <Col md="6">
                  <InputGroup className="amount-input">
                    <FormControl
                      placeholder="Enter Amount in INR"
                      type="number"
                      value={inputINR > 0 ? inputINR : ''}
                      onChange={handleInoutINR}
                    />
                    <InputGroup.Text>INR</InputGroup.Text>
                  </InputGroup>
                </Col>
              </Row>
              <Row className="justify-content-center">
                <Col md="6">
                  <InputGroup className="amount-input">
                    <FormControl
                      placeholder="ETH Equivalent"
                      type="number"
                      value={inputETH > 0 ? inputETH : ''}
                      readOnly
                    />
                    <InputGroup.Text>ETH</InputGroup.Text>
                  </InputGroup>
                </Col>
              </Row>
            </Container>
            <div className="btn-grp">
              <Button
                className="deposit-btn"
                variant="success"
                onClick={showDeposit ? handleDeposit : handleWithdraw}
              >
                {showDeposit ? 'Deposit' : 'Withdraw'}
              </Button>
              <Button
                className="withdraw-btn"
                variant="info"
                onClick={handleClose}
              >
                Close
              </Button>
            </div>
            {response && (
              <Container>
                <Row className="justify-content-center">
                  <Col md="6">
                    <Alert variant="info">{response}</Alert>
                  </Col>
                </Row>
              </Container>
            )}
          </>
        ) : null}
      </div>
    </>
  )
}

export default BankInfo

This is the component that shows the account balance in INR and ETH and handles deposit and withdrawal. A bunch of state variables are used for smooth functioning of UI.

ConnectBtn.jsx

import { useState, useEffect } from 'react'
import { Button } from 'react-bootstrap'

import { connectWallet, getWalletStatus } from '../utils/walletFunctions'

export const ConnectBtn = ({ setStatus, setConnected, setWallet }) => {
  const [walletAddress, setWalletAddress] = useState('')

  const handleConnect = async () => {
    const walletResponse = await connectWallet()
    setStatus(walletResponse.status)
    setConnected(walletResponse.connected)
    setWalletAddress(walletResponse.address)
    setWallet(walletResponse.address)
  }

  useEffect(() => {
    const checkWalletStatus = async () => {
      const walletResponse = await getWalletStatus()
      setStatus(walletResponse.status)
      setConnected(walletResponse.connected)
      setWalletAddress(walletResponse.address)
      setWallet(walletResponse.address)
    }

    const walletListener = () => {
      if (window.ethereum) {
        window.ethereum.on('accountsChanged', (accounts) => {
          checkWalletStatus()
        })
      }
    }

    checkWalletStatus()
    walletListener()
  }, [setConnected, setStatus, setWallet])

  return (
    <div className="connect-btn">
      <Button variant="primary" onClick={handleConnect}>
        {walletAddress.length === 0
          ? 'Connet Wallet'
          : 'Connected: ' +
            String(walletAddress).substring(0, 6) +
            '...' +
            String(walletAddress).substring(38)}
      </Button>
    </div>
  )
}

export default ConnectBtn

ConnectBtn component displays the status of wallet connection to the app and also used to request connection when not connected.

Footer.jsx

import { IoIosInformationCircleOutline } from 'react-icons/io'

const Footer = () => {
  return (
    <footer className="footer">
      <p className="footer-text">
        <IoIosInformationCircleOutline className="info-icon" /> This application
        in running on rinkeby test network. Please only use test Ethers.
      </p>
    </footer>
  )
}

export default Footer

A simple footer component to warn the user to only use test ethers.

index.css

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
    monospace;
}

.amount-input {
  margin: 10px;
}

.balance-card {
  margin-top: 20px;
  text-align: center;
}

.balance-eth {
  color: rgb(19, 202, 28);
}

.balance-inr {
  color: rgb(25, 214, 214);
}

.btn-grp {
  margin: 20px;
}

.deposit-btn {
  margin-right: 20px;
}

.footer {
  position: fixed;
  left: 0;
  bottom: 0;
  width: 100%;
  padding: 5px;
  background-color: teal;
  color: white;
  text-align: center;
}

.footer-text {
  font-size: large;
}

.info-icon {
  font-size: x-large;
}

.withdraw-btn {
  margin-left: 20px;
}

.connect-btn {
  text-align: center;
  margin: 20px;
}

.refresh-icon {
  margin-left: 10px;
  font-size: 28px;
  cursor: pointer;
}

.status-box {
  margin-top: 20px;
  text-align: center;
}

.status-box-null {
  display: none;
}

Styling is nothing fancy, just to align all the components properly.

Adding all components to App.js

App.js

import { useState } from 'react'

import NavBar from './components/NavBar'
import ConnectBtn from './components/ConnectBtn'
import StatusBox from './components/StatusBox'
import BankInfo from './components/BankInfo'
import Footer from './components/Footer'

function App() {
  const [status, setStatus] = useState('')
  const [connected, setConnected] = useState()
  const [wallet, setWallet] = useState()
  return (
    <>
      <NavBar />
      <ConnectBtn
        setStatus={setStatus}
        setConnected={setConnected}
        setWallet={setWallet}
      />
      <StatusBox status={status} />
      {connected && <BankInfo onAccoutChange={wallet} />}
      <Footer />
    </>
  )
}

export default App

Add all the components to App.js in order to render them in our app.

This wraps up our react app. Run the following command to run it on localhost.

npm start

If things doesn't work as expected, checkout github repo here for reference.

In the next tutorial, we'll see how to host this app on heroku. Click here.

17