Connect your Smart Contract to a Front End Application.

Created by:

Avatar

piablo @ D_D Academy

September 22, 2022

Twitter @ropats16

About this lesson

Congratulations on making it to this final tutorial in a series on smart contract development of the ERC-721 standard, a.k.a. NFTs. In the first in the track, you created a basic smart contract using Remix. Then a shift to a dedicated developer's environment to create and deploy an NFT contract to an actual test network. In lesson 3 you added tiers to your NFTs, finally learning to make your contracts air-tight by building your own automated testing suite in the fourth of the series.

But to create a full stack decentralised application, we also need a UI, or front-end for users to interact with. In this lesson that is exactly what we're going to create.

Expect somewhere from one to four hours to complete the lesson, depending on your prior experience and the need to grasp new concepts. To help you maintain a steady pace and encourage breaks for rejuvenation, you'll find our pomodoro timer βŒ› in the menu header. It'll remind you to "go touch grass" and appreciate nature's role in enhancing our well-being. 🌱 Relish your journey in development in a good state of mind and body.

What are we building

Now we're going use all of this knowledge 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. We'll need some extra magic to make that all happen.

For handling wallet connections to our application, we'll be introducing you to RainbowKit. And using the provided hooks from WAGMI, we can check the connection to the wallet and call functions from our smart contract through the front-end. RainbowKit and WAGMI are just another couple of shining examples of open source public goods. We love open source at Developer_DAO. And public goods are gooood!

Let's breakdown the lesson

  • Set up our development environment with TierNFT project
  • Initialise a nextJS app
  • Install RainbowKit, WAGMI and Ethers to create main application
  • Import dependencies
  • Create an Index page as main entry point for displaying the app content.
  • Create NFT cards
  • Enable minting of NFTs in the application.
  • Lesson Summary

Tools to get the job done

  • nextJS - a React framework for building a full-stack app
  • RainbowKit - a React library for out-of-the-box wallet management
  • viem - a compact JavaScript library for interacting with the Ethereum eco-system
  • WAGMI is a collection of React hooks that allows us to work effectively with Ethereum
  • Alchemy is an RPC provider that lets our app connect to the blockchain
  • WalletConnect - is a bridge that connects Decentralized Applications (DApps) to your web3 wallet

* always make sure you access up-to-date release notes and documentation

Let's dive in!


Getting started

We're going to start where we left off in Write Automated Tests for your TierNFT.

Open your code editor and

cd
into the
tierNFT
project directory. For all things related to our front-end we are going to create a new sub-directory within our project's root directory to maintain good practice, keep our code clean, and be easy to locate and read. The first step is to initiate a Next.js application.

πŸ’‘
Make sure you are in your project's root directory before continuing.

The next 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 since we have been using npm as the package manager for all our previous builds in this track, we'll add the
--use-npm
flags:

# create your Next.js application
npx create-next-app frontend --use-npm

For your next steps, choose 'No' to TypeScript, because we want to create a JavaScript project, but choose 'Yes' to all the other options:

Need to install the following packages:
  create-next-app@14.0.4
