Building a Blockchain-Based Expense Management DApp on Conflux eSpace

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

:white_check_mark: Deploying a Solidity smart contract for expense management
:white_check_mark: Integrating the contract with a React frontend using ethers.js
:white_check_mark: Allowing users to list expenses, approve, or decline transactions
:white_check_mark: 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.

  1. 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! :rocket:

Resources

:link: Project Code: https://github.com/Vikash-8090-Yadav/Expenditure-Dapp

:link: Video Tutorial: https://youtu.be/V1DaZI0qWwU?si=tHHPJJtTE0uB-BRJ

Don’t forget to share and subscribe for more Web3 tutorials! :fire: