diff --git a/packages/hardhat/contracts/ExpenseSplitter.sol b/packages/hardhat/contracts/ExpenseSplitter.sol index ba4e97f..338c0f9 100644 --- a/packages/hardhat/contracts/ExpenseSplitter.sol +++ b/packages/hardhat/contracts/ExpenseSplitter.sol @@ -2,31 +2,66 @@ pragma solidity ^0.8.19; contract ExpenseSplitter { + struct Expense { + uint256 id; + uint256 amount; + bool isWithdrawn; + } + address public owner; - mapping(address => uint256) public balances; - address[] public participants; + mapping(address => Expense[]) public userExpenses; + uint256 public nextExpenseId; - event ExpenseAdded(address indexed payer, uint256 amount); - event FundsWithdrawn(address indexed user, uint256 amount); + event ExpenseAdded(address indexed payer, uint256 expenseId, uint256 amount); + event FundsWithdrawn(address indexed user, uint256 expenseId, uint256 amount); constructor() { owner = msg.sender; + nextExpenseId = 1; } function addExpense(address[] memory _participants) public payable { require(msg.value > 0, "Must send ETH"); + require(_participants.length > 0, "No participants provided"); + uint256 share = msg.value / _participants.length; for (uint256 i = 0; i < _participants.length; i++) { - balances[_participants[i]] += share; + userExpenses[_participants[i]].push(Expense(nextExpenseId, share, false)); + } + + emit ExpenseAdded(msg.sender, nextExpenseId, msg.value); + nextExpenseId++; + } + + function withdrawFunds(uint256 expenseId) public { + Expense[] storage expenses = userExpenses[msg.sender]; + for (uint256 i = 0; i < expenses.length; i++) { + if (expenses[i].id == expenseId && !expenses[i].isWithdrawn) { + uint256 amount = expenses[i].amount; + expenses[i].isWithdrawn = true; + payable(msg.sender).transfer(amount); + + emit FundsWithdrawn(msg.sender, expenseId, amount); + return; + } } - emit ExpenseAdded(msg.sender, msg.value); + revert("No withdrawable funds found for this expenseId"); } - function withdrawFunds() public { - uint256 amount = balances[msg.sender]; - require(amount > 0, "No funds to withdraw"); - balances[msg.sender] = 0; - payable(msg.sender).transfer(amount); - emit FundsWithdrawn(msg.sender, amount); + function getMyExpenses() public view returns (uint256[] memory, uint256[] memory, bool[] memory) { + Expense[] storage expenses = userExpenses[msg.sender]; + uint256 length = expenses.length; + + uint256[] memory ids = new uint256[](length); + uint256[] memory amounts = new uint256[](length); + bool[] memory statuses = new bool[](length); + + for (uint256 i = 0; i < length; i++) { + ids[i] = expenses[i].id; + amounts[i] = expenses[i].amount; + statuses[i] = expenses[i].isWithdrawn; + } + + return (ids, amounts, statuses); } -} \ No newline at end of file +} diff --git a/packages/nextjs/app/expensesplitter/page.tsx b/packages/nextjs/app/expensesplitter/page.tsx index c7d797c..d3709b3 100644 --- a/packages/nextjs/app/expensesplitter/page.tsx +++ b/packages/nextjs/app/expensesplitter/page.tsx @@ -1,12 +1,17 @@ "use client"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { ethers } from "ethers"; import deployedContracts from "~~/contracts/deployedContracts"; const ExpenseSplitter = () => { const [amount, setAmount] = useState(""); - const [participants, setParticipants] = useState(""); + const [participants, setParticipants] = useState([]); + const [expenses, setExpenses] = useState<{ id: number; amount: string; status: string }[]>([]); + + useEffect(() => { + fetchExpenses(); + }, []); const addExpense = async () => { if (!window.ethereum) return alert("Install Metamask"); @@ -17,11 +22,34 @@ const ExpenseSplitter = () => { deployedContracts[31337].ExpenseSplitter.abi, signer, ); - const addresses = participants.split(",").map(addr => addr.trim()); - await contract.addExpense(addresses, { value: ethers.parseEther(amount) }); + + const addresses = participants.map(addr => addr.trim()); + const tx = await contract.addExpense(addresses, { value: ethers.parseEther(amount) }); + await tx.wait(); + fetchExpenses(); + }; + + const fetchExpenses = async () => { + if (!window.ethereum) return; + const provider = new ethers.BrowserProvider(window.ethereum); + const signer = await provider.getSigner(); + const contract = new ethers.Contract( + deployedContracts[31337].ExpenseSplitter.address, + deployedContracts[31337].ExpenseSplitter.abi, + signer, + ); + + const [ids, amounts, statuses] = await contract.getMyExpenses(); + const formattedExpenses = ids.map((id: bigint, index: number) => ({ + id: Number(id), + amount: ethers.formatEther(amounts[index]), + status: statuses[index] ? "Withdrawn" : "Active", + })); + + setExpenses(formattedExpenses); }; - const withdrawFunds = async () => { + const withdrawFunds = async (expenseId: number) => { if (!window.ethereum) return alert("Install Metamask"); const provider = new ethers.BrowserProvider(window.ethereum); const signer = await provider.getSigner(); @@ -30,18 +58,74 @@ const ExpenseSplitter = () => { deployedContracts[31337].ExpenseSplitter.abi, signer, ); - await contract.withdrawFunds(); + + const tx = await contract.withdrawFunds(expenseId); + await tx.wait(); + fetchExpenses(); }; return ( -
-

Decentralized Expense Splitter

- setAmount(e.target.value)} /> - setParticipants(e.target.value)} /> - - -

Withdraw Funds

- +
+

Decentralized Expense Splitter

+ setAmount(e.target.value)} + /> + {participants.map((participant, index) => ( + + setParticipants([...participants.slice(0, index), e.target.value, ...participants.slice(index + 1)]) + } + /> + ))} + + + +
+

My Expense Splitters

+ {expenses.length > 0 ? ( + expenses.map((expense, index) => ( +
+ ID: {expense.id} + {expense.amount} ETH + + {expense.status} + + {!expense.status.includes("Withdrawn") && ( + + )} +
+ )) + ) : ( +

No expenses found

+ )} +
); }; diff --git a/packages/nextjs/contracts/deployedContracts.ts b/packages/nextjs/contracts/deployedContracts.ts index 6934013..29a12b8 100644 --- a/packages/nextjs/contracts/deployedContracts.ts +++ b/packages/nextjs/contracts/deployedContracts.ts @@ -23,6 +23,12 @@ const deployedContracts = { name: "payer", type: "address", }, + { + indexed: false, + internalType: "uint256", + name: "expenseId", + type: "uint256", + }, { indexed: false, internalType: "uint256", @@ -42,6 +48,12 @@ const deployedContracts = { name: "user", type: "address", }, + { + indexed: false, + internalType: "uint256", + name: "expenseId", + type: "uint256", + }, { indexed: false, internalType: "uint256", @@ -66,14 +78,31 @@ const deployedContracts = { type: "function", }, { - inputs: [ + inputs: [], + name: "getMyExpenses", + outputs: [ { - internalType: "address", + internalType: "uint256[]", name: "", - type: "address", + type: "uint256[]", + }, + { + internalType: "uint256[]", + name: "", + type: "uint256[]", + }, + { + internalType: "bool[]", + name: "", + type: "bool[]", }, ], - name: "balances", + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "nextExpenseId", outputs: [ { internalType: "uint256", @@ -99,25 +128,46 @@ const deployedContracts = { }, { inputs: [ + { + internalType: "address", + name: "", + type: "address", + }, { internalType: "uint256", name: "", type: "uint256", }, ], - name: "participants", + name: "userExpenses", outputs: [ { - internalType: "address", - name: "", - type: "address", + internalType: "uint256", + name: "id", + type: "uint256", + }, + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + internalType: "bool", + name: "isWithdrawn", + type: "bool", }, ], stateMutability: "view", type: "function", }, { - inputs: [], + inputs: [ + { + internalType: "uint256", + name: "expenseId", + type: "uint256", + }, + ], name: "withdrawFunds", outputs: [], stateMutability: "nonpayable",