Lesson 5: Connecting to a Frontend
If you've been through the previous lessons we learnt how to write smart contracts, use token standards, and test driven development, as well. But to create a full stack decentralised application, we also need a frontend for users to interact with. In this lesson that is exactly what we're going to create.
We shall use:
- RainbowKit for out-of-the-box wallet management (i.e. Handling wallet connections to our application).
- WAGMI for checking connection to a wallet and calling functions from our smart contract through the frontend using the provided hooks.
and create a basic NFT minting application that displays the three tiers of NFTs created in our smart contract and gives users a one-click option to mint them. Once minted, users will be redirected to a page that displays information of their mint.
Let's dive in!
Getting started
In the past, you've created a smart contract using Remix, you've shifted to a dedicated code editor environment and deployed to an actual test network. In lesson 3 you added tiers to your NFTs and finally learnt to make your contracts airtight using tests in lesson 4. Now we're going use all of this knowledge and connect it to a frontend for users to see and interact with.
We're going to start where we left off in lesson 4. Open your code editor and
cd
into the TierNFT
project directory.
For all things related to our frontend we are going to create a new sub-directory within our project's root directory to maintain good practice, keep our code clean, and easy to locate and read.
The first step is to initiate a nextJS application inside the project's root directory by running the following command.
Make sure you are in your root directory before running the command.
The command creates a sub-directory named frontend
inside our root directory
and sets up all the necessary files and folders within it. The create-next-app
command
uses the yarn
package manager by default. But be careful...
There is a chance that a different package manager has been used for your previous project set up, so check that out and choose your command accordingly:
# default for yarn npx create-next-app frontend # possible alternative for npm npx create-next-app frontend --use-npm
Once the setup is complete, we open the frontend
directory and get cracking.
cd frontend
The Frontend
For users to easily interact with our smart contract and mint the NFTs we must provide an interface. A simple process that is easily accessible.
Let's start by creating a way to handle wallet connections.
The Main Application
We will use RainbowKit for handling
the wallet connections. It provides us the ability to handle wallet connections,
support for numerous wallets and a customisable ConnectButton
out-of-the-box among
other features. It relies on the WAGMI and ethers
libraries for interoperability between various chains and wallet providers and
thus needs some configurations.
But first we need to install the necessary dependencies.
yarn add @rainbow-me/rainbowkit wagmi ethers # or npm install @rainbow-me/rainbowkit wagmi ethers
To get this setup we need to start working on the _app.js
file in the /frontend/pages
directory. The file should look something like this from our initial setup.
import '../styles/globals.css' function MyApp({ Component, pageProps }) { return <Component {...pageProps} /> } export default MyApp
We need to add some additional imports to create our configurations.
// previous imports... import '@rainbow-me/rainbowkit/styles.css'; import { getDefaultWallets, RainbowKitProvider, } from '@rainbow-me/rainbowkit'; import { chain, configureChains, createClient, WagmiConfig, } from 'wagmi'; import { alchemyProvider } from 'wagmi/providers/alchemy'; import { publicProvider } from 'wagmi/providers/public'; import { useEffect, useState } from 'react'; // function MyApp code...
import '@rainbow-me/rainbowkit/styles.css';
gives us some basic styling from Rainbowkit.import { getDefaultWallets, RainbowKitProvider, } from '@rainbow-me/rainbowkit';
returns the default wallet provider option and a RainbowKit Provider to "wrap" around our app so that we can use its features throughout the app's pages and components.import { chain, configureChains, createClient, WagmiConfig, } from 'wagmi';
provides a way to configure the WagmiConfig wrapper for our app based on the chains and providers of our choice along with some other customisations.import { alchemyProvider } from 'wagmi/providers/alchemy';
andimport { publicProvider } from 'wagmi/providers/public';
give us the provider configs for the providers we will be using. In our app we are using analchemyProvider
along with a fall backpublicProvider
. A provider allows our application to communicate with a blockchain.useEffect
anduseState
are react hooks that help us perform side effects and capture the state, respectively. More on state and the useEffect hook.
Hooks are JavaScript functions that manage the state's behaviour and side effects by isolating them from a component.
After importing, we create the configurations we need, and create a new instance of
wagmiClient
using them.
// import statements code... const { chains, provider } = configureChains( [chain.polygonMumbai], [ alchemyProvider({ apiKey: process.env.API_KEY }), publicProvider() ] ); const { connectors } = getDefaultWallets({ appName: 'My RainbowKit App', chains }); const wagmiClient = createClient({ autoConnect: true, connectors, provider }) // function MyApp code...
For this application we will continue using the mumbai
testnet. Other chains can
be added simply using the following syntax: chain.chain_name
.
For custom providers, like the alchemyProvider
we can pass in our private
apiKey
as well.
Set your API Key in a new file named .env
inside our frontend
directory
as follows:
API_KEY='YOUR-API-KEY-FROM-PROVIDER'
Now we can wrap our application in the RainbowKitProvider
and WagmiConfig
so that have access to their features throughout our application. Our code should
look like this:
// import statements and configs code... function MyApp({ Component, pageProps }) { return ( <WagmiConfig client={wagmiClient}> <RainbowKitProvider chains={chains}> <Component {...pageProps} /> </RainbowKitProvider > </WagmiConfig > ); }; export default MyApp
Now that we have our application setup, we can edit the Index Page.
The Index Page
The index.js
page in the /frontend/pages
directory should look like this
after deleting all the children within the <main>
element and completely
deleting the <footer>
.
import Head from 'next/head' import Image from 'next/image' import styles from '../styles/Home.module.css' export default function Home() { return ( <div className={styles.container}> <Head> <title>Create Next App</title> <meta name="description" content="Generated by create next app" /> <link rel="icon" href="/favicon.ico" /> </Head> <main className={styles.main}> </main> </div> ) }
Before we dive into the code for our index page, let's copy and paste some styling from the side drawer below, into our file. This is some default styling created for the application, which you can play around with themes, responsiveness, etc., if needed.
Moving onto imports.
// prev imports... import { ConnectButton } from '@rainbow-me/rainbowkit'; import { useAccount, useContractRead, useContractWrite } from 'wagmi'; import TierABI from '../../artifacts/contracts/TierNFT.sol/TierNFT.json'; import styles from '../styles/Home.module.css'; import { ethers } from 'ethers'; import { useEffect, useState } from 'react'; // function Home() code...
- RainbowKit provides us a ready-made
ConnectButton
. - We need to use the
useAccount
hook from WAGMI to check account connection anduseContractRead
anduseContractWrite
to interact with our smart contract. We will look into WAGMI and these hooks a little down the line. TierABI
is our contract's Application Binary Interface (ABI). The ABI helps us interact with the smart contract outside the blockchain or contract-to-contract. You can read more about ABIs here.- The
ethers
library will assist us with some utilities in our code. - Once again,
useEffect
anduseState
are react hooks that will help us perform side effects and capture the state, respectively.
export default function Home() { const CONTRACT_ADDRESS = /*Enter Contract Address between ''*/; const { isConnected } = useAccount(); const [isUserConnected, setIsUserConnected] = useState(false); const [latestNFTMinted, setLatestNFTMinted] = useState(0); const [modalShow, setModalShow] = useState(false); const [isMinting, setIsMinting] = useState(false); // More code to come... }
We're creating a default function called Home
that will render our home/
index page. As you can see , we are required to add the CONTRACT_ADDRESS
which is displayed in your command line when we deploy the contract.
The useAccount
hook lets us access account data and check connection status.
We will use this as a check to provide access to the minting features of our
app.
A few useState
s let us store state information such as whether a user is
connected. The general syntax for using the useState
hook is:
const [variableName, functionToSetVariable] = useState(intialValue)
The initial value also serves as the type for the value assigned. If the initial value is a string, then the values set later must be strings too. The hook can be created without an initial value as well but a good practice is to always choose and pass in an initial value based on the value type we expect to assign to that variable.
We will revisit these state variables later to see their implementation.
Header
We need a <header>
that displays our application name and let's us
connect our wallet.
export default function Home() { // variable definitions... return ( <div className={styles.container}> {/* <Head> element... */} <header style={header}> <h1>TierNFTs</h1> <ConnectButton /> </header> {/* <main> element */} </div> ) } // styling code...
Since we care about accessibility, we ensure we are using only a single <h1>
element on a page and the correct sub heading elements as well. The ConnectButton
is
a direct import from RainbowKit
that is generated using the configurations
setup previously in our Main Application.
With this setup we can run the npm run dev
or yarn dev
command in our console
and click the returned link (http://localhost:portnumber
) to preview our application.
This is a good way to debug and see if our code is behaving as expected.
Make sure you are in the frontend directory before running the command.
The preview should look like this:
NFT Cards
Next we need to create some cards that display the 3 tiers of NFTs we created in our smart contract and provide a way to mint them.
export default function Home() { // variable definitions... const { data: mintData, writeAsync: mint, isLoading: isMintLoading, } = useContractWrite({ addressOrName: CONTRACT_ADDRESS, contractInterface: TierABI.abi, functionName: "mint", }); // return statement for rendering... }
The useContractWrite
hook
from WAGMI let's us pass in the contract address, contract ABI and the
function name to return a JavaScript function (that we have called mint
)
that can interact with our smart contract and trigger a write
function in it.
How cool is that?!
It also returns some additional features for error handling and checking
whether the function is running or completed.
We are assigning well defined names to the returned values for easily calling them
later. For example, the data
returned is called mintData
.
Even though we have this function, we need to pass in custom message values
i.e. an amount in ETH, as defined in our contract. For this we create our
own mintToken
function that calls the mint
function from WAGMI within it.
// WAGMI useContractWrite hook... const mintToken = async (e) => { try { let mintTxn = await mint({ recklesslySetUnpreparedOverrides: { value: ethers.utils.parseEther(e.target.value), } }); await mintTxn.wait(); console.log("This is the mint data", mintData); } catch (error) { console.log("Error minting NFT", error.message); } }; // function Home() code...
- We pass in
e
as an argument so that we can pick the amount of ETH associated with a particular Tier. This is passed to the mint function using therecklesslySetUnpreparedOverrides
override config. Read more about it here. - We want to wait for our minting transaction to complete before moving
on to other pieces of code so we use
wait()
. - After minting is processed we log the returned data to ensure the success of our transaction.
- Using
try...catch
statements makes our error handling more robust. The compiler runs each line of code and exits in between if an error occurs. We can console log this error to easily identify the cause and location of the error with clear comments.
Then we create a new folder named nfts
in the /public
directory for
saving Tier SVGs. You can find links for downloading the SVGs here.
Save them with the same names as shown in the links above for smooth flow in the code.
Make sure you have downloaded the SVG file and not just saved the URL of the file.
Now we can render our NFT Cards.
// return statement for rendering and <header> element... <main className={styles.main}> <div style={NFTFlex}> <div style={NFTCard}> Tier 0 <Image src='/nfts/0_basic.svg' width='200px' height='200px' /> <button value='0.01' onClick={(e) => mintToken(e)} style={NFTMint} disabled={isMintLoading}> Mint </button> </div> <div style={NFTCard} > Tier 1 <Image src='/nfts/1_medium.svg' width='200px' height='200px' /> <button value='0.02' onClick={(e) => mintToken(e)} style={NFTMint} disabled={isMintLoading}> Mint </button> </div> <div style={NFTCard} > Tier 2 <Image src='/nfts/2_premium.svg' width='200px' height='200px' /> <button value='0.05' onClick={(e) => mintToken(e)} style={NFTMint} disabled={isMintLoading}> Mint </button> </div> </div> </main> // rest of the code...
Let's see what is happening in each NFT Card by taking Tier 0
as an
example.
<div style={NFTCard}> Tier 0 <Image src='/nfts/0_basic.svg' width='200px' height='200px' /> <button value='0.01' onClick={(e) => mintToken(e)} style={NFTMint} disabled={isMintLoading}> Mint </button> </div>
- We start off by displaying the NFT Tier name.
- A nextJS
Image
element renders a preview of the NFT Image. Earlier, we saved our images in the/public/nfts
folder. A nextJS app automatically detects content from the/public
directory so we can directly reference the source as/nfts/0_basic.svg
. We also need to pass in the width and height explicitly. - A
button
element let's us call themintToken
functiononClick
and we send the value associated with the button to that function so that it can be used to mint the specific tier of NFT in the<button>
element. Make sure you pass in the right value for each tier. Lastly, we want to disable the button when another mint is underway. For this we use the true/ false values received fromisMintLoading
. The button will be disabled whenisMintLoading
istrue
.
Great but anyone can view and call our mint functions in the frontend of our app
even if their wallet is not connected to the application. This could result in some
errors. We must check if a user is connected before we let them
interact with our application simply by using the isConnected
utility
from Wagmi and setting setting its value in our isUserConnected
state.
// consts... useEffect(() => { try { setIsUserConnected(isConnected); } catch (error) { console.log("Error connecting to user", error.message); } }, [isConnected]); // function Home() code...
The useEffect
hook runs the functions within it everytime the value of
its dependencies (isConnected
, in this case) changes.
Then we can wrap the <main>
element as follows:
// return statement for rendering... {isUserConnected ? <main className={styles.main}> {/* NFT Flex div code here... */} </main> : <main className={styles.main}> <div>Please connect your wallet.</div> </main> } // closing <div> and styling consts...
This type of syntax (condition ? ifTrueDoThis : ifFalseDoThis
) is known
as a ternary
operator. It checks whether the condition is true
or false
and returns one of two values accordingly. In our case, we check if the
the user is connected and show them the mint options if true
, else we ask
them to connect their wallet.
Now our preview should look like this once a wallet is connected:
Woohoo! You've done a lot so far.
Try minting these NFTs and check your wallet's profile on
https://testnets.opensea.io/YOUR_WALLET_ADDRESS
. Refer to the following
side drawer for the code written till now if stuck somewhere.
Take a breather and stretch yourself before we do the last bit of coding.
Successful Mint Modal
Doesn't it feel like we are still missing something?
Right! We want our users to see information when they minting. We want them to know when a mint is in progress and we want them to see the result once the mint is successful.
But what is this modal. It is a an element that is usually used as an overlay that provides its own interaction environment separate from the page below it. This interaction environment provides a focus element where only options from this can be selected while it is active.
Okay lets write some code for it.
To be able to display information about our latest mint, we must first get information about it from our smart contract.
// imports, consts and WAGMI useContractWrite hook... const { data: tokenData, refetch: refetchTokenData, } = useContractRead({ addressOrName: CONTRACT_ADDRESS, contractInterface: TierABI.abi, functionName: "totalSupply", watch: true, }) const { data: tokenURI, } = useContractRead({ addressOrName: CONTRACT_ADDRESS, contractInterface: TierABI.abi, functionName: "tokenURI", args: tokenData, watch: true, }) // useEffect to check user connection... useEffect(() => { try { if (tokenURI) { setLatestNFTMinted(JSON.parse(window.atob(tokenURI.substring(tokenURI.indexOf(',') + 1)))); } } catch (error) { console.log("Error fetching token URI", error.message); } }, [tokenData, tokenURI]); const mintToken = async (e) => { try { let mintTxn = await mint({ recklesslySetUnpreparedOverrides: { value: ethers.utils.parseEther(e.target.value), } }); await mintTxn.wait(); console.log("This is the mint data", mintData); refetchTokenData(); // catch statement for mintToken function... }; // return statement...
- The first thing we need is the
tokenId
of the latest NFT minted. We can do so by reading the value of thetotalSupply
stored in our smart contract because thetotalSupply
should equal to the latesttokenId
in our case. We can fetch this using theuseContractRead
hook from WAGMI that is configured for reading data from smart contracts. The cool thing about solidity is that it automatically gives us a way to read values of variables that are tagged aspublic
, which is why we are able to read the value oftotalSupply
. - The
refetchTokenData
function will be called in ourmintToken
function to update thetokenData
after minting has succeeded. We pass awatch: true
key-value pair so that the hook keeps an active watch on the value of totalSupply. - Then the returned value (tokenData) is passed in as an argument to the
tokenURI
function from our smart contract and we receive aBase64
encoded string in return. - A
useEffect
hook keeps updating this string everytime thetokenData
ortokenURI
is updated and stores a decoded JSON format of it in thelatestNFTMinted
variable.
We're almost there. We just need to do two more checks before we actually look at our modal. The first is that we must display our modal only when we want it to and the other is to display a loading message until our NFT has successfully minted. This is how the mintToken function should look now:
// useEffect for tokenURI... const mintToken = async (e) => { try { setIsMinting(true); setModalShow(true); let mintTxn = await mint({ recklesslySetUnpreparedOverrides: { value: ethers.utils.parseEther(e.target.value), } }); await mintTxn.wait(); console.log("This is the mint data", mintData); refetchTokenData(); setIsMinting(false); } catch (error) { console.log("Error minting NFT", error.message); } }; // return statement...
When we trigger the mintToken
function we want to set the state to
isMinting
. Based on this condition, we can inform the users that the
mint is in progress. We also want our modal to pop up right after this
so we set the value of modalShow
to true
.
We're there at last! Now we can render our modal.
// return statement for rendering... {isUserConnected ? <main className={styles.main}> <div style={NFTFlex}> {/* NFT Card div code... */} </div> {modalShow && ( <div style={modal}> {isMinting ? <div style={modalContent}> <h2>Minting...</h2> </div> : <div style={modalContent}> <h2>Mint Successful</h2> <div style={modalBody}> <h3>{latestNFTMinted.name}</h3> <Image src={latestNFTMinted.image} height='200px' width='200px' /> </div> <div style={modalFooter}> <button style={modalButton}> <a href={`https://testnets.opensea.io/assets/mumbai/${CONTRACT_ADDRESS}/${tokenData}`} target="_blank"> View on OpenSea </a> </button> <button style={modalButton}> <a href={`https://mumbai.polygonscan.com/tx/${mintData.hash}`} target="_blank"> View on Polygonscan </a> </button> <button onClick={() => setModalShow(false)} style={modalButton}>Close</button> </div> </div> } </div> )} </main> // closing <div> and styling consts...
This may seem like a lot of code but we're going to look at it in bits:
- We have a heading that tells us our mint is successful.
- The body shows us the name, id and image of the NFT minted. We fetch
from the information stored earlier in the
latestNFTMinted
variable. - We have a footer that provides links to view our NFT on OpenSea and to
the transaction details on Polygonscan. Additionally we have a Close
button that changes the state of the
modalShow
tofalse
and thus closes the modal modal when we click it.
The final modal should look like this:
If you are stuck somewhere, please refer to the code side drawer below.
Fin.
Hoorah! We've done it. Pat yourselves on the back and get some ice cream!
Right from learning the basics of smart contract development using solidity to creating a full stack decentralised NFT Minting Application.
Stay tuned for more!