From f2ccf5a406d42d801a219228f2261f4e1a8353be Mon Sep 17 00:00:00 2001 From: Nathanael Fredericko <79194414+nathfred@users.noreply.github.com> Date: Wed, 2 Apr 2025 21:00:00 +0700 Subject: [PATCH 1/3] Update UI --- packages/nextjs/app/expensesplitter/page.tsx | 54 ++++++++++++++++---- 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/packages/nextjs/app/expensesplitter/page.tsx b/packages/nextjs/app/expensesplitter/page.tsx index c7d797c..a727e07 100644 --- a/packages/nextjs/app/expensesplitter/page.tsx +++ b/packages/nextjs/app/expensesplitter/page.tsx @@ -6,7 +6,18 @@ import deployedContracts from "~~/contracts/deployedContracts"; const ExpenseSplitter = () => { const [amount, setAmount] = useState(""); - const [participants, setParticipants] = useState(""); + const [participants, setParticipants] = useState([]); + const [message, setMessage] = useState(""); + + const addParticipant = () => { + setParticipants([...participants, ""]); + }; + + const updateParticipant = (index: number, value: string) => { + const newParticipants = [...participants]; + newParticipants[index] = value; + setParticipants(newParticipants); + }; const addExpense = async () => { if (!window.ethereum) return alert("Install Metamask"); @@ -17,8 +28,8 @@ const ExpenseSplitter = () => { deployedContracts[31337].ExpenseSplitter.abi, signer, ); - const addresses = participants.split(",").map(addr => addr.trim()); - await contract.addExpense(addresses, { value: ethers.parseEther(amount) }); + await contract.addExpense(participants, { value: ethers.parseEther(amount) }); + setMessage("Expense added successfully!"); }; const withdrawFunds = async () => { @@ -31,17 +42,38 @@ const ExpenseSplitter = () => { signer, ); await contract.withdrawFunds(); + setMessage("Funds withdrawn successfully!"); }; 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) => ( + updateParticipant(index, e.target.value)} + /> + ))} + + + + {message &&
{message}
}
); }; From 1fd5cf7249018a0a64c5efe1116abb98e5ce84f3 Mon Sep 17 00:00:00 2001 From: Nathanael Fredericko <79194414+nathfred@users.noreply.github.com> Date: Wed, 2 Apr 2025 22:30:28 +0700 Subject: [PATCH 2/3] update UI --- .../hardhat/contracts/ExpenseSplitter.sol | 4 ++ packages/nextjs/app/expensesplitter/page.tsx | 71 ++++++++++++++----- .../nextjs/contracts/deployedContracts.ts | 13 ++++ 3 files changed, 69 insertions(+), 19 deletions(-) diff --git a/packages/hardhat/contracts/ExpenseSplitter.sol b/packages/hardhat/contracts/ExpenseSplitter.sol index ba4e97f..4f03023 100644 --- a/packages/hardhat/contracts/ExpenseSplitter.sol +++ b/packages/hardhat/contracts/ExpenseSplitter.sol @@ -5,12 +5,14 @@ contract ExpenseSplitter { address public owner; mapping(address => uint256) public balances; address[] public participants; + bool public isWithdrawn; event ExpenseAdded(address indexed payer, uint256 amount); event FundsWithdrawn(address indexed user, uint256 amount); constructor() { owner = msg.sender; + isWithdrawn = false; } function addExpense(address[] memory _participants) public payable { @@ -19,6 +21,7 @@ contract ExpenseSplitter { for (uint256 i = 0; i < _participants.length; i++) { balances[_participants[i]] += share; } + isWithdrawn = false; emit ExpenseAdded(msg.sender, msg.value); } @@ -27,6 +30,7 @@ contract ExpenseSplitter { require(amount > 0, "No funds to withdraw"); balances[msg.sender] = 0; payable(msg.sender).transfer(amount); + isWithdrawn = true; emit FundsWithdrawn(msg.sender, amount); } } \ No newline at end of file diff --git a/packages/nextjs/app/expensesplitter/page.tsx b/packages/nextjs/app/expensesplitter/page.tsx index a727e07..37069f5 100644 --- a/packages/nextjs/app/expensesplitter/page.tsx +++ b/packages/nextjs/app/expensesplitter/page.tsx @@ -1,23 +1,18 @@ "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 [message, setMessage] = useState(""); + // const [message, setMessage] = useState(""); + const [expenses, setExpenses] = useState([]); - const addParticipant = () => { - setParticipants([...participants, ""]); - }; - - const updateParticipant = (index: number, value: string) => { - const newParticipants = [...participants]; - newParticipants[index] = value; - setParticipants(newParticipants); - }; + useEffect(() => { + fetchExpenses(); + }, []); const addExpense = async () => { if (!window.ethereum) return alert("Install Metamask"); @@ -28,8 +23,24 @@ const ExpenseSplitter = () => { deployedContracts[31337].ExpenseSplitter.abi, signer, ); - await contract.addExpense(participants, { value: ethers.parseEther(amount) }); - setMessage("Expense added successfully!"); + const addresses = participants.map(addr => addr.trim()); + await contract.addExpense(addresses, { value: ethers.parseEther(amount) }); + }; + + 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 funds = await contract.balances(await signer.getAddress()); + const isWithdrawn = await contract.isWithdrawn(); + setExpenses([ + { address: contract.target, funds: ethers.formatEther(funds), status: isWithdrawn ? "Withdrawn" : "Active" }, + ]); }; const withdrawFunds = async () => { @@ -42,7 +53,6 @@ const ExpenseSplitter = () => { signer, ); await contract.withdrawFunds(); - setMessage("Funds withdrawn successfully!"); }; return ( @@ -61,19 +71,42 @@ const ExpenseSplitter = () => { type="text" placeholder="Participant Address" value={participant} - onChange={e => updateParticipant(index, e.target.value)} + onChange={e => + setParticipants([...participants.slice(0, index), e.target.value, ...participants.slice(index + 1)]) + } /> ))} - - - {message &&
{message}
} +
+

