How To Build  A Decentralized Payment Voucher Management System

How To Build A Decentralized Payment Voucher Management System

Building a Payment Voucher Management System with Solidity, Next.js, Tailwind.css, Ethers.js and Hardhat.

In this guide, you'll learn how to build and deploy a full-stack Payment Voucher Management System.

This system can be used in schools with a bursary department charged with keeping records of goods to be bought, how much the goods are worth as well as making payments to the vendor for the goods bought.

To view the final source code for this project, visit this repo

Prerequisites

To be successful in this guide, you must have the following:

  1. Node.js version 16.14.0 or greater installed on your machine. I recommend installing Node using either nvm.

  2. Metamask wallet extension installed as a browser extension

The stack

In this guide, we will build out a full-stack application using:

Web application framework - Next.js
Solidity development environment - Hardhat
Ethereum Node - Quick Node
Ethereum Web Client Library - Ethers.js

About the project

The project we will be building will be BlockPay - a payment management system for vouchers, which keeps a record of the name of the goods bought, the name of the vendor, the address of the vendor(which receives payment), the price of the goods, and time of payment. This system also makes payments to the vendors for goods received.

So basically, in this dApp you can create a draft listing an item to be bought, the price of the item, the name of the vendor, the address of the vendor, the price of the item and the reason for purchase.

When the draft is created, it can then be approved or not. If it is approved, it can then be converted into a voucher and paid for. Goods are paid for by the system (dApp) and not any account connected to it.

This dApp consists of four agents: a Solicitor, the Vice-Chancellor, the Expenditure Control Unit (ECU) and the Bursar.

Anyone who connects his/her wallet that isn't the Vice-Chancellor, the Expenditure Control Unit (ECU) or the Bursar is a Solicitor.

A Solicitor can create a draft, fund the dApp, view the dApp balance, and view all created drafts and vouchers.

The Vice-Chancellor is the account that deploys the smart contract of this dApp. He/She can perform all the activities of a Solicitor, as well as approve created drafts, appoint staff (Bursar and ECU), and withdraw funds from this dApp.

After drafts have been approved, for them to be paid for they are converted into vouchers. The ECU is in charge of this, he/she converts approved drafts to vouchers. The ECU is appointed by the Vice-Chancellor.

The Bursar is just in charge of making payments of vouchers. He/She is appointed by the Vice-Chancellor.

I feel this is a good project because I learned a lot while building it. Asides from learning about, Solidity, React, Tailwind, Ethers.js and Hardhat, I learned how to google and search for errors and bugs. Additionally, the ideas, tools and techniques we will be working with lay the foundation for many other types of applications on this stack. Also, we will learn how the client-side interacts with a smart contract.

Project setup

To build the smart contract we would be using Hardhat. Hardhat is an Ethereum development environment and framework designed for full-stack development in Solidity. In simple words, you can write your smart contract, deploy them, run tests, and debug your code.

To set up a Hardhat project, Open up a terminal and execute these commands

mkdir blockPay
cd blockPay
mkdir smart-contract
cd smart-contract
npm init --yes
npm install --save-dev hardhat

If you are a Windows user, you'll have to add one more dependency. so in the terminal, add the following command :

npm install --save-dev @nomicfoundation/hardhat-toolbox

In the same directory where you installed Hardhat run:

npx hardhat

Make sure you select Create a Javascript Project and then follow the steps in the terminal to complete your Hardhat setup.

Now let's create a new file inside the contracts directory and call it PaymentVoucher.sol

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

contract PaymentVoucher {
    uint public voucherCounter = 0;
    uint public draftCounter = 0;
    uint public approvedDraftsCounter = 0;
    uint public paidVouchersCounter = 0;
    mapping(uint => Voucher) public vouchers;
    mapping(uint => Draft) public drafts;
    event DraftCreated(
        uint DraftId,
        string nameOfGoods,
        string nameOfVendor,
        string reason,
        uint AmountToBePaid,
        bool approved,
        bool created
    );
    event DraftApproved(
        uint DraftId,
        string nameOfGoods,
        string nameOfVendor,
        string reason,
        uint AmountToBePaid,
        bool approved,
        bool created
    );
    event VoucherCreated(
        uint id,
        uint toBePaid,
        address payable Vendor,
        string _nameOfVendor,
        string _nameOfGoods,
        bool Paid,
        bool created
    );
    event VoucherPaid(
        uint id,
        uint toBePaid,
        address payable Vendor,
        string _nameOfVendor,
        string _nameOfGoods,
        bool Paid,
        bool created,
        uint timePaid
    );

    //address public solicitor;
    address public VC;
    address public Bursar;
    address public ECU; // expenditure control unit (raises voucher)
    //address public Audit; //verifies voucher(may not need this unit)

    struct Draft {
        uint DraftId;
        string nameOfGoods;
        string nameOfVendor;
        address payable Vendor;
        string reason;
        uint AmountToBePaid;
        bool approved;
        bool created;
    }

    struct Voucher {
        uint id;
        uint draftId;
        uint toBePaid;
        address payable Vendor;
        string nameOfVendor;
        string nameOfGoods;
        bool Paid;
        bool created;
        uint timePaid;
    }

    constructor() payable {
        VC = msg.sender;
    }

    //appoint Staff
    function appointStaff(address _bursar, address _ecu) public {
        //ensure that only the VC can call this function
      require(msg.sender == VC,"Only the Vice-Chanscellor can appoint                Staff");
        Bursar = _bursar;
        ECU = _ecu;
    }

    //Create a draft
    function createDraft(
        address payable _vendor,
        string memory _nameOfGoods,
        string memory _nameOfVendor,
        string memory _reason,
        uint _toBePaid
    ) public {
     //ensure that the address calling this function isn't same as vendor
        require(_vendor != msg.sender, "You can't create a draft");
        //ensure that the vendor address is not empty
        require(_vendor != address(0), "Please give a Valid Address");
        //ensure that the amount to be paid is valid
        require(_toBePaid > 0, "Invalid Amount");
        //ensure that the draft has not already been created & approved
        require(!drafts[draftCounter].created, "Already Created");
        require(!drafts[draftCounter].approved, "Already Approved");
        //create draft
        drafts[draftCounter] = Draft(
            draftCounter,
            _nameOfGoods,
            _nameOfVendor,
            _vendor,
            _reason,
            _toBePaid,
            false,
            true
        );
        emit DraftCreated(
            draftCounter,
            _nameOfGoods,
            _nameOfVendor,
            _reason,
            _toBePaid,
            false,
            true
        );
        //increment draft count
        draftCounter++;
    }

    function approveDraft(uint _id) public {
        Draft storage _draft = drafts[_id];
        //make sure that you can't approve an invalid draft
        require(_draft.DraftId >= 0, "This is an invalid draft");
        //make sure only the VC can approve drafts
        require(msg.sender == VC, "Only the VC can approve drafts");
        //make sure that the draft has been created
        require(_draft.created == true, "Not created");
        //make sure that he draft has not already been approved
       require(!_draft.approved, "This draft has already been approved");
        //update the draft
        drafts[_id] = Draft(
            _draft.DraftId,
            _draft.nameOfGoods,
            _draft.nameOfVendor,
            _draft.Vendor,
            _draft.reason,
            _draft.AmountToBePaid,
            true,
            true
        );

        //emit event
        emit DraftApproved(
            _draft.DraftId,
            _draft.nameOfGoods,
            _draft.nameOfVendor,
            _draft.reason,
            _draft.AmountToBePaid,
            true,
            true
        );
        //increment approved draft counter
        approvedDraftsCounter++;
    }

    //create a voucher (function takes in id of an approved draft)
    function createVoucher(uint _draftId) public {
        //create an instance of Voucher
        Voucher storage _voucher = vouchers[voucherCounter];
        //create an instance of Draft
        Draft storage _draft = drafts[_draftId];
        // require (drafts[_draftId] = _draft, "invalid draft");
        //only ECU can create a voucher
        require(
            msg.sender == ECU || msg.sender == Bursar,
            "only the Expenditure Control Unit or Bursar can create a Voucher"
        );
        //ensure that the draft has been approved
        require(_draft.approved == true, "Invalid Draft ID");
        //ensure that the voucher has not already been paid for
        require(!_voucher.Paid, "This Voucher has already been paid for");
        // ensure that the voucher is not duplicated
        require(!vouchers[_draftId].created, "Already created");
        //Pass Draft Values to Voucher Values
        _voucher.nameOfGoods = _draft.nameOfGoods;
        _voucher.nameOfVendor = _draft.nameOfVendor;
        _voucher.toBePaid = _draft.AmountToBePaid;
        _voucher.Vendor = _draft.Vendor;
        _voucher.draftId = _draft.DraftId;
        //create voucher
        vouchers[voucherCounter] = Voucher(
            voucherCounter,
            _voucher.draftId,
            _voucher.toBePaid,
            _voucher.Vendor,
            _voucher.nameOfVendor,
            _voucher.nameOfGoods,
            false,
            true,
            0
        );
        //trigger event
        emit VoucherCreated(
            voucherCounter,
            _voucher.toBePaid,
            _voucher.Vendor,
            _voucher.nameOfVendor,
            _voucher.nameOfGoods,
            false,
            true
        );
        //increment voucher count
        voucherCounter++;
    }

    //pay for good (takes in the voucher id)
    function payforGoods(uint _id) public payable {
        // fetch the voucher
        Voucher storage _voucher = vouchers[_id];
        //fetch the Vendor
        address payable _vendor = _voucher.Vendor;
        //make sure only the treasury division can pay
        require(msg.sender == Bursar, "Only the bursar can make payments");
        // //make sure the Voucher is valid
        require(_voucher.id >= 0, "This is not a valid Voucher");
        // make sure the bursary has enough funds
        require(
            address(this).balance >= _voucher.toBePaid,
            "Sorry, the bursary has insufficient funds"
        );
        //make sure the voucher has not been paid for
        require(!_voucher.Paid, "This Voucher has already been paid for");

        // transfer the money to the vendor
        _vendor.transfer(_voucher.toBePaid);
        //Update the Voucher
        vouchers[_id] = Voucher(
            _id,
            _voucher.draftId,
            _voucher.toBePaid,
            _vendor,
            _voucher.nameOfVendor,
            _voucher.nameOfGoods,
            true,
            true,
            block.timestamp
        );
        //Trigger an event
        emit VoucherPaid(
            _voucher.id,
            _voucher.toBePaid,
            _vendor,
            _voucher.nameOfVendor,
            _voucher.nameOfGoods,
            _voucher.Paid,
            _voucher.created,
            block.timestamp
        );
        //increase the paid vouchers counter
        paidVouchersCounter++;
    }

    //returns all vouchers
    function getAllVouchers() public view returns (Voucher[] memory) {
        Voucher[] memory ret = new Voucher[](voucherCounter);
        for (uint i = 0; i < voucherCounter; i++) {
            Voucher storage voucher = vouchers[i];
            ret[i] = voucher;
        }
        return ret;
    }

    //return all drafts
    function getAllDrafts() public view returns (Draft[] memory) {
        Draft[] memory ret = new Draft[](draftCounter);
        for (uint i = 0; i < draftCounter; i++) {
            Draft storage draft = drafts[i];
            ret[i] = draft;
        }
        return ret;
    }

    function bursaryBalance() public view returns (uint) {
        return address(this).balance;
    }

    function withdraw() public {
        require(msg.sender == VC, "only the VC can withdraw");
        uint256 amount = address(this).balance;
        (bool sent, ) = VC.call{value: amount}("");
        require(sent, "Failed to send Ether");
    }

    // Function to receive Ether. msg.data must be empty
    receive() external payable {}

    // Fallback function is called when msg.data is not empty
    fallback() external payable {}

    //get all unapproved drafts
    function getUnApprovedDrafts() public view returns (Draft[] memory) {
        uint draftCount = draftCounter;
        uint unApprovedDraftCount = draftCounter - approvedDraftsCounter;
        uint currentIndex = 0;
        Draft[] memory items = new Draft[](unApprovedDraftCount);
        for (uint i = 0; i < draftCount; i++) {
            if (drafts[i].approved == false) {
                uint currentId = i;
                Draft storage currentItem = drafts[currentId];
                items[currentIndex] = currentItem;
                currentIndex += 1;
            }
        }
        return items;
    }

    //get approved Drafts
    function getApprovedDrafts() public view returns (Draft[] memory) {
     uint draftCount = draftCounter;
     uint uncreatedVoucherCount = approvedDraftsCounter - voucherCounter;
     uint currentIndex = 0;
      Draft[] memory approvedDrafts = new Draft[](uncreatedVoucherCount);
        for (uint i = 0; i < draftCount; i++) {
        if (drafts[i].approved == true && vouchers[i].created == false) {
                uint currentId = i;
                Draft storage currentItem = drafts[currentId];
                approvedDrafts[currentIndex] = currentItem;
                currentIndex += 1;
            }
        }
        return approvedDrafts;
    }

    //get all unpaid vouchers
    function getUnPaidVouchers() public view returns (Voucher[] memory) {
        uint voucherCount = voucherCounter;
        uint unPaidVoucherCount = voucherCounter - paidVouchersCounter;
        uint currentIndex = 0;
        Voucher[] memory items = new Voucher[](unPaidVoucherCount);
        for (uint i = 0; i < voucherCount; i++) {
            if (vouchers[i].Paid == false) {
                uint currentId = i;
                Voucher storage currentItem = vouchers[currentId];
                items[currentIndex] = currentItem;
                currentIndex += 1;
            }
        }
        return items;
    }

    //get all paid vouchers
    function getPaidVouchers() public view returns (Voucher[] memory) {
        uint voucherCount = voucherCounter;
        uint currentIndex = 0;
      Voucher[] memory paidVouchers = new Voucher[](paidVouchersCounter);
        for (uint i = 0; i < voucherCount; i++) {
            if (vouchers[i].Paid == true) {
                uint currentId = i;
                Voucher storage currentItem = vouchers[currentId];
                paidVouchers[currentIndex] = currentItem;
                currentIndex += 1;
            }
        }
        return paidVouchers;
    }
}

Now let's install dotenv package to be able to import the env file and use it in our config. Open up a terminal pointing at smart-contract directory and execute this command

npm install dotenv

Go to Quicknode and sign up for an account. If you already have an account, log in. Quicknode is a node provider that lets you connect to various blockchains. We will be using it to deploy our contract through Hardhat. After creating an account, Create an endpoint on Quicknode, select Ethereum, and then select the Goerli network. Click Continue in the bottom right and then click on Create Endpoint. Copy the link given to you in HTTP Provider and add it to the .env file below for QUICKNODE_HTTP_URL.

To get your private key, you need to export it from Metamask. Open Metamask, click on the three dots, click on Account Details and then Export Private Key. MAKE SURE YOU ARE USING A TEST ACCOUNT THAT DOES NOT HAVE MAINNET FUNDS FOR THIS. Add this Private Key below in your .env file for PRIVATE_KEY variable.

QUICKNODE_HTTP_URL="add-quicknode-http-provider-url-here"

PRIVATE_KEY="add-the-private-key-here"

Let's deploy the contract to the goerli network. Create a new file, or replace the default file, named deploy.js under the scripts folder

Let's write some code to deploy the contract in deploy.js file.

const { ethers } = require("hardhat");
require("dotenv").config({ path: ".env" });

async function main() {
  //get the contract factory
  const paymentVoucherContract = await ethers.getContractFactory(
    "PaymentVoucher"
  );

  //deploy the contract
  const deployedPaymentVoucherContract = await paymentVoucherContract.deploy();

//wait for the deployment to finish
  await deployedPaymentVoucherContract.deployed();

  //get the address of the payment voucher contract
  console.log(
    "Payment Voucher Contract Address:",
    deployedPaymentVoucherContract.address
  );
}

//call the main function and catch if there is any error
main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

Now open the hardhat.config.js file, we'll set up the goerli network here so that we can deploy our contract to the Goerli network. Replace all the lines in the hardhat.config.js file with the given below lines

require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config({ path: ".env" });

const QUICKNODE_HTTP_URL = process.env.QUICKNODE_HTTP_URL;
const PRIVATE_KEY = process.env.PRIVATE_KEY;

module.exports = {
  solidity: "0.8.4",
  networks: {
    goerli: {
      url: QUICKNODE_HTTP_URL,
      accounts: [PRIVATE_KEY],
      gasPrice: 10000000000,
    },
  },
};

Note: I put a specific gas price here because the contract was taking ages to deploy. And I was not patient enough to wait :)

Compile the contract, open up a terminal pointing at smart-contract directory and execute this command

npx hardhat compile

To deploy, open up a terminal pointing at smart-contract directory and execute this command

npx hardhat run scripts/deploy.js --network goerli

Save the Payment Voucher Contract Address that was printed on your terminal in your notepad, you would need it further down in the tutorial.

Website

To develop the website we would be using React and Next Js. React is a javascript framework that is used to make websites and Next Js is built on top of React. First, You would need to create a new next app. Your folder structure should look something like

- blockPay
       - smart-contract
       - my-app

To create this my-app, in the terminal point to blockPay folder and type

npx create-next-app@latest

and press enter for all the questions

Now to run the app, execute these commands in the terminal

cd my-app
npm run dev

Now go to http://localhost:3000, your app should be running.

Now let's install Web3Modal library. Web3Modal is an easy-to-use library to help developers add support for multiple providers in their apps with a simple customizable configuration. Open up a terminal pointing atmy-app directory and execute this command

npm install web3modal

In the same terminal also install ethers.js

npm install ethers

In your public folder, download this image (Download Link). Make sure that the name of the downloaded image is saved as 0.svg.

Setting up Tailwind CSS

We'll be using Tailwind CSS for styling, we will set that up in this step.

Tailwind is a utility-first CSS framework that makes it easy to add styling and create good-looking websites without a lot of work.

Next, install the Tailwind dependencies, and make sure your terminal is pointing to my-app directory and execute this command:

npm install -D tailwindcss@latest postcss@latest autoprefixer@latest

Next, we will create the configuration files needed for Tailwind to work with Next.js (tailwind.config.js and postcss.config.js) by running the following command:

npx tailwindcss init -p

Next, configure your template content paths in tailwind.config.js:

/* tailwind.config.js */
module.exports = {
  content: [
    "./pages/**/*.{js,ts,jsx,tsx}",
    "./components/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

Finally, delete the code in styles/globals.css and update it with the following:

@tailwind base;
@tailwind components;
@tailwind utilities;

Then, open your pages/_app.js file under the pages folder and paste the following code, explanation of the code can be found in the comments.

import "../styles/globals.css";
import Head from "next/head";
import Link from "next/link";
import React, { useState } from "react";
import FundModal from "../components/fundModal";
import { Contract, ethers } from "ethers";
import Web3Modal from "web3modal";
import { abi, PAYMENT_VOUCHER_CONTRACT_ADDRESS } from "../constants";

export default function App({ Component, pageProps }) {
  const [show, setShow] = useState(false);

  const getBalance = async () => {
    try {
      const web3Modal = new Web3Modal();
      const connection = await web3Modal.connect();
      const provider = new ethers.providers.Web3Provider(connection);

      //get instance of the contract
      const paymentVoucherContract = new Contract(
        PAYMENT_VOUCHER_CONTRACT_ADDRESS,
        abi,
        provider
      );

      ///call the get the balance function
      const balance = await paymentVoucherContract.bursaryBalance();
      const bBalance = ethers.utils.formatUnits(balance.toString(), "ether");
      window.alert(`The Bursary balance is ${bBalance} ETH`);
    } catch (error) {
      console.error(error);
    }
  };
  return (
    <div>
      <Head>
        <title>BlockPay</title>
        <meta name="description" content="Voucher-System" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <nav className="border-b">
        <p className="text-4xl font-bold flex justify-center items-center">
          Block Pay
        </p>
        <div className="flex justify-center items-center mt-2">
          <Link href="/" className="mr-4 text-pink-500 font-semibold">
            Home
          </Link>
          <Link
            href="/create_draft"
            className="mr-6 text-pink-500 font-semibold"
          >
            Create Draft
          </Link>
          <button
            onClick={() => setShow(true)}
            className="mr-6 text-pink-500 font-semibold"
          >
            Fund Bursary
          </button>
          <Link href="/all_drafts" className="mr-6 text-pink-500 font-semibold">
            All Drafts
          </Link>
          <Link
            href="/all_vouchers"
            className="mr-6 text-pink-500 font-semibold"
          >
            All Vouchers
          </Link>
          <button
            onClick={getBalance}
            className="mr-6 text-pink-500 font-semibold"
          >
            Bursary Balance
          </button>
        </div>
      </nav>
      {show ? (
        <FundModal OnClose={() => setShow(false)} />
      ) : (
        <Component {...pageProps} />
      )}
      <footer className="flex p-4 border-t-2 border-solid border-gray-200 justify-center items-center">
        © 2023~
        <Link
          href="https://github.com/Mayorval?tab=repositories"
          className="hover:text-pink-500"
        >
          Val Chinedu
        </Link>
      </footer>
    </div>
  );
}

This page returns the head, the navigation and the footer. The navigation has links for the home route, as well as pages to create a draft, view all drafts, view all vouchers, a modal to fund the dApp and view its balance.

The next page we'll update is pages/index.js. This is the main entry point of the app, this page will let you connect your wallet and get you signed in as either a Solicitor, Vice-Chancellor, Bursar or ECU.

import { Contract, providers } from "ethers";
import { useEffect, useRef, useState } from "react";
import Web3Modal from "web3modal";
import { abi, PAYMENT_VOUCHER_CONTRACT_ADDRESS } from "../constants";
import styles from "../styles/Home.module.css";

export default function Home() {
  //walletconnected keeps track of whether the user's wallet is connected or not
  const [walletConnected, setWalletConnected] = useState(false);
  //loading is set to true when we are waiting for the transaction to get mined
  const [loading, setLoading] = useState(false);
  //checks if the currently connected metaMask wallet is the VC(Vice-Chancellor)
  const [isVC, setIsVC] = useState(false);
  //checks if the connected metaMask Wallet is the Bursar
  const [isBursar, setIsBursar] = useState(false);
  //checks if the connected metaMask wallet is the Expenditure Control Unit
  const [isECU, setIsECU] = useState(false);
  //create a reference to the Web3 Modal (used for connecting to MetaMask) which persists as long as the page is open
  const web3ModalRef = useRef();

  //getVC: calls the contract to retrieve the VC
  const getVC = async () => {
    try {
      const provider = await getProviderOrSigner();

      //get an instance of the Payment Voucher Contract
      const paymentVoucherContract = new Contract(
        PAYMENT_VOUCHER_CONTRACT_ADDRESS,
        abi,
        provider
      );

      const VC = await paymentVoucherContract.VC();
      const signer = await getProviderOrSigner(true);
      const userAddress = await signer.getAddress();

      if (VC.toLowerCase() === userAddress.toLowerCase()) {
        setIsVC(true);
      }
    } catch (error) {
      console.error(error);
    }
  };

  //getBursar: calls the contract to retrieve the Bursar
  const getBursar = async () => {
    try {
      const provider = await getProviderOrSigner();

      //get an instance of the Payment Voucher Contract
      const paymentVoucherContract = new Contract(
        PAYMENT_VOUCHER_CONTRACT_ADDRESS,
        abi,
        provider
      );

      const Bursar = await paymentVoucherContract.Bursar();
      const signer = await getProviderOrSigner(true);
      const userAddress = await signer.getAddress();

      if (Bursar.toLowerCase() === userAddress.toLowerCase()) {
        setIsBursar(true);
      }
    } catch (error) {
      console.error(error);
    }
  };

  //getECU: calls the contract to retrieve the ECU
  const getECU = async () => {
    try {
      //Get the provider from web3Modal, which in our case is MetaMask
      //No need for the signer here, as we are reading state from the  blockchain
      const provider = await getProviderOrSigner();

      //get an instance of the Payment Voucher Contract
      const paymentVoucherContract = new Contract(
        PAYMENT_VOUCHER_CONTRACT_ADDRESS,
        abi,
        provider
      );

      const ECU = await paymentVoucherContract.ECU();
      //we will get the signer now to extract the address of the currently connected Metamask account
      const signer = await getProviderOrSigner(true);
      const userAddress = await signer.getAddress();

      if (ECU.toLowerCase() === userAddress.toLowerCase()) {
        setIsECU(true);
      }
    } catch (error) {
      console.error(error);
    }
  };

  /**
   * Returns a Provider or Signer object representing the Ethereum RPC with or without the
   * signing capabilities of metamask attached
   *
   * @param {*} needSigner - True if you need the signer, default false otherwise
   */
  const getProviderOrSigner = async (needSigner = false) => {
    //connect to MetaMask
    //since we store 'web3Modal' as a reference, we need to access the 'current' value to get access to the underlying object
    const provider = await web3ModalRef.current.connect();
    const web3Provider = new providers.Web3Provider(provider);

    //if user is not connected to the Goerli network, let them know and throw an error
    const { chainId } = await web3Provider.getNetwork();
    if (chainId !== 5) {
      window.alert("Change the network to Goerli");
      throw new Error("Incorrect Password");
    }

    if (needSigner) {
      const signer = web3Provider.getSigner();
      return signer;
    }
    return web3Provider;
  };

  //withdrawFund: allows only the VC to withdraw the funds from the contract
  const withdrawFunds = async () => {
    try {
      const signer = await getProviderOrSigner(true);

      //get an instance of the payment voucher contract
      const paymentVoucherContract = new Contract(
        PAYMENT_VOUCHER_CONTRACT_ADDRESS,
        abi,
        signer
      );

      //call the withdraw function
      const tx = await paymentVoucherContract.withdraw();
      setLoading(true);
      //wait for transaction to get mined
      await tx.wait();
      setLoading(false);
      window.alert("You have succeessfully Withdrawn Funds");
    } catch (error) {
      console.error(error);
    }
  };

  /**
   * connectWallet: connects the MetaMask wallet
   */
  const connectWallet = async () => {
    try {
      //Get the provider from web3Modal, which in our case is MetaMask
      //When used for the first time, it prompts the user to connect their wallet
      await getProviderOrSigner();
      await getVC();
      await getBursar();
      await getECU();
      setWalletConnected(true);
    } catch (error) {
      console.error(error);
    }
  };

  useEffect(() => {
    //if wallet is not connected, create a new instance of Web3Modal and connect the MetaMask wallet
    if (!walletConnected) {
      //Assign the Web3Modal class to the reference object by setting its 'current' value
      // The 'current' value is persisted throughout as long as this page is open
      web3ModalRef.current = new Web3Modal({
        network: "goerli",
        providerOptions: {},
        disableInjectedProvider: false,
      });
      //connectWallet();
    }
  }, [walletConnected]);

  //renderButton: Returns a button based on the state of the dapp

  const renderButton = () => {
    //if wallet is not connected, return a button which allows them to connect their wallet
    if (!walletConnected) {
      return (
        <div>
          <h1 className={styles.title}>Welcome to BlockPay</h1>
          <div className={styles.description}>
            A Decentralized Payment Voucher Management System
          </div>
          <button
            onClick={connectWallet}
            className="mt-4 w-2/5 bg-pink-500 text-white font-bold py-2 px-4 rounded"
          >
            Connect Wallet
          </button>
        </div>
      );
    }

    // If we are currently waiting for something, return a loading button
    if (loading) {
      return (
        <div className="min-h-screen flex justify-center items-center">
          <button className="mt-4 w-2/5 bg-pink-500 text-white font-bold py-2 px-4 rounded">
            Please Wait...
          </button>
        </div>
      );
    }

    //if wallet is connected, and not VC, Bursar, TD, or ECU, render button to create draft, get created drafts and fund bursary
    if (walletConnected && !isVC && !isBursar && !isECU) {
      return (
        <div>
          <h1 className="text-3xl my-8">Hi there, Solicitor!</h1>
          <div className="leading-none my-8 mx-0 text-xl">Welcome back!</div>
          <div className="flex p-2 justify-center w-full"></div>
        </div>
      );
    }

    //if wallet is connected and user is VC, render Approve Draft, Withdraw funds, Get All Vouchers, fund bursary
    if (isVC) {
      return (
      <div>
       <h1 className="text-4xl my-8 mx-0">Hi there, Vice-Chancellor!</h1>
      <div className="leading-none my-8 mx-0 text-xl">Welcome back!</div>
          <div className="flex">
            <button
              onClick={(e) => {
                e.preventDefault;
                window.location.href = "/approve_draft";
              }}
              className="mx-1 w-1/3 bg-pink-500 text-white text-lg font-bold py-0 px-0 rounded"
            >
              Approve Drafts
            </button>
            <button
              onClick={(e) => {
                e.preventDefault;
                window.location.href = "/appoint_staff";
              }}
              className="mx-1 w-1/3 bg-pink-500 text-white text-lg font-bold py-0 px-0 rounded"
            >
              Appoint Staff
            </button>
            <button
              onClick={withdrawFunds}
              className="mx-1 w-1/3 bg-pink-500 text-white text-lg font-bold py-0 px-0 rounded"
            >
              Withdraw Funds
            </button>
          </div>
        </div>
      );
    }

    //if wallet is connected and the user is the Bursar, then render make payments, get all vouchers, fund bursary
    if (isBursar) {
      return (
        <div>
          <h1 className="text-3xl my-8">Hi there, Bursar!</h1>
          <div className="leading-none my-8 mx-0 text-xl">Welcome back!</div>
          <div className="flex p-2 justify-center w-full">
            <button
              onClick={(e) => {
                e.preventDefault;
                window.location.href = "/make_payments";
              }}
              className="mt-4 w-full bg-pink-500 text-white font-bold py-2 px-8 rounded-lg"
            >
              Make Payments
            </button>
          </div>
        </div>
      );
    }

    //if wallet is coneected and user is ECU, render create vouchers and fund bursary
    if (isECU) {
      return (
        <div>
          <h1 className="text-3xl my-8 mx-0">
            Hi there, Head Expenditure Control Unit!
          </h1>
          <div className="leading-none my-8 mx-0 text-xl">Welcome back!</div>
          <div className="flex p-2 justify-center w-full">
            <button
              onClick={(e) => {
                e.preventDefault;
                window.location.href = "/create_voucher";
              }}
              className="mt-4 w-2/5 bg-pink-500 text-white font-bold py-2 px-4 rounded-lg"
            >
              Create Voucher
            </button>
          </div>
        </div>
      );
    }
  };

  return (
    <div>
      {loading ? (
        <div className="min-h-screen flex justify-center items-center">
          <button className="mt-4 w-2/5 bg-pink-500 text-white font-bold py-2 px-4 rounded">
            Please Wait...
          </button>
        </div>
      ) : (
        <div className="min-h-screen flex justify-center items-center font-mono">
          {renderButton()}
          <div>
            <img className="w-3/5 h-2/4 ml-32" src="./0.svg" />
          </div>
        </div>
      )}
    </div>
  );
}

Creating a draft

Next, let's create a page that lets us create drafts. In your pages folder, create a new file called create_draft.js (pages/create_draft.js) and add the following code.

import { Contract, ethers } from "ethers";
import { useState } from "react";
import Web3Modal from "web3modal";
import { abi, PAYMENT_VOUCHER_CONTRACT_ADDRESS } from "../constants";
import React from "react";

const CreateDraft = () => {
  const [vendor, setVendor] = useState("");
  const [nameOfGoods, setNameOfGoods] = useState("");
  const [nameOfVendor, setNameOfVendor] = useState("");
  const [reason, setReason] = useState("");
  const [amount, setAmount] = useState("");
  const [loading, setLoading] = useState(false);

  const createDraft = async (e) => {
    e.preventDefault();
    try {
      //get the signer to perfom the transaction
      const web3Modal = new Web3Modal();
      const connection = await web3Modal.connect();
      const provider = new ethers.providers.Web3Provider(connection);
      const signer = provider.getSigner();

      //get an instance of the contract
      const paymentVoucherContract = new Contract(
        PAYMENT_VOUCHER_CONTRACT_ADDRESS,
        abi,
        signer
      );
      //convert the price inputted to an ether value
      const price = ethers.utils.parseUnits(amount.toString(), "ether");

      //call the create draft function from our smart contract
      const tx = await paymentVoucherContract.createDraft(
        vendor,
        nameOfGoods,
        nameOfVendor,
        reason,
        price
      );
      setLoading(true);
       //wait for the transaction
      await tx.wait();
      setLoading(false);
      window.alert("You have successfully created a draft!");
       //reset the form fields
      setVendor("");
      setNameOfGoods("");
      setNameOfVendor("");
      setReason("");
      setAmount("");
    } catch (error) {
      console.error(error);
    }
  };

  if (loading) {
    return (
      <div className="min-h-screen flex justify-center items-center">
        <button className="mt-4 w-2/5 bg-pink-500 text-white font-bold py-2 px-4 rounded">
          Please Wait...
        </button>
      </div>
    );
  }

  return (
    <div className="min-h-screen flex flex-row justify-center items-center font-mono">
      <form
        onSubmit={createDraft}
        className="flex flex-col border rounded-xl shadow-xl w-72 h-fit"
      >
        <h1 className="text-4xl w-full pl-4">Create Draft</h1>
        <div className="border mx-2 mt-2 shadow"></div>
        <label className="mt-4 mb-1 ml-2 uppercase font-bold text-lg text-gray-900">
          Name of Item:
        </label>
        <input
          className="border mx-2 rounded shadow  focus:outline-none focus:ring focus:ring-pink-500"
          value={nameOfGoods}
          placeholder="Enter name of item"
          onChange={(e) => setNameOfGoods(e.target.value)}
        />
        <label className="mt-4 mb-1 ml-2 uppercase font-bold text-lg text-gray-900">
          Name of Vendor:
        </label>
        <input
          className="border mx-2 rounded shadow  focus:outline-none focus:ring focus:ring-pink-500"
          value={nameOfVendor}
          placeholder="Enter name of vendor"
          onChange={(e) => setNameOfVendor(e.target.value)}
        />
        <label className="mt-4 mb-1 ml-2 uppercase font-bold text-lg text-gray-900">
          Address of Vendor(ETH):
        </label>
        <input
          className="border mx-2 rounded shadow focus:outline-none focus:ring focus:ring-pink-500"
          value={vendor}
          placeholder="Enter ETH address of vendor"
          onChange={(e) => setVendor(e.target.value)}
        />
        <label className="mt-4 mb-1 ml-2 uppercase font-bold text-lg text-gray-900">
          Reason for Purchase:
        </label>
        <textarea
          className="border mx-2 rounded shadow  focus:outline-none focus:ring focus:ring-pink-500"
          value={reason}
          placeholder="Reason for purchase"
          onChange={(e) => setReason(e.target.value)}
        />
        <label className="mt-4 mb-1 ml-2 uppercase font-bold text-lg text-gray-900">
          Amount(ETH):
        </label>
        <input
          className="border mx-2 rounded shadow  focus:outline-none focus:ring focus:ring-pink-500"
          value={amount}
          placeholder="Enter amount in ETH"
          onChange={(e) => setAmount(e.target.value)}
        />
        <div className="flex justify-center items-center">
          <button
            type="submit"
            className="mt-8 w-1/2 py-2 mb-2 bg-pink-500 text-white font-bold rounded"
          >
            Create
          </button>
        </div>
        <div className="mt-4"></div>
      </form>
    </div>
  );
};

export default CreateDraft;

Appoint Staff

Now, we are going to create a page to let the Vice-Chancellor appoint staff. In your pages folder, create a new file called appoint_staff.js (pages/appoint_staff.js) and add the following code.

import { Contract, ethers } from "ethers";
import { useState } from "react";
import Web3Modal from "web3modal";
import { abi, PAYMENT_VOUCHER_CONTRACT_ADDRESS } from "../constants";
import React from "react";

const AppointStaff = () => {
  const [bursar, setBursar] = useState("");
  const [ECU, setECU] = useState("");
  const [loading, setLoading] = useState(false);

  const appoint = async (e) => {
    e.preventDefault();
    try {
        //get the signer to sign this transaction
      const web3Modal = new Web3Modal();
      const connection = await web3Modal.connect();
      const provider = new ethers.providers.Web3Provider(connection);
      const signer = provider.getSigner();

        //create an instance of the smart contract
      const paymentVoucherContract = new Contract(
        PAYMENT_VOUCHER_CONTRACT_ADDRESS,
        abi,
        signer
      );

        //call the function of the smart contract that appoints staff
      const tx = await paymentVoucherContract.appointStaff(bursar, ECU);
      setLoading(true);
      await tx.wait();
      setLoading(false);
      window.alert("You have successfully appointed new staff");
      setBursar("");
      setECU("");
    } catch (error) {
      console.error(error);
    }
  };
  if (loading) {
    return (
      <div className="min-h-screen flex justify-center items-center">
        <button className="mt-4 w-2/5 bg-pink-500 text-white font-bold py-2 px-4 rounded">
          Loading...
        </button>
      </div>
    );
  }

  return (
    <div className="min-h-screen flex flex-row justify-center items-center font-mono">
      <form
        onSubmit={appoint}
        // className="flex flex-col border-solid border-4 rounded-lg h-96 w-72"
        className=" flex flex-col border shadow-xl rounded-xl overflow-hidden h-96 w-72"
      >
        <h1 className="text-4xl w-full pl-4">Appoint Staff</h1>
        <div className="border mx-2 mt-2 shadow"></div>
        <label className="mt-8 mb-1 ml-2 uppercase font-bold text-lg text-gray-900">
          Bursar:
        </label>
        <input
          value={bursar}
          name="bursar"
          placeholder="Enter Bursar Address"
          onChange={(e) => setBursar(e.target.value)}
          className="border mx-2 rounded shadow focus:outline-none focus:ring focus:ring-pink-500"
        />
        <label className="mt-8 mb-1 ml-2 px-0 uppercase font-bold text-lg text-gray-900">
          Expenditure Control Unit:
        </label>
        <input
          value={ECU}
          name="ECU"
          placeholder="Enter ECU Address"
          onChange={(e) => setECU(e.target.value)}
          className="border mx-2 rounded shadow  focus:outline-none focus:ring focus:ring-pink-500"
        />
        <div className="flex mt-2 h-full justify-center items-center">
          <button
            type="submit"
            className="mt-2 w-1/2 bg-pink-500 text-white font-bold py-2 px-4 rounded"
          >
            Appoint
          </button>
        </div>
      </form>
    </div>
  );
};

export default AppointStaff;

You should note that this page is only visible to the Vice-Chancellor(the account that deploys the smart contract) because only him/her can appoint staff. So once the Vice-Chancellor is signed in, only him/her can see a button to appoint staff.

Approve Draft Page

Now, it's time to create a page to appoint staff. In this page which is only visible to the Vice-Chancellor, drafts are being approved. In your pages folder, create a new file called approve_draft.js (pages/approve_draft.js) and add the following code.

import { Contract, ethers } from "ethers";
import { useEffect, useState } from "react";
import Web3Modal from "web3modal";
import { abi, PAYMENT_VOUCHER_CONTRACT_ADDRESS } from "../constants";
import React from "react";

const ApproveDraft = () => {
  const [unApprovedDrafts, setUnApprovedDrafts] = useState([]);
  const [loadingState, setLoadingState] = useState("not-loaded");
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    loadUnApprovedDrafts();
  }, []);

  const loadUnApprovedDrafts = async () => {
    try {
      //get the provider
      const web3Modal = new Web3Modal();
      const connection = await web3Modal.connect();
      const provider = new ethers.providers.Web3Provider(connection);

      //get instance of the contract
      const paymentVoucherContract = new Contract(
        PAYMENT_VOUCHER_CONTRACT_ADDRESS,
        abi,
        provider
      );

      //call the unApprovedDrafts function
      const data = await paymentVoucherContract.getUnApprovedDrafts();
      const items = data.map((i) => {
        let price = ethers.utils.formatUnits(
          i.AmountToBePaid.toString(),
          "ether"
        );
        let item = {
          DraftId: i.DraftId.toNumber(),
          nameOfGoods: i.nameOfGoods,
          nameOfVendor: i.nameOfVendor,
          Vendor: i.Vendor,
          reason: i.reason,
          price,
          approved: i.approved.toString(),
          created: i.created.toString(),
        };
        return item;
      });
      setUnApprovedDrafts(items);
      setLoadingState("loaded");
    } catch (error) {
      console.error(error);
    }
  };

  if (loadingState === "loaded" && !unApprovedDrafts.length) {
    return <h1 className="py-10 px-20 text-3xl">No Unapproved Drafts</h1>;
  }

  if (loading) {
    return (
      <div className="min-h-screen flex justify-center items-center">
        <button className="mt-4 w-2/5 bg-pink-500 text-white font-bold py-2 px-4 rounded">
          Please Wait...
        </button>
      </div>
    );
  }

  const approveDraft = async (unApprovedDraft) => {
    try {
      //get the signer
      const web3Modal = new Web3Modal();
      const connection = await web3Modal.connect();
      const provider = new ethers.providers.Web3Provider(connection);
      const signer = provider.getSigner();

      //get instance of the contract
      const paymentVoucherContract = new Contract(
        PAYMENT_VOUCHER_CONTRACT_ADDRESS,
        abi,
        signer
      );

      const tx = await paymentVoucherContract.approveDraft(
        unApprovedDraft.DraftId
      );
      setLoading(true);
      await tx.wait();
      setLoading(false);
      window.alert("You have successfully approved the draft!");
      loadUnApprovedDrafts();
    } catch (error) {
      console.error(error);
    }
  };

  return (
    <div className="min-h-screen font-mono">
      <h1 className=" flex justify-center mt-2 text-3xl">Unapproved Drafts</h1>
      <div className="h-fit flex w-full mx-4 p-4 flex-wrap">
        {unApprovedDrafts.map((unApprovedDraft, i) => (
          <div
            key={i}
            className="flex flex-col border rounded-xl m-2 p-4 shadow-xl w-fit h-fit"
          >
            <div className="flex">
              <h3 className="font-extrabold">GOOD:</h3>
              <p className="pl-1">{unApprovedDraft.nameOfGoods}</p>
            </div>
            <div className="flex">
              <h3 className="font-extrabold">NAME OF VENDOR:</h3>
              <p className="pl-1">{unApprovedDraft.nameOfVendor}</p>
            </div>
            <div>
              <h3 className="font-extrabold">VENDOR ADDRESS(ETH):</h3>
              <p>{unApprovedDraft.Vendor}</p>
            </div>
            <div className="flex">
              <h3 className="font-extrabold">REASON:</h3>
              <p className="pl-1">{unApprovedDraft.reason}</p>
            </div>
            <div className="flex">
              <h3 className="font-extrabold">CREATED:</h3>
              <p className="pl-1">{unApprovedDraft.created}</p>
            </div>
            <div className="flex">
              <h3 className="font-extrabold">APPROVED:</h3>
              <p className="pl-1">{unApprovedDraft.approved}</p>
            </div>
            <div className="w-full bg-black rounded p-2">
              <div className="flex justify-center">
                <h3 className="font-semibold text-white text-base">
                  PRICE(ETH):
                </h3>
                <p className="pl-1 text-white">{unApprovedDraft.price} ETH</p>
              </div>
              <div className="w-full flex justify-center">
                <button
                  type="submit"
                  onClick={() => approveDraft(unApprovedDraft)}
                  className="flex justify-center rounded-lg w-1/2 text-white font-extrabold bg-pink-500"
                >
                  APPROVE
                </button>
              </div>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
};

export default ApproveDraft;

Creating a page to view all drafts

This page loads all of the created drafts. In your pages folder, create a new file called all_drafts.js (pages/all_drafts.js) and add the following code.

import { Contract, providers, ethers } from "ethers";
import { useEffect, useRef, useState } from "react";
import Web3Modal from "web3modal";
import { abi, PAYMENT_VOUCHER_CONTRACT_ADDRESS } from "../constants";
import React from "react";

const AllDrafts = () => {
  const [drafts, setDrafts] = useState([]);
  const [loadingState, setLoadingState] = useState("not-loaded");

  useEffect(() => {
    loadDrafts();
  }, []);

  const loadDrafts = async () => {
    try {
      const web3Modal = new Web3Modal();
      const connection = await web3Modal.connect();
      const provider = new ethers.providers.Web3Provider(connection);

      //get instance of the contract
      const paymentVoucherContract = new Contract(
        PAYMENT_VOUCHER_CONTRACT_ADDRESS,
        abi,
        provider
      );

      //call the get all drafts function
      const data = await paymentVoucherContract.getAllDrafts();
      const items = data.map((i) => {
        let price = ethers.utils.formatUnits(
          i.AmountToBePaid.toString(),
          "ether"
        );
        let item = {
          DraftId: i.DraftId.toNumber(),
          nameOfGoods: i.nameOfGoods,
          nameOfVendor: i.nameOfVendor,
          Vendor: i.Vendor,
          reason: i.reason,
          price,
          approved: i.approved.toString(),
          created: i.created.toString(),
        };
        return item;
      });
      setDrafts(items);
      setLoadingState("loaded");
    } catch (error) {
      console.error(error);
    }
  };

  if (loadingState === "loaded" && !drafts.length)
    return (
      <div className="min-h-screen flex justify-center items-center font-mono">
        <h1 className="py-10 px-20 text-3xl">No Drafts Created</h1>
      </div>
    );

  return (
    <div className="min-h-screen font-mono">
      <h1 className=" flex justify-center mt-2 text-3xl">All Drafts</h1>
      <div className="h-fit flex w-full mx-4 p-4 flex-wrap">
        {drafts.map((draft, i) => (
          <div
            key={i}
            className="flex flex-col border rounded-xl m-2 p-4 shadow-xl w-fit h-fit"
          >
            <div className="flex">
              <h3 className="font-extrabold">GOOD:</h3>
              <p className="pl-1">{draft.nameOfGoods}</p>
            </div>
            <div className="flex">
              <h3 className="font-extrabold">NAME OF VENDOR:</h3>
              <p className="pl-1">{draft.nameOfVendor}</p>
            </div>
            <div>
              <h3 className="font-extrabold">VENDOR ADDRESS(ETH):</h3>
              <p>{draft.Vendor}</p>
            </div>
            <div className="flex">
              <h3 className="font-extrabold">REASON:</h3>
              <p className="pl-1">{draft.reason}</p>
            </div>
            <div className="flex">
              <h3 className="font-extrabold">CREATED:</h3>
              <p className="pl-1">{draft.created}</p>
            </div>
            <div className="flex">
              <h3 className="font-extrabold">APPROVED:</h3>
              <p className="pl-1">{draft.approved}</p>
            </div>
            <div className="flex bg-pink-500 justify-center">
              <h3 className="font-extrabold text-white">PRICE(ETH):</h3>
              <p className="pl-1 text-white">{draft.price} ETH</p>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
};

export default AllDrafts;

Create Voucher Page

Now, we can create a page to create a voucher. Remember that only approved drafts can be converted to vouchers. So, this page loads all approved drafts which can then be converted to vouchers by the ECU. In your pages folder, create a new file called create_voucher.js (pages/create_voucher.js) and add the following code.

import { Contract, ethers } from "ethers";
import { useEffect, useState } from "react";
import Web3Modal from "web3modal";
import { abi, PAYMENT_VOUCHER_CONTRACT_ADDRESS } from "../constants";
import React from "react";

const CreateVoucher = () => {
  const [approvedDrafts, setApprovedDrafts] = useState([]);
  const [loadingState, setLoadingState] = useState("not-loaded");
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    loadApprovedDrafts();
  }, []);

  const loadApprovedDrafts = async () => {
    try {
      //get the provider
      const web3Modal = new Web3Modal();
      const connection = await web3Modal.connect();
      const provider = new ethers.providers.Web3Provider(connection);

      //get an instance of the contract
      const paymentVoucherContract = new Contract(
        PAYMENT_VOUCHER_CONTRACT_ADDRESS,
        abi,
        provider
      );

      //call the approved drafts function
      const data = await paymentVoucherContract.getApprovedDrafts();
      const items = data.map((i) => {
        let price = ethers.utils.formatUnits(
          i.AmountToBePaid.toString(),
          "ether"
        );
        let item = {
          DraftId: i.DraftId.toNumber(),
          nameOfGoods: i.nameOfGoods,
          nameOfVendor: i.nameOfVendor,
          Vendor: i.Vendor,
          reason: i.reason,
          price,
          approved: i.approved.toString(),
          created: i.created.toString(),
        };
        return item;
      });
      setApprovedDrafts(items);
      setLoadingState("loaded");
    } catch (error) {
      console.error(error);
    }
  };

  if (loadingState === "loaded" && !approvedDrafts.length) {
    return (
      <div className="min-h-screen flex justify-center items-center">
        <h1 className="py-10 px-20 text-3xl">No Approved Drafts</h1>
      </div>
    );
  }

  if (loading) {
    return (
      <div className="min-h-screen flex justify-center items-center">
        <button className="mt-4 w-2/5 bg-pink-500 text-white font-bold py-2 px-4 rounded">
          Please Wait...
        </button>
      </div>
    );
  }

  const create_Voucher = async (approvedDraft) => {
    try {
      //get the signer
      const web3Modal = new Web3Modal();
      const connection = await web3Modal.connect();
      const provider = new ethers.providers.Web3Provider(connection);
      const signer = provider.getSigner();

      //get instance of the contract
      const paymentVoucherContract = new Contract(
        PAYMENT_VOUCHER_CONTRACT_ADDRESS,
        abi,
        signer
      );

      //create a voucher
      const tx = await paymentVoucherContract.createVoucher(
        approvedDraft.DraftId
      );
      setLoading(true);
      await tx.wait();
      setLoading(false);
      window.alert("You have successfully created a Voucher!");
      loadApprovedDrafts();
    } catch (error) {
      console.error(error);
    }
  };

  return (
    <div className="min-h-screen font-mono">
      <h1 className=" flex justify-center mt-2 text-3xl">Approved Drafts</h1>
      <div className="h-fit flex w-full mx-4 p-4 flex-wrap">
        {approvedDrafts.map((approvedDraft, i) => (
          <div
            key={i}
            className="flex flex-col border rounded-xl m-2 p-4 shadow-xl w-fit h-fit"
          >
            <div className="flex">
              <h3 className="font-extrabold">GOOD:</h3>
              <p className="pl-1">{approvedDraft.nameOfGoods}</p>
            </div>
            <div className="flex">
              <h3 className="font-extrabold">NAME OF VENDOR:</h3>
              <p className="pl-1">{approvedDraft.nameOfVendor}</p>
            </div>
            <div>
              <h3 className="font-extrabold">VENDOR ADDRESS(ETH):</h3>
              <p>{approvedDraft.Vendor}</p>
            </div>
            <div className="flex">
              <h3 className="font-extrabold">REASON:</h3>
              <p className="pl-1">{approvedDraft.reason}</p>
            </div>
            <div className="flex">
              <h3 className="font-extrabold">CREATED:</h3>
              <p className="pl-1">{approvedDraft.created}</p>
            </div>
            <div className="flex">
              <h3 className="font-extrabold">APPROVED:</h3>
              <p className="pl-1">{approvedDraft.approved}</p>
            </div>
            <div className="w-full bg-black rounded p-2">
              <div className="flex justify-center">
                <h3 className="font-semibold text-white text-base">
                  PRICE(ETH):
                </h3>
                <p className="pl-1 text-white">{approvedDraft.price} ETH</p>
              </div>
              <div className="w-full flex justify-center">
                <button
                  type="submit"
                  onClick={() => create_Voucher(approvedDraft)}
                  className="flex justify-center rounded-lg w-1/2 text-white font-extrabold bg-pink-500"
                >
                  CREATE
                </button>
              </div>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
};

export default CreateVoucher;

Creating a page to view all vouchers

This page loads all of the created drafts. In your pages folder, create a new file called all_vouchers.js (pages/all_vouchers.js) and add the following code.

import { Contract, ethers } from "ethers";
import { useEffect, useState } from "react";
import Web3Modal from "web3modal";
import { abi, PAYMENT_VOUCHER_CONTRACT_ADDRESS } from "../constants";
import React from "react";

const all_vouchers = () => {
  const [vouchers, setVouchers] = useState([]);
  const [loadingState, setLoadingState] = useState("not-loaded");

  useEffect(() => {
    loadVouchers();
  }, []);

  const loadVouchers = async () => {
    try {
      //get the provider
      const web3Modal = new Web3Modal();
      const connection = await web3Modal.connect();
      const provider = new ethers.providers.Web3Provider(connection);

      //get an instance of the contract
      const paymentVoucherContract = new Contract(
        PAYMENT_VOUCHER_CONTRACT_ADDRESS,
        abi,
        provider
      );

      //call the get all vouchers function
      const data = await paymentVoucherContract.getAllVouchers();
      const items = data.map((i) => {
        let price = ethers.utils.formatUnits(i.toBePaid.toString(), "ether");
        let time = new Date(i.timePaid * 1000).toDateString();
        //let time = timeStamp.toDateString();
        let item = {
          id: i.id.toNumber(),
          draftId: i.draftId.toNumber(),
          price,
          Vendor: i.Vendor,
          nameOfVendor: i.nameOfVendor,
          nameOfGoods: i.nameOfGoods,
          Paid: i.Paid.toString(),
          created: i.created.toString(),
          time,
        };
        return item;
      });
      setVouchers(items);
      setLoadingState("loaded");
      console.log(items);
    } catch (error) {
      console.error(error);
    }
  };

  if (loadingState === "loaded" && !vouchers.length)
    return (
      <div className="min-h-screen flex justify-center items-center font-mono">
        <h1 className="py-10 px-20 text-3xl">No Vouchers Created</h1>
      </div>
    );

  return (
    <div className="min-h-screen font-mono">
      <h1 className="flex justify-center mt-2 text-3xl">All Vouchers</h1>
      <div className="h-fit flex w-full mx-4 p-4 flex-wrap">
        {vouchers.map((voucher, i) => (
          <div
            key={i}
            className="flex flex-col border rounded-xl m-2 p-4 shadow-xl w-fit h-fit"
          >
            <div className="flex">
              <h3 className="font-extrabold">GOOD:</h3>
              <p className="pl-1">{voucher.nameOfGoods}</p>
            </div>
            <div className="flex">
              <h3 className="font-extrabold">NAME OF VENDOR:</h3>
              <p className="pl-1">{voucher.nameOfVendor}</p>
            </div>
            <div>
              <h3 className="font-extrabold">VENDOR ADDRESS(ETH):</h3>
              <p>{voucher.Vendor}</p>
            </div>
            <div className="flex">
              <h3 className="font-extrabold">CREATED:</h3>
              <p className="pl-1">{voucher.created}</p>
            </div>
            <div className="flex">
              <h3 className="font-extrabold">TIME PAID:</h3>
              <p className="pl-1">
                {voucher.Paid === "true" ? voucher.time : "Not Paid"}
              </p>
            </div>
            <div className="flex">
              <h3 className="font-extrabold">PAID:</h3>
              <p className="pl-1">{voucher.Paid}</p>
            </div>
            <div className="flex bg-pink-500 justify-center">
              <h3 className="font-extrabold text-white">PRICE(ETH):</h3>
              <p className="pl-1 text-white">{voucher.price} ETH</p>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
};
export default all_vouchers;

Make Payments Page

This page allows the Bursar to make payments for unpaid vouchers. So, the page loads all unpaid vouchers with each voucher having a 'pay' button. In your pages folder, create a new file called make_payments.js (pages/make_payments.js) and add the following code.

import { Contract, ethers } from "ethers";
import { useEffect, useState } from "react";
import Web3Modal from "web3modal";
import { abi, PAYMENT_VOUCHER_CONTRACT_ADDRESS } from "../constants";
import React from "react";

const MakePayments = () => {
  const [unPaidVouchers, setUnPaidVouchers] = useState([]);
  const [loadingState, setLoadingState] = useState("not-loaded");
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    loadUnPaidVouchers();
  }, []);

  const loadUnPaidVouchers = async () => {
    try {
      //get the provider
      const web3Modal = new Web3Modal();
      const connection = await web3Modal.connect();
      const provider = new ethers.providers.Web3Provider(connection);

      //get instance of the contract
      const paymentVoucherContract = new Contract(
        PAYMENT_VOUCHER_CONTRACT_ADDRESS,
        abi,
        provider
      );

      //get all the unPaid Vouchers
      const data = await paymentVoucherContract.getUnPaidVouchers();
      const items = data.map((i) => {
        let price = ethers.utils.formatUnits(i.toBePaid.toString(), "ether");
        let time = new Date(i.timePaid * 1000).toDateString();
        let item = {
          id: i.id.toNumber(),
          draftId: i.draftId.toNumber(),
          price,
          Vendor: i.Vendor,
          nameOfVendor: i.nameOfVendor,
          nameOfGoods: i.nameOfGoods,
          Paid: i.Paid.toString(),
          created: i.created.toString(),
          time,
        };
        return item;
      });
      setUnPaidVouchers(items);
      setLoadingState("loaded");
    } catch (error) {
      console.error(error);
    }
  };

  if (loadingState === "loaded" && !unPaidVouchers.length) {
    return <h1 className="py-10 px-20 text-3xl">No Unpaid Vouchers</h1>;
  }

  if (loading) {
    return (
      <div className="min-h-screen flex justify-center items-center">
        <button className="mt-4 w-2/5 bg-pink-500 text-white font-bold py-2 px-4 rounded">
          Please Wait...
        </button>
      </div>
    );
  }

  const payVoucher = async (unPaidVoucher) => {
    try {
      //get the signer
      const web3Modal = new Web3Modal();
      const connection = await web3Modal.connect();
      const provider = new ethers.providers.Web3Provider(connection);
      const signer = provider.getSigner();

      //get instance of the contract
      const paymentVoucherContract = new Contract(
        PAYMENT_VOUCHER_CONTRACT_ADDRESS,
        abi,
        signer
      );

      const tx = await paymentVoucherContract.payforGoods(unPaidVoucher.id);
      setLoading(true);
      await tx.wait();
      setLoading(false);
      window.alert("You have successfully paid for the goods!");
      loadUnPaidVouchers();
    } catch (error) {
      console.error(error);
    }
  };

  return (
    <div className="min-h-screen font-mono">
      <h1 className=" flex justify-center mt-2 text-3xl">Unpaid Vouchers</h1>
      <div className="h-fit flex w-full mx-4 p-4 flex-wrap">
        {unPaidVouchers.map((unPaidVoucher, i) => (
          <div
            key={i}
            className="flex flex-col border rounded-xl m-2 p-4 shadow-xl w-fit h-fit"
          >
            <div className="flex">
              <h3 className="font-extrabold">GOOD:</h3>
              <p className="pl-1">{unPaidVoucher.nameOfGoods}</p>
            </div>
            <div className="flex">
              <h3 className="font-extrabold">NAME OF VENDOR:</h3>
              <p className="pl-1">{unPaidVoucher.nameOfVendor}</p>
            </div>
            <div>
              <h3 className="font-extrabold">VENDOR ADDRESS(ETH):</h3>
              <p>{unPaidVoucher.Vendor}</p>
            </div>
            <div className="flex">
              <h3 className="font-extrabold">DATE PAID:</h3>
              <p className="pl-1">
                {unPaidVoucher.Paid === "true" ? unPaidVoucher.time : "null"}
              </p>
            </div>
            <div className="flex">
              <h3 className="font-extrabold">CREATED:</h3>
              <p className="pl-1">{unPaidVoucher.created}</p>
            </div>
            <div className="flex">
              <h3 className="font-extrabold">PAID:</h3>
              <p className="pl-1">{unPaidVoucher.Paid}</p>
            </div>
            <div className="w-full bg-black rounded p-2">
              <div className="flex justify-center">
                <h3 className="font-semibold text-white text-base">
                  PRICE(ETH):
                </h3>
                <p className="pl-1 text-white">{unPaidVoucher.price} ETH</p>
              </div>
              <div className="w-full flex justify-center">
                <button
                  type="submit"
                  onClick={() => payVoucher(unPaidVoucher)}
                  className="flex justify-center rounded-lg w-1/2 text-white font-extrabold bg-pink-500"
                >
                  PAY
                </button>
              </div>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
};

export default MakePayments;

We are almost done with this project. Now, let's create an interface that allows us to fund this dApp.

Creating Fund-bursary Page

To create this page, your my-app folder, create a new folder called components and inside the components folder, create a new file called fundModal.jsx. So, your structure should look like this: my-app > components > fundModal.jsx. In your fundModal.jsx copy the following code.

import React from 'react';
import { Contract, providers, utils, ethers } from "ethers";
import { useState } from "react";
import Web3Modal from "web3modal";
import { abi, PAYMENT_VOUCHER_CONTRACT_ADDRESS } from "../constants";

const FundModal = props => {
    const [amount, setAmount] = useState("");
    const [loading, setLoading] = useState(false);

  //fundBursary: allows anyone to fund the bursary
  const FundBursary = async () => {
    try {
        //get the provider
      const web3Modal = new Web3Modal();
      const connection = await web3Modal.connect();
      const provider = new ethers.providers.Web3Provider(connection);
      const signer = provider.getSigner();

      //create an instance of the payment voucher contract
      const paymentVoucherContract = new Contract(
        PAYMENT_VOUCHER_CONTRACT_ADDRESS,
        abi,
        signer
      );

      //send funds to the contract
      const tx = {
        to: PAYMENT_VOUCHER_CONTRACT_ADDRESS,
        value: utils.parseEther(amount.toString()),
      };
      const tx1 = await signer.sendTransaction(tx);
      setLoading(true);
        //wait for the transaction
      await tx1.wait();
      setLoading(false);
      window.alert("You have succeessfully funded the Payment Voucher System");
      setAmount("");
    } catch (error) {
      console.error(error);
    }
  };



    if (loading) {
        return (
          <div className="min-h-screen flex justify-center items-center">
            <button className="mt-4 w-2/5 bg-pink-500 text-white font-bold py-2 px-4 rounded">
              Please Wait...
            </button>
          </div>
        );
      }

    return(
        <div className ="min-h-screen flex justify-center bg-tranparent items-center" onClick={props.OnClose}>
            <div className="border rounded-xl px-2 shadow-xl w-fit h-fit" onClick={e => e.stopPropagation()}>
                <div className="flex flex-col">
                    <h1 className="font-extrabold flex justify-center mt-2">Fund the Bursary</h1>
                    <div className="border mx-2 shadow"></div>
                </div>
                <div className="flex flex-col">
                   <label className="font-bold mt-4">Amount(ETH):</label>
                   <input
                     className="border rounded shadow  focus:outline-none focus:ring focus:ring-pink-500"
                     value={amount}
                     placeholder="Enter Amount in ETH"
                     onChange={(e) => setAmount(e.target.value)}
                   /> 
                </div>
                <div className="mt-6 flex justify-around">
                    <button className="rounded bg-gray-200 font-semibold mx-2 mb-2 w-1/2" onClick={props.OnClose}>CLOSE</button>
                    <button className="bg-pink-400 rounded text-white font-semibold mx-2 mb-2 w-1/2"onClick={FundBursary}>FUND</button>
                </div>
            </div>
        </div>
    );
}

export default FundModal;

Finally, we are going to get our smart contract's address and ABI inorder for our front-end to communicate with our smart contract.

Now, create a new folder under my-app and name it constants. And inside constants, create a file called index.js, and replace it with the following code.

Replace "address of your contract" with the address of the PaymentVoucher contract that you deployed and saved to your notepad. Replace ---your abi--- with the abi of your PaymentVoucher Contract. To get the abi for your contract, go to your smart-contract/artifacts/contracts/PaymentVoucher.sol folder and from your PaymentVouhcer.json file get the array marked under the "abi" key.

export const abi =---your abi---
export const PAYMENT_VOUCHER_CONTRACT_ADDRESS = "address of your contract"

Now in your terminal pointing to my-app folder, execute

npm run dev

Your Payment Voucher Management System should now work without errors.

Acknowledgements

Special thanks to Learnweb3DAO, for the awesome learning tracks and amazing community. Also, Nader Dabit's course on NFT marketplace helped me a lot while building this project.