Ok to proceed? (y) y
βœ” Would you like to use TypeScript? … No
βœ” Would you like to use ESLint? … Yes
βœ” Would you like to use Tailwind CSS? … Yes
βœ” Would you like to use `src/` directory? … Yes
βœ” Would you like to use App Router? (recommended) … Yes
βœ” Would you like to customize the default import alias (@/*)? … Yes
Creating a new Next.js app in ((path)):/developerdao/frontend.

Once the setup is complete, we open the

frontend
directory and get cracking:

cd frontend


The Front-end

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

Install RainbowKit and it's dependencies:

npm install @rainbow-me/rainbowkit wagmi viem

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";

export default function App({ Component, pageProps }) {
  return (
    <Component {...pageProps} />
  );
}

export default MyApp;

We need to add some additional imports to create our configurations.

import "@/styles/globals.css";
// new block of imports:
import "@rainbow-me/rainbowkit/styles.css";
import { getDefaultWallets, RainbowKitProvider } from "@rainbow-me/rainbowkit";
import { WagmiConfig, createConfig, configureChains } from "wagmi";
import { polygonMumbai } from "wagmi/chains";

import { alchemyProvider } from "wagmi/providers/alchemy";
import { publicProvider } from "wagmi/providers/public";
import { useEffect, useState } from "react";
// end of new block of imports

// the rest of the 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 { polygonMumbai, 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';
    and
    import { publicProvider } from 'wagmi/providers/public';
    give us the provider configs for the providers we will be using. In our app we are using an
    alchemyProvider
    along with a fall back
    publicProvider
    . A provider allows our application to communicate with a blockchain.
  • useEffect
    and
    useState
    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.

Because our dApp is going to use WalletConnect, we need a

projectID
from the WalletConnectCloud. So go and set up an account and grab that ID. You'll be done in a few minutes πŸ€—

All good? Okay, let's get back to the

_app.js
file, set up the configurations we need, and create a new instance of the
wagmiClient
which will use them:

// import statements code...

const { chains, publicClient } = configureChains(
  [polygonMumbai],
  [alchemyProvider({ apiKey: process.env.NEXT_PUBLIC_ALCHEMY_ID }), publicProvider()],
);

const { connectors } = getDefaultWallets({
  appName: "TierNFTs Front-end Integration",
  projectId: process.env.NEXT_PUBLIC_WALLETCONNECTCLOUD_PROJECT_ID,
  chains,
});

const wagmiConfig = createConfig({
  autoConnect: true,
  connectors,
  publicClient,
});

// 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'

in our case:

  NEXT_PUBLIC_ALCHEMY_ID=
  NEXT_PUBLIC_PROJECT_ID=
  NEXT_PUBLIC_CONTRACT_ADDRESS=

Great! Now we can wrap our application in the

RainbowKitProvider
and
WagmiConfig
so that it can 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 config={wagmiConfig}>
      <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

Notice that we are using the

page folder routing
in our app, which give us an
index.js
page in the
/frontend/pages
directory. This file work as our homepage at the root level, in our url will be
htpp://localhost:3000/
by default and it's code should look like this after deleting all the children within the
<main>
element and completely deleting the
<footer>
.

import Head from "next/head";
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,
  usePrepareContractWrite,
  useWaitForTransaction,
} from "wagmi";
import TierABI from "@/artifacts/contracts/TierNFT.sol/TierNFT.json";
import styles from "@/styles/Home.module.css";
import { parseEther } from "viem";
import { useEffect, useState } from "react";
import Image from "next/image";

// function Home() code...
  • RainbowKit provides us a ready-made
    ConnectButton
    .
  • We need to use the
    useAccount
    hook from WAGMI to check account connection and
    useContractRead
    and
    useContractWrite
    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
    viem
    library will assist us with some utilities in our code.
  • Once again,
    useEffect
    and
    useState
    are react hooks that will help us perform side effects and capture the state, respectively.
export default function Home() {
  const CONTRACT_ADDRESS = const CONTRACT_ADDRESS =
    process.env.NEXT_PUBLIC_TIERNFT_CONTRACT_ADDRESS || "0x5794c3de7a59587048b76060d282476e12a8aff8"; /* Or enter Contract Address between ''*/;

  const { isConnected } = useAccount();

  const [isUserConnected, setIsUserConnected] = useState(false);
  const [latestNFTMinted, setLatestNFTMinted] = useState(0);
  const [modalShow, setModalShow] = 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:

App Header Preview

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 { config, isLoading: isMintLoading } = usePrepareContractWrite({
    address: CONTRACT_ADDRESS,
    abi: TierABI.abi,
    functionName: "safeMint",
    value: parseEther(mintingPrice), // "0.02"
    // enabled: Boolean(mintingPrice !== "0"), // "true"
  });

  const { data: mintData, write } = useContractWrite(config);

  const { data: txData } = useWaitForTransaction({
    hash: mintData?.hash,
  });
  // return statement for rendering...
}

The

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.

The

usePrepareContractWrite
hook also work better with the
usePrepareContractWrite
hook as explained on wagmi docs.

// WAGMI useContractWrite hook...

const mintToken = async (e) => {
  try {
    let mintTxn = await mint({
      recklesslySetUnpreparedOverrides: {
        value: 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 the
    recklesslySetUnpreparedOverrides
    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="200" height="200" alt="Basic Tier" />
      <button
        value="0.01"
        onClick={(e) => {
          setMintingPrice(e.target.value);
          mintToken();
        }}
        style={NFTMint}
        disabled={isMintLoading}
      >
        Mint
      </button>
    </div>
    <div style={NFTCard}>
      Tier 1
      <Image src="/nfts/1_medium.svg" width="200" height="200" alt="Medium Tier" />
      <button
        value="0.02"
        onClick={(e) => {
          setMintingPrice(e.target.value);
          mintToken();
        }}
        style={NFTMint}
        disabled={isMintLoading}
      >
        Mint
      </button>
    </div>
    <div style={NFTCard}>
      Tier 2
      <Image src="/nfts/2_premium.svg" width="200" height="200" alt="Premium Tier" />
      <button
        value="0.05"
        onClick={(e) => {
          setMintingPrice(e.target.value);
          mintToken();
        }}
        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="200" height="200" alt="Premium Tier" />
  <button
    value="0.01"
    onClick={(e) => {
      setMintingPrice(e.target.value);
      mintToken();
    }}
    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 first set the minting price choosen and then it calls the
    mintToken
    function
    onClick
    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 from
    isMintLoading
    . The button will be disabled when
    isMintLoading
    is
    true
    .

Great but anyone can view and call our mint functions in the front-end 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

condition ? ifTrueDoThis : ifFalseDoThis
type of syntax 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:

NFT Cards Preview

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({
  address: CONTRACT_ADDRESS,
  abi: TierABI.abi,
  functionName: "totalSupply",
  watch: true,
  enabled: process.env.NEXT_PUBLIC_CONTRACT_ADDRESS,
});

const { data: tokenURI } = useContractRead({
  address: CONTRACT_ADDRESS,
  abi: TierABI.abi,
  functionName: "tokenURI",
  args: [1],
  watch: true,
  enabled: process.env.NEXT_PUBLIC_CONTRACT_ADDRESS,
});

// 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) => {
  if (mintingPrice !== "0") {
    try {
      setModalShow(true);
      write();
      refetchTokenData(); // <--------- this is the new line: here an exaplanation of the refetchTokenData() Function for i.e.
      setMintingPrice("0");
    } catch (error) {
      console.log("Error minting NFT", error.message);
    }
  } else {
    alert("Please select a tier to mint");
  }
};

// JSX return statement...
  • The first thing we need is the
    tokenId
    of the latest NFT minted. We can do so by reading the value of the
    totalSupply
    stored in our smart contract because the
    totalSupply
    should equal to the latest
    tokenId
    in our case. We can fetch this using the
    useContractRead
    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 as
    public
    , which is why we are able to read the value of
    totalSupply
    .
  • The
    refetchTokenData
    function will be called in our
    mintToken
    function to update the
    tokenData
    after minting has succeeded. We pass a
    watch: 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 a
    Base64
    encoded string in return.
  • A
    useEffect
    hook keeps updating this string everytime the
    tokenData
    or
    tokenURI
    is updated and stores a decoded JSON format of it in the
    latestNFTMinted
    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) => {
  if (mintingPrice !== "0") {
    try {
      setModalShow(true);
      write();
      console.log("This is the mint data", mintData);
      refetchTokenData(); // <--------- this is the new line: here an exaplanation of the refetchTokenData() Function for i.e.
      setMintingPrice("0");
    } catch (error) {
      console.log("Error minting NFT", error.message);
      setMintingPrice("0");
    }
  } else {
    alert("Please select a tier to mint");
  }
};

// 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}>
          {txData === undefined ? (
            <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="200" width="200" alt="NFT Image" />
              </div>
              <div style={modalFooter}>
                <button style={modalButton}>
                  <a
                    href={`https://testnets.opensea.io/assets/mumbai/${CONTRACT_ADDRESS}/${
                      formatUnits(tokenData, 0) - 1
                    }`}
                    target="_blank"
                  >
                    View on OpenSea
                  </a>
                </button>
                <button style={modalButton}>
                  {txData && txData.transactionHash ? (
                    <a
                      href={`https://mumbai.polygonscan.com/tx/${txData.transactionHash}`}
                      target="_blank"
                    >
                      View on Polygonscan
                    </a>
                  ) : undefined}
                </button>
                <button onClick={() => setModalShow(false)} style={modalButton}>
                  Close
                </button>
              </div>
            </div>
          )}
        </div>
      )}
    </main>
  ) : (
    <main className={styles.main}>
      <div>Please connect your wallet</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
    to
    false
    and thus closes the modal modal when we click it.

The final modal should look like this:

Successful Mint Modal Preview

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!