Skip to main content

Building a full-stack Decentralised Application on the Ethereum blockchain

Written by Perry Fardella

Part Five: Creating a front-end for the Decentralised Application

This blog series is broken down into five parts:

So, now we have a smart contract out in the world that serves as the back-end of our Dapp, how do we interact with our smart contract? Well, we build a front-end for our Dapp that will allow users ease of access to our smart contract.

How are we building the front-end of our Dapp?

We’re going to be using the Next.js framework and Web3.js library to build a React web application to serve as the front-end of our Dapp and I’ll primarily be using TypeScript as my programming language of choice. Next is a React framework that I’m very fond of for its ease of development and generation of static web pages on deployment. Web3.js is a JavaScript library of handy tools that make interacting with local and remote Ethereum blockchains a breeze.

Initialising a new project

Much like developing our smart contract, we’re going to use NPM to get us started here. Open your terminal of choice, in the directory you’d like to save your project to, and enter the following command:

npx create-next-app@latest inbox-Dapp --ts

This command initialises a Next.js project called inbox-Dapp, with TypeScript support and everything else we need to quickly get a front-end up with very little additional work needed. You should now see a file structure similar to this:

A screenshot of the directory list in VS Code

Running the application

In your terminal, enter the following command:

cd inbox-Dapp

To drill down into the folder where our project files are stored, enter the following command:

npm run dev

This will initialise a local development environment with our application running on it, at the default web address of ‘http://localhost:3000/’. By navigating to the address in a web browser of your choice you will be greeted by a welcome screen, telling you to get started by editing the pages/index.tsx file.

Building our web3 client to handle wallet integration and interact with the Inbox smart contract

Now that we’ve got the bare bones of our web application together, we’re going to build what I’ve dubbed the ‘web3 client’ to handle our MetaMask wallet integration and connection with our deployed smart contract.

Installing the web3.js library

The first thing we need to do is install the web3.js library which will provide us with all the utilities we need to interact with a smart contract on the Ethereum network. Details about how the web3.js library works are available here.

Enter the following command into your terminal to install the web3.js library in our project.

npm install web3

Creating the web3 client file

Now we need to create a new file to build our client in. In your terminal use the following command to create a new folder in our root directory.

mkdir web3Client

Then we’ll navigate into that folder in our terminal using the command:

cd web3Client

Finally we’ll create a new file for our client using the command:

touch Web3Client.ts

Creating the web3 client

Now that we’ve created our web3 client file, open it up in VS code and post the below code into it. I’ve added a lot of comments to this code in an attempt to make it self documenting and explain what each section is doing even if you can’t understand the code. I’ll break down some of the more complex parts below in greater detail.

// Import the web3 library we'll be using
import Web3 from "web3";
// Import web3 data types we'll be using
import { AbiItem } from "web3-utils";
import { Contract } from "web3-eth-contract";
// Declare global client variables
let selectedAccount: string;
let inboxContract: Contract;
let isConnected = false;
// Declare the ABI of the Inbox smart contract
const inboxAbi: AbiItem[] = [
 {
   inputs: [
     {
       internalType: "string",
       name: "initialMessage",
       type: "string",
     },
   ],
   stateMutability: "nonpayable",
   type: "constructor",
 },
 {
   inputs: [],
   name: "getMessage",
   outputs: [
     {
       internalType: "string",
       name: "",
       type: "string",
     },
   ],
   stateMutability: "view",
   type: "function",
 },
 {
   inputs: [
     {
       internalType: "string",
       name: "newMessage",
       type: "string",
     },
   ],
   name: "setMessage",
   outputs: [],
   stateMutability: "nonpayable",
   type: "function",
 },
];
// Connect function for handling connection with the user’s wallet and the smart contract
const connect = async () => {
 let provider = window.ethereum;
 if (typeof provider !== "undefined") {
   provider
     .request({ method: "eth_requestAccounts" })
     .then((accounts: string[]) => {
       selectedAccount = accounts[0];
       console.log(`Selected account is ${selectedAccount}`);
     })
     .catch((err: any) => {
       console.log(err);
       return;
     });
   window.ethereum.on("accountsChanged", function (accounts: string[]) {
     selectedAccount = accounts[0];
     console.log(`Selected account changed to ${selectedAccount}`);
   });
 }
 const web3 = new Web3(provider);
 inboxContract = new web3.eth.Contract(
   inboxAbi,
   // Inbox contract address on the Rinkeby network
   "0x8d8671021Ea191Bf2523fEb915dd5fBC3f08b88a"
 );
 inboxContract.defaultChain = "rinkeby";
 isConnected = true;
};
// Function for calling the getMessage method on the smart contract
export const getMessage = async () => {
 if (!isConnected) {
   await connect();
 }
 return inboxContract.methods
   .getMessage()
   .call()
   .then((message: string) => {
     return message;
   });
};
// Function for calling the setMessage method on the smart contract
export const setMessage = async (message: string) => {
 if (!isConnected) {
   await connect();
 }
 return inboxContract.methods
   .setMessage(message)
   .send({ from: selectedAccount })
   .on("transactionHash", function (hash: string) {
     console.log(hash);
   })
   .on("confirmation", function (confirmationNumber: number, receipt: any) {
     console.log('confirmation number: ' + confirmationNumber);
     console.log(receipt);
   })
   .on("receipt", function (receipt: any) {
     console.log(receipt);
   })
   .on("error", function (error: any, receipt: any) {
     // If the transaction was rejected by the network with a receipt, the second parameter will be the receipt.
     console.log(error);
     console.log(receipt);
   });
};

Declaring the ABI

You’ll notice the following comment in the code above:

// Declare the ABI of the Inbox smart contract

In the section below this comment we are providing the front-end of our web application with the Application Binary Interface (ABI) of our smart contract. As you may remember from part one of the blog, the ABI contains the structure of our smart contract, including all the available variables and methods. Our web application will then use this ABI to access these variables and methods so that we may interact with our smart contract. By declaring the ABI, the front-end of our Dapp - the web application - then knows exactly how it can access the smart contract and what kinds of data it can provide to and is likely to receive from the smart contract.

Connecting to a browser wallet

Establishing a connection to a browser wallet is becoming easier and easier these days as many browsers add default integrations. Within browsers that have capability with browser wallets, the browser wallets can be accessed through the window.ethereum object.

Within the connect function of our web3 client we handle connecting to a user’s browser wallet. The first thing we need to do is to check whether the client has a wallet installed in their browser. We do this by assigning the window.ethereum object to a variable and then checking if it's null or not, which is simply a programmatical way to check if a browser wallet exists. This is done with the following code:

let provider = window.ethereum;
 if (typeof provider !== "undefined") {

If the window.ethereum object is not null, then a wallet exists, and we initiate a connection with that wallet and request access to a user’s account with the following line of code:

 .request({ method: "eth_requestAccounts" })

The window.ethereum object can also be used to track events occurring within the user’s wallet, such as the active account in the wallet being switched to another one. We’re tracking this using the following block of code:

window.ethereum.on("accountsChanged", function (accounts: string[]) {
    selectedAccount = accounts[0];
    console.log(`Selected account changed to ${selectedAccount}`);
  });

Connecting to a smart contract

Now that we have the functionality set up to connect to a user's wallet, we need to establish a connection with our deployed smart contract so that we can interact with it. This is where we’ll be using some of the key functionality from the web3.js library.

The first thing we need to do is to create a new instance of the Web3 object provided to us by the web3.js library, which we can call an array of useful methods to help us connect to our smart contract. We do this by instantiating a new variable we’ll call web3 and providing it with our provider variable which is holding our window.ethereum object. Essentially we’re providing the web3 variable with the wallet connection we’ve established. This is done using the following line of code:

const web3 = new Web3(provider);

Now that we have our web3 variable instantiated, we can use it to set up the smart contract connection. We’re going to do this by creating a new variable called inboxContract and using the .eth.contract methods on our web3 variable to assign it to our deployed smart contract. We’ll need to provide this method with the ABI of our smart contract, as well as the address where our smart contract is deployed on the blockchain. At this point, if you’ve been following the blog series and deployed your own smart contract to the Rinkeby test network, I encourage you to add your own smart contract’s address to the web3 client. If you have not deployed your own smart contract, or would simply prefer to use mine, go ahead and keep the current address that is in the web3 client. The code block for initiating this connection is as follows:

inboxContract = new web3.eth.Contract(
   inboxAbi,
   // Inbox contract address on the Rinkeby network
   "0x8d8671021Ea191Bf2523fEb915dd5fBC3f08b88a"
 );

Once we’ve established this connection, we need to set the chain that our smart contract is deployed on. We only need to do this because our contract is not on the main net, which is the default. This can be done with the following line of code:

inboxContract.defaultChain = "rinkeby";

And that’s it, we’ve now established a live connection with our smart contract on the RInkeby network.

Calling the methods of a smart contract

At this point we’ve established a connection to both the user’s wallet and the smart contract. Now it’s time to call the methods available on our smart contract so that we can use the functionality we’ve given it. The methods available on our smart contract can be called by accessing the methods property of our new inboxContract object. From there the method can be called using the call() method. The code below demonstrates how we are calling the getMessage() method of our smart contract to retrieve the message stored there:

// Function for calling the getMessage method on the smart contract
export const getMessage = async () => {
 if (!isConnected) {
   await connect();
 }
 return inboxContract.methods
   .getMessage()
   .call()
   .then((message: string) => {
     return message;
   });
};

Building out the web application to utilise our web3 client

Now we need to build a visual element to our web application that will display on the screen to the user and allow the user to interact with it so that they can access the smart contract’s features easily. I’m not going to go into depth on how React applications work, but I’ve added a large number of comments that should explain what each block of code is doing here. I’ve also largely neglected the styling of our web application, as the focus here is on functionality and not aesthetics.

Go ahead and copy the following code into your index.tsx file within the pages folder:

import type { NextPage } from "next";
import Head from "next/head";
import styles from "../styles/Home.module.css";
import { useState } from "react";
import { getMessage, setMessage } from "../web3Client/Web3Client";
// Extend the global window interface to avoid a typescript error here
// where ethereum can't be found in the browser
declare global {
 interface Window {
   ethereum: any;
   web3: any;
 }
}
const Home: NextPage = () => {
 // React hooks to store data in state
 const [message, setStateMessage] = useState("");
 const [textInput, setTextInput] = useState("");
 // Function to handle the updating of inputs in the input field
 const handleInputUpdate = (input: string) => {
   setTextInput(input);
 };
 // Function that calls the getMessage method from our client
 const fetchMessage = () => {
   getMessage()
     .then((message) => {
       setStateMessage(message);
     })
     .catch((err) => {
       console.log(err);
     });
 };
 // Function that calls the setMessage method from our client
 const postMessage = (updatedMessage: string) => {
   setMessage(updatedMessage)
     .then(fetchMessage)
     .catch((err) => {
       console.log(err);
     });
   setTextInput("");
 };
 return (
   <div className={styles.container}>
     <Head>
       <title>The Inbox</title>
       <meta name="description" content="The Inbox Dapp" />
       <link rel="icon" href="/favicon.ico" />
     </Head>
     <main className={styles.main}>
       <h1>The Inbox</h1>
       <h2>The current message is: {message}</h2>
       <button onClick={() => fetchMessage()}>Get the message</button>
       <br/>
       <input
         type="text"
         value={textInput}
         onChange={(event) => handleInputUpdate(event.target.value)}
       ></input>
       <button onClick={() => postMessage(textInput)}>Set the message</button>
     </main>
   </div>
 );
};
export default Home;

Running and utilising the web application

Now that we have our web application and web3 client set up and ready to go, it’s time to launch our application and use it. Enter the following command into your terminal to launch a local development environment running our application:

npm run dev

Now we can navigate to ‘http://localhost:3000/’ in our web browser to see our web application in action. It should look similar to the following image:

Connecting to the right network

Before using our application, it’s important to remember that our smart contract we’re trying to access is on the Rinkeby test network and not the Ethereum main network. So, make sure your browser’s wallet is connected to the correct network, otherwise our web application will break as it will be looking for our smart contract on the wrong network!

Getting the message stored on the smart contract

By clicking the ‘Get the message’ button, our application will begin the wallet integration and smart contract connection process. Once you’ve clicked the button, you should see a prompt from your browser wallet about connecting your wallet. By clicking accept, you’re allowing the Dapp to connect with your wallet and use it to make a request to retrieve the message being stored in the smart contract. Once you’ve accepted the wallet integration you should see the message being stored in the smart contract populated right after ‘The current message is:’.

Setting a new message on the smart contract

To set a new message in the smart contract, simply enter your message in the input box and then hit the ‘Set the message button’. You should then see a prompt from your browser’s wallet to accept the request to set the message on the smart contract as well as a breakdown of the fees it will cost to make this transaction. After accepting the transaction, the message will automatically populate or update but you may just have to wait 10-15 seconds for it to process properly.

We’re finished!

Just like that, we have constructed an entire full-stack Dapp.

I want to thank everyone who has made it this far, it certainly hasn’t been the easiest project to build, so give yourself a pat on the back.

I hope this blog post series has served as a good introduction to blockchain development and the flow involved with building Dapps.

If you have any questions, don’t hesitate to reach out to me, with the best place to do so being on LinkedIn.

This blog series is broken down into five parts: