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:
Node.js version
16.14.0
or greater installed on your machine. I recommend installing Node using either nvm.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.