My Expense Splitters

+ {expenses.map((expense, index) => ( +
+ {expense.address} + {expense.funds} ETH + + {expense.status} + + + + +
+ ))} +
); }; diff --git a/packages/nextjs/contracts/deployedContracts.ts b/packages/nextjs/contracts/deployedContracts.ts index 6934013..414b556 100644 --- a/packages/nextjs/contracts/deployedContracts.ts +++ b/packages/nextjs/contracts/deployedContracts.ts @@ -84,6 +84,19 @@ const deployedContracts = { stateMutability: "view", type: "function", }, + { + inputs: [], + name: "isWithdrawn", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, { inputs: [], name: "owner", From 42ea28284508ec15ade501c12eb3cc6f2461818e Mon Sep 17 00:00:00 2001 From: Nathanael Fredericko <79194414+nathfred@users.noreply.github.com> Date: Wed, 2 Apr 2025 22:56:38 +0700 Subject: [PATCH 3/3] Major update : Support multiple expense per user --- .../hardhat/contracts/ExpenseSplitter.sol | 65 ++++++++++++----- packages/nextjs/app/expensesplitter/page.tsx | 71 ++++++++++++------- .../nextjs/contracts/deployedContracts.ts | 69 +++++++++++++----- 3 files changed, 146 insertions(+), 59 deletions(-) diff --git a/packages/hardhat/contracts/ExpenseSplitter.sol b/packages/hardhat/contracts/ExpenseSplitter.sol index 4f03023..338c0f9 100644 --- a/packages/hardhat/contracts/ExpenseSplitter.sol +++ b/packages/hardhat/contracts/ExpenseSplitter.sol @@ -2,35 +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; - bool public isWithdrawn; + 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; - isWithdrawn = false; + 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; + } } - isWithdrawn = false; - 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); - isWithdrawn = true; - 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 37069f5..d3709b3 100644 --- a/packages/nextjs/app/expensesplitter/page.tsx +++ b/packages/nextjs/app/expensesplitter/page.tsx @@ -7,8 +7,7 @@ import deployedContracts from "~~/contracts/deployedContracts"; const ExpenseSplitter = () => { const [amount, setAmount] = useState(""); const [participants, setParticipants] = useState([]); - // const [message, setMessage] = useState(""); - const [expenses, setExpenses] = useState([]); + const [expenses, setExpenses] = useState<{ id: number; amount: string; status: string }[]>([]); useEffect(() => { fetchExpenses(); @@ -23,8 +22,11 @@ const ExpenseSplitter = () => { deployedContracts[31337].ExpenseSplitter.abi, signer, ); + const addresses = participants.map(addr => addr.trim()); - await contract.addExpense(addresses, { value: ethers.parseEther(amount) }); + const tx = await contract.addExpense(addresses, { value: ethers.parseEther(amount) }); + await tx.wait(); + fetchExpenses(); }; const fetchExpenses = async () => { @@ -36,14 +38,18 @@ const ExpenseSplitter = () => { deployedContracts[31337].ExpenseSplitter.abi, signer, ); - const funds = await contract.balances(await signer.getAddress()); - const isWithdrawn = await contract.isWithdrawn(); - setExpenses([ - { address: contract.target, funds: ethers.formatEther(funds), status: isWithdrawn ? "Withdrawn" : "Active" }, - ]); + + 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(); @@ -52,7 +58,10 @@ const ExpenseSplitter = () => { deployedContracts[31337].ExpenseSplitter.abi, signer, ); - await contract.withdrawFunds(); + + const tx = await contract.withdrawFunds(expenseId); + await tx.wait(); + fetchExpenses(); }; return ( @@ -62,6 +71,7 @@ const ExpenseSplitter = () => { className="p-2 border rounded" type="text" placeholder="Amount in ETH" + value={amount} onChange={e => setAmount(e.target.value)} /> {participants.map((participant, index) => ( @@ -90,22 +100,31 @@ const ExpenseSplitter = () => {

My Expense Splitters

- {expenses.map((expense, index) => ( -
- {expense.address} - {expense.funds} ETH - - {expense.status} - - - - -
- ))} + {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 414b556..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,19 +78,23 @@ const deployedContracts = { type: "function", }, { - inputs: [ + inputs: [], + name: "getMyExpenses", + outputs: [ { - internalType: "address", + internalType: "uint256[]", name: "", - type: "address", + type: "uint256[]", }, - ], - name: "balances", - outputs: [ { - internalType: "uint256", + internalType: "uint256[]", name: "", - type: "uint256", + type: "uint256[]", + }, + { + internalType: "bool[]", + name: "", + type: "bool[]", }, ], stateMutability: "view", @@ -86,12 +102,12 @@ const deployedContracts = { }, { inputs: [], - name: "isWithdrawn", + name: "nextExpenseId", outputs: [ { - internalType: "bool", + internalType: "uint256", name: "", - type: "bool", + type: "uint256", }, ], stateMutability: "view", @@ -112,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",