Introduction
Managing expenses on a blockchain ensures transparency, security, and decentralization. In this blog, we’ll walk through building a Blockchain-based Expense Management DApp using Conflux eSpace, Solidity, Hardhat, and React. This DApp allows users to record expenses, categorize them, and approve transactions, ensuring a trustless and verifiable expense tracking system.
Features
Deploying a Solidity smart contract for expense management
Integrating the contract with a React frontend using ethers.js
Allowing users to list expenses, approve, or decline transactions
Fetching on-chain data and displaying transaction history
Smart Contract Development
First, let’s create the ExpenseManagement contract in Solidity. This contract will store expenses and allow approvals.
- Setting Up the Smart Contract
Create a new Solidity file ExpenseManagement.sol
:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract ExpenseManagement {
// Struct to represent an expense
struct Expense {
uint256 id;
string title;
string description;
string category;
uint256 amount; // Amount in ETH
address destinationAddress;
address createdBy; // Address of the user who created the expense
bool isAccepted;
bool isRejected;
}
// Mapping to store expenses by their ID
mapping(uint256 => Expense) public expenses;
// Mapping to store expense IDs by the address of the user who created them
mapping(address => uint256[]) public expensesByCreator;
// Counter to generate unique IDs for expenses
uint256 public expenseCounter;
// Event to log when an expense is created
event ExpenseCreated(
uint256 id,
string title,
string description,
string category,
uint256 amount,
address destinationAddress,
address createdBy
);
// Event to log when an expense is accepted
event ExpenseAccepted(uint256 id, uint256 amount, address destinationAddress);
// Event to log when an expense is rejected
event ExpenseRejected(uint256 id);
// Function to create a new expense
function createExpense(
string memory _title,
string memory _description,
string memory _category,
uint256 _amount,
address _destinationAddress
) public {
require(_amount > 0, "Amount must be greater than 0");
require(_destinationAddress != address(0), "Invalid destination address");
expenseCounter++;
expenses[expenseCounter] = Expense({
id: expenseCounter,
title: _title,
description: _description,
category: _category,
amount: _amount,
destinationAddress: _destinationAddress,
createdBy: msg.sender, // Store the address of the user who created the expense
isAccepted: false,
isRejected: false
});
// Add the expense ID to the list of expenses created by this user
expensesByCreator[msg.sender].push(expenseCounter);
emit ExpenseCreated(
expenseCounter,
_title,
_description,
_category,
_amount,
_destinationAddress,
msg.sender
);
}
// Function to accept an expense and transfer the amount to the destination address
function acceptExpense(uint256 _id) public payable {
require(expenses[_id].id != 0, "Expense does not exist");
require(!expenses[_id].isAccepted, "Expense is already accepted");
require(!expenses[_id].isRejected, "Expense is already rejected");
Expense storage expense = expenses[_id];
// Ensure the contract has enough balance to transfer the amount
require(address(this).balance >= expense.amount, "Insufficient contract balance");
// Mark the expense as accepted
expense.isAccepted = true;
// Transfer the amount to the destination address
(bool success, ) = expense.destinationAddress.call{value: expense.amount}("");
require(success, "Transfer failed");
emit ExpenseAccepted(_id, expense.amount, expense.destinationAddress);
}
// Function to reject an expense
function rejectExpense(uint256 _id) public {
require(expenses[_id].id != 0, "Expense does not exist");
require(!expenses[_id].isAccepted, "Expense is already accepted");
require(!expenses[_id].isRejected, "Expense is already rejected");
expenses[_id].isRejected = true;
emit ExpenseRejected(_id);
}
// Function to get details of an expense by ID
function getExpense(uint256 _id) public view returns (
uint256 id,
string memory title,
string memory description,
string memory category,
uint256 amount,
address destinationAddress,
address createdBy,
bool isAccepted,
bool isRejected
) {
require(expenses[_id].id != 0, "Expense does not exist");
Expense memory expense = expenses[_id];
return (
expense.id,
expense.title,
expense.description,
expense.category,
expense.amount,
expense.destinationAddress,
expense.createdBy,
expense.isAccepted,
expense.isRejected
);
}
// Function to get all expense IDs created by a specific address
function getExpensesByCreator(address _creator) public view returns (uint256[] memory) {
return expensesByCreator[_creator];
}
// Fallback function to accept ETH deposits into the contract
receive() external payable {}
}
2. Deploying the Contract Using Hardhat
Create a Hardhat script to deploy the contract:
const hre = require("hardhat");
// const fs = require('fs');
async function main() {
const Expense = await hre.ethers.getContractFactory("ExpenseManagement")
const expense = await Expense.deploy();
await expense.deployed();
console.log("Expense Management deployed to:", expense.address);
// fs.writeFileSync('./config.js', `export const marketplaceAddress = "${nftMarketplace.address}"`)
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
Frontend Integration (React + ethers.js)
1. Fetching Expenses from the Smart Contract
Create a React component to retrieve and display expenses:
useEffect(() => {
const fetchExpenses = async () => {
if (!window.ethereum) {
console.error("Please install MetaMask or another Ethereum wallet");
return;
}
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
const contract = new ethers.Contract(marketplaceAddress, abi.abi, signer);
try {
const userAddress = await signer.getAddress();
const expenseIds = await contract.getExpensesByCreator(userAddress);
const expensesData = await Promise.all(
expenseIds.map(async (id: number) => {
const expense = await contract.getExpense(id);
return {
id: expense.id.toNumber(),
title: expense.title,
description: expense.description,
category: expense.category,
amount: ethers.utils.formatEther(expense.amount),
date: new Date().toISOString().split("T")[0], // Assuming you want the current date
destinationAddress: expense.destinationAddress,
};
})
);
setExpenses(expensesData);
} catch (error) {
console.error("Error fetching expenses:", error);
}
};
fetchExpenses();
}, []);
2. Approving an Expense
const handleAcceptExpense = async (id: number) => {
if (!window.ethereum) {
console.error("Please install MetaMask or another Ethereum wallet")
return
}
setLoadingApprove((prev) => ({ ...prev, [id]: true }))
const provider = new ethers.providers.Web3Provider(window.ethereum)
const signer = provider.getSigner()
const contract = new ethers.Contract(marketplaceAddress, abi.abi, signer)
try {
const transaction = await contract.acceptExpense(id)
const receipt = await transaction.wait()
if (receipt.status === 1) {
setExpenses((prevExpenses) =>
prevExpenses.map((expense) =>
expense.id === id ? { ...expense, isAccepted: true, transactionHash: receipt.transactionHash } : expense,
),
)
}
} catch (error) {
console.error("Error accepting expense:", error)
} finally {
setLoadingApprove((prev) => ({ ...prev, [id]: false }))
}
}
const handleDeclineExpense = async (id: number) => {
if (!window.ethereum) {
console.error("Please install MetaMask or another Ethereum wallet")
return
}
setLoadingDecline((prev) => ({ ...prev, [id]: true }))
const provider = new ethers.providers.Web3Provider(window.ethereum)
const signer = provider.getSigner()
const contract = new ethers.Contract(marketplaceAddress, abi.abi, signer)
try {
const transaction = await contract.rejectExpense(id)
const receipt = await transaction.wait()
if (receipt.status === 1) {
setExpenses((prevExpenses) =>
prevExpenses.map((expense) => (expense.id === id ? { ...expense, isRejected: true } : expense)),
)
}
} catch (error) {
console.error("Error declining expense:", error)
} finally {
setLoadingDecline((prev) => ({ ...prev, [id]: false }))
}
}
Conclusion
This Expense Management DApp leverages blockchain for transparent and secure expense tracking. By using Solidity for smart contracts, Hardhat for deployment, and React with ethers.js for frontend integration, we create a fully functional decentralized expense tracking system. Stay tuned for more Web3 & Blockchain development tutorials!
Resources
Project Code: https://github.com/Vikash-8090-Yadav/Expenditure-Dapp
Video Tutorial: https://youtu.be/V1DaZI0qWwU?si=tHHPJJtTE0uB-BRJ
Don’t forget to share and subscribe for more Web3 tutorials!