17
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.
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.
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.
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')
);
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.
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
.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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