From 505f6d98c8e56dd890aa14145c19abb8ddaba225 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=85=9A=F0=9F=85=90=F0=9F=85=A1=F0=9F=85=90?= =?UTF-8?q?=F0=9F=85=9D?= Date: Wed, 4 Jun 2025 14:47:53 +0530 Subject: [PATCH 01/21] - --- .gitmodules | 6 +++ contracts/lib/chainlink-brownie-contracts | 1 + contracts/lib/forge-std | 1 + frontend/.env | 2 +- frontend/src/App.css | 0 frontend/src/App.jsx | 2 +- frontend/src/contractsABI/ChainvoiceABI.js | 58 ++++++---------------- 7 files changed, 26 insertions(+), 44 deletions(-) create mode 100644 .gitmodules create mode 160000 contracts/lib/chainlink-brownie-contracts create mode 160000 contracts/lib/forge-std delete mode 100644 frontend/src/App.css diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..5c6b89f7 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "contracts/lib/forge-std"] + path = contracts/lib/forge-std + url = https://github.com/foundry-rs/forge-std +[submodule "contracts/lib/chainlink-brownie-contracts"] + path = contracts/lib/chainlink-brownie-contracts + url = https://github.com/smartcontractkit/chainlink-brownie-contracts diff --git a/contracts/lib/chainlink-brownie-contracts b/contracts/lib/chainlink-brownie-contracts new file mode 160000 index 00000000..67887b84 --- /dev/null +++ b/contracts/lib/chainlink-brownie-contracts @@ -0,0 +1 @@ +Subproject commit 67887b84d3add02a25ef4145fc014e2f549509da diff --git a/contracts/lib/forge-std b/contracts/lib/forge-std new file mode 160000 index 00000000..3b20d60d --- /dev/null +++ b/contracts/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 3b20d60d14b343ee4f908cb8079495c07f5e8981 diff --git a/frontend/.env b/frontend/.env index e761d5b0..8a6d8689 100644 --- a/frontend/.env +++ b/frontend/.env @@ -5,7 +5,7 @@ ViTE_EXPLORER=https://sepolia.etherscan.io/ # -VITE_CONTRACT_ADDRESS=0x11B4894d1D723FB24310f28E58796d452eBb5aDe +VITE_CONTRACT_ADDRESS=0x6E7362a86192F0A0A9AF5F0804EcEEDF92dDb0e7 VITE_PRICEFEED_ADDRESS=0xAE26bD30c88Cc8bcB9b9Be97De7Ad33315fb3D65 # 0x00eca3bd631b3623680d9a511bd89dd5fc965dea79104b02cf6d74ad8eaf31a6 diff --git a/frontend/src/App.css b/frontend/src/App.css deleted file mode 100644 index e69de29b..00000000 diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index ec859072..939f0854 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -14,7 +14,7 @@ import { BrowserRouter as Router, Route, Routes } from 'react-router-dom' import './App.css' import Landing from './page/Landing' import Applayout from './page/Applayout' - + import { citreaTestnet } from './utils/CitreaTestnet'; import Home from './page/Home'; import Feature from './page/Feature'; diff --git a/frontend/src/contractsABI/ChainvoiceABI.js b/frontend/src/contractsABI/ChainvoiceABI.js index 4c3976a3..07fb7160 100644 --- a/frontend/src/contractsABI/ChainvoiceABI.js +++ b/frontend/src/contractsABI/ChainvoiceABI.js @@ -1,13 +1,7 @@ export const ChainvoiceABI = [ { "type": "constructor", - "inputs": [ - { - "name": "_priceFeed", - "type": "address", - "internalType": "address" - } - ], + "inputs": [], "stateMutability": "nonpayable" }, { @@ -164,7 +158,7 @@ export const ChainvoiceABI = [ }, { "type": "function", - "name": "feeAmountInUSD", + "name": "fee", "inputs": [], "outputs": [ { @@ -177,10 +171,10 @@ export const ChainvoiceABI = [ }, { "type": "function", - "name": "getMyReceivedInvoices", + "name": "getReceivedInvoices", "inputs": [ { - "name": "add", + "name": "_address", "type": "address", "internalType": "address" } @@ -344,8 +338,14 @@ export const ChainvoiceABI = [ }, { "type": "function", - "name": "getMySentInvoices", - "inputs": [], + "name": "getSentInvoices", + "inputs": [ + { + "name": "_address", + "type": "address", + "internalType": "address" + } + ], "outputs": [ { "name": "", @@ -503,19 +503,6 @@ export const ChainvoiceABI = [ ], "stateMutability": "view" }, - { - "type": "function", - "name": "getTreasuryAddress", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "address" - } - ], - "stateMutability": "view" - }, { "type": "function", "name": "invoices", @@ -767,9 +754,9 @@ export const ChainvoiceABI = [ "name": "setFeeAmount", "inputs": [ { - "name": "fee", - "type": "uint16", - "internalType": "uint16" + "name": "_fee", + "type": "uint256", + "internalType": "uint256" } ], "outputs": [], @@ -780,7 +767,7 @@ export const ChainvoiceABI = [ "name": "setTreasuryAddress", "inputs": [ { - "name": "add", + "name": "newTreauserAdd", "type": "address", "internalType": "address" } @@ -801,19 +788,6 @@ export const ChainvoiceABI = [ ], "stateMutability": "view" }, - { - "type": "function", - "name": "usdToNativeCurrencyConversion", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, { "type": "function", "name": "withdraw", From 5a178ba47bd838baa35c4451d873c446ea583db6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=85=9A=F0=9F=85=90=F0=9F=85=A1=F0=9F=85=90?= =?UTF-8?q?=F0=9F=85=9D?= Date: Mon, 9 Jun 2025 13:53:28 +0530 Subject: [PATCH 02/21] removed env --- frontend/.env | 18 ------------------ frontend/.env-copy | 0 frontend/.gitignore | 3 ++- 3 files changed, 2 insertions(+), 19 deletions(-) create mode 100644 frontend/.env-copy diff --git a/frontend/.env b/frontend/.env index 8a6d8689..e69de29b 100644 --- a/frontend/.env +++ b/frontend/.env @@ -1,18 +0,0 @@ -# Sepolia testnet rpc URL -VITE_BLOCKCHAIN_URI=https://ethereum-sepolia.publicnode.com/ -VITE_CHAIN_ID=11155111 -ViTE_EXPLORER=https://sepolia.etherscan.io/ - - -# -VITE_CONTRACT_ADDRESS=0x6E7362a86192F0A0A9AF5F0804EcEEDF92dDb0e7 -VITE_PRICEFEED_ADDRESS=0xAE26bD30c88Cc8bcB9b9Be97De7Ad33315fb3D65 -# 0x00eca3bd631b3623680d9a511bd89dd5fc965dea79104b02cf6d74ad8eaf31a6 - -# 0x24F13d40CF7DE6a81a2a1949aA45F2242e81f1e2 - -# forge create --rpc-url https://ethereum-sepolia.publicnode.com/ --private-key d198e5f47b1b0f09b8412805be0b90b3ee1321eec231f86a5cc884d3f460f1e2 src/Chainvoice.sol:Chainvoice --broadcast --constructor-args 0x694AA1769357215DE4FAC081bf1f309aDC325306 - -# ETH/USD Mainnet: 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419 -# ETH/USD Sepolia: 0x694AA1769357215DE4FAC081bf1f309aDC325306 -# ETH/USD Goerli: 0xD4a33860578De61DBAbDc8BFdb98FD742fA7028e diff --git a/frontend/.env-copy b/frontend/.env-copy new file mode 100644 index 00000000..e69de29b diff --git a/frontend/.gitignore b/frontend/.gitignore index 68b18a8f..bf1f56f8 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -11,6 +11,7 @@ node_modules dist dist-ssr *.local +.env # Editor directories and files .vscode/* @@ -22,4 +23,4 @@ dist-ssr *.ntvs* *.njsproj *.sln -*.sw? +*.sw? \ No newline at end of file From 61894ff43bd9662fc00e591ba5304af2e909dcf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=85=9A=F0=9F=85=90=F0=9F=85=A1=F0=9F=85=90?= =?UTF-8?q?=F0=9F=85=9D?= Date: Mon, 9 Jun 2025 13:55:14 +0530 Subject: [PATCH 03/21] removed chainlink --- contracts/lib/chainlink-brownie-contracts | 1 - 1 file changed, 1 deletion(-) delete mode 160000 contracts/lib/chainlink-brownie-contracts diff --git a/contracts/lib/chainlink-brownie-contracts b/contracts/lib/chainlink-brownie-contracts deleted file mode 160000 index 67887b84..00000000 --- a/contracts/lib/chainlink-brownie-contracts +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 67887b84d3add02a25ef4145fc014e2f549509da From a6c2fac16c927f1703986e3802cf92c3c21b658b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=85=9A=F0=9F=85=90=F0=9F=85=A1=F0=9F=85=90?= =?UTF-8?q?=F0=9F=85=9D?= Date: Mon, 9 Jun 2025 13:57:23 +0530 Subject: [PATCH 04/21] removed chainlink --- frontend/.env | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 frontend/.env diff --git a/frontend/.env b/frontend/.env deleted file mode 100644 index e69de29b..00000000 From 3231f303c837344f3b0e0ad876edb5c6a06767dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=85=9A=F0=9F=85=90=F0=9F=85=A1=F0=9F=85=90?= =?UTF-8?q?=F0=9F=85=9D?= Date: Mon, 9 Jun 2025 14:00:02 +0530 Subject: [PATCH 05/21] added env-copy --- frontend/.env-copy | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/frontend/.env-copy b/frontend/.env-copy index e69de29b..dd86d5ba 100644 --- a/frontend/.env-copy +++ b/frontend/.env-copy @@ -0,0 +1,15 @@ +# env-copy + +# Blockchain RPC URL (e.g., Sepolia, Ethereum Classic, etc.) +VITE_BLOCKCHAIN_URI=https://your-network-rpc-url/ + +# Network Chain ID (e.g., 11155111 for Sepolia, 61 for Ethereum Classic) +VITE_CHAIN_ID=your_chain_id_here + +# Block explorer URL for your network +VITE_EXPLORER=https://your-network-explorer-url/ + +# Deployed contract address (update after deployment) +VITE_CONTRACT_ADDRESS=0xYourContractAddressHere + + From 8a05a038c8890b8bc5162dbca723d0ab8379f8d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=85=9A=F0=9F=85=90=F0=9F=85=A1=F0=9F=85=90?= =?UTF-8?q?=F0=9F=85=9D?= Date: Mon, 9 Jun 2025 14:05:28 +0530 Subject: [PATCH 06/21] removed unecessary folder --- contracts/src/MockV3Aggregator.sol | 66 ------------------------------ 1 file changed, 66 deletions(-) delete mode 100644 contracts/src/MockV3Aggregator.sol diff --git a/contracts/src/MockV3Aggregator.sol b/contracts/src/MockV3Aggregator.sol deleted file mode 100644 index c00a076b..00000000 --- a/contracts/src/MockV3Aggregator.sol +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import {AggregatorV3Interface} from "../lib/chainlink-brownie-contracts/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol"; - -contract MockV3Aggregator is AggregatorV3Interface { - uint256 public constant version = 4; - - uint8 public decimals; - int256 public latestAnswer; - uint256 public latestTimestamp; - uint256 public latestRound; - - mapping(uint256 => int256) public getAnswer; - mapping(uint256 => uint256) public getTimestamp; - mapping(uint256 => uint256) private getStartedAt; - - constructor(uint8 _decimals, int256 _initialAnswer) { - decimals = _decimals; - updateAnswer(_initialAnswer); - } - - function updateAnswer(int256 _answer) public { - latestAnswer = _answer; - latestTimestamp = block.timestamp; - latestRound++; - getAnswer[latestRound] = _answer; - getTimestamp[latestRound] = block.timestamp; - getStartedAt[latestRound] = block.timestamp; - } - - function updateRoundData(uint80 _roundId, int256 _answer, uint256 _timestamp, uint256 _startedAt) public { - latestRound = _roundId; - latestAnswer = _answer; - latestTimestamp = _timestamp; - getAnswer[latestRound] = _answer; - getTimestamp[latestRound] = _timestamp; - getStartedAt[latestRound] = _startedAt; - } - - function getRoundData(uint80 _roundId) - external - view - returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) - { - return (_roundId, getAnswer[_roundId], getStartedAt[_roundId], getTimestamp[_roundId], _roundId); - } - - function latestRoundData() - external - view - returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) - { - return ( - uint80(latestRound), - getAnswer[latestRound], - getStartedAt[latestRound], - getTimestamp[latestRound], - uint80(latestRound) - ); - } - - function description() external pure returns (string memory) { - return "v0.6/test/mock/MockV3Aggregator.sol"; - } -} \ No newline at end of file From 91c19254066c93ffd05ce9523253dd083f7f18c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=85=9A=F0=9F=85=90=F0=9F=85=A1=F0=9F=85=90?= =?UTF-8?q?=F0=9F=85=9D?= Date: Mon, 9 Jun 2025 14:24:51 +0530 Subject: [PATCH 07/21] changed app.jsx --- frontend/src/App.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 939f0854..5edf1be7 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -11,7 +11,6 @@ import { } from "@tanstack/react-query"; import * as chains from "wagmi/chains"; import { BrowserRouter as Router, Route, Routes } from 'react-router-dom' -import './App.css' import Landing from './page/Landing' import Applayout from './page/Applayout' From 86982f753b6d45350f82fda148163e88a5e015fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=85=9A=F0=9F=85=90=F0=9F=85=A1=F0=9F=85=90?= =?UTF-8?q?=F0=9F=85=9D?= Date: Mon, 9 Jun 2025 18:26:48 +0530 Subject: [PATCH 08/21] readme update --- README.md | 43 +++++++++++++++++++++++++++++++++++++++++++ contracts/.env-copy | 11 +++++++++++ 2 files changed, 54 insertions(+) create mode 100644 contracts/.env-copy diff --git a/README.md b/README.md index fc05779c..485a9fee 100644 --- a/README.md +++ b/README.md @@ -45,3 +45,46 @@ forge install ```bash forge test ``` +## Deploying to Ethereum Classic (ETC) with Foundry + +This guide explains how to deploy Chainvoice smart contracts to the Ethereum Classic Mainnet using Foundry (Forge). + +Prerequisites +- Foundry installed +- A funded wallet with ETC +- RPC URL (e.g. from Rivet, Ankr, or Chainstack) + +1. Create .env File for Secrets + + - Create a .env in your project root i.e. `contracts/` + - Copy all the varible from `contracts/.env-copy` to newly created `.env` + + `cp .env-copy .env` + - Assign valid values to the variable. + +2. Compile Contract + + `forge build` +3. Load your .env in the terminal + + `source .env` +4. Deploy the Contract using forge create + +``` +forge create src/Contract.sol:Contract \ + --rpc-url $SEPOLIA_RPC_URL \ + --private-key $PRIVATE_KEY \ + --broadcast \ +``` +5. Finally add Contract Address to Frontend `.env` + - Create a new .env file by copying .env-copy: + + `cp frontend/.env-copy frontend/.env` + - Open the new .env file and update the variables, especially: + `VITE_CONTRACT_ADDRESS=your_deployed_contract_address_here` + + Replace your_deployed_contract_address_here with the actual contract address you got after deployment. + + - Save the .env file. + + - Restart your frontend development server so the new environment variables are loaded. \ No newline at end of file diff --git a/contracts/.env-copy b/contracts/.env-copy new file mode 100644 index 00000000..586d96d4 --- /dev/null +++ b/contracts/.env-copy @@ -0,0 +1,11 @@ +# Wallet Private Key +PRIVATE_KEY=your_private_key_here + +# Ethereum Sepolia +SEPOLIA_RPC_URL=https://ethereum-sepolia.publicnode.com/ +SEPOLIA_CHAIN_ID=11155111 +SEPOLIA_EXPLORER=https://sepolia.etherscan.io/ + +# Ethereum Classic +ETC_RPC_URL=https://etc.rivet.link +ETC_CHAIN_ID=61 From e7c3f4120944978774d565a365b37ba00e764e30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=85=9A=F0=9F=85=90=F0=9F=85=A1=F0=9F=85=90?= =?UTF-8?q?=F0=9F=85=9D?= Date: Mon, 9 Jun 2025 18:36:06 +0530 Subject: [PATCH 09/21] readme update --- README.md | 10 +++++----- contracts/{.env-copy => .env.example} | 0 frontend/{.env-copy => .env.example} | 0 3 files changed, 5 insertions(+), 5 deletions(-) rename contracts/{.env-copy => .env.example} (100%) rename frontend/{.env-copy => .env.example} (100%) diff --git a/README.md b/README.md index 485a9fee..4984b471 100644 --- a/README.md +++ b/README.md @@ -57,9 +57,9 @@ Prerequisites 1. Create .env File for Secrets - Create a .env in your project root i.e. `contracts/` - - Copy all the varible from `contracts/.env-copy` to newly created `.env` + - Copy all the varible from `contracts/.env.example` to newly created `.env` - `cp .env-copy .env` + `cp .env.example .env` - Assign valid values to the variable. 2. Compile Contract @@ -77,9 +77,9 @@ forge create src/Contract.sol:Contract \ --broadcast \ ``` 5. Finally add Contract Address to Frontend `.env` - - Create a new .env file by copying .env-copy: + - Create a new .env file by copying .env.example: - `cp frontend/.env-copy frontend/.env` + `cp frontend/.env.example frontend/.env` - Open the new .env file and update the variables, especially: `VITE_CONTRACT_ADDRESS=your_deployed_contract_address_here` @@ -87,4 +87,4 @@ forge create src/Contract.sol:Contract \ - Save the .env file. - - Restart your frontend development server so the new environment variables are loaded. \ No newline at end of file + - Restart your frontend development server so the new environment variables are loaded. diff --git a/contracts/.env-copy b/contracts/.env.example similarity index 100% rename from contracts/.env-copy rename to contracts/.env.example diff --git a/frontend/.env-copy b/frontend/.env.example similarity index 100% rename from frontend/.env-copy rename to frontend/.env.example From 77f3b144c048ca499a0d91aad8a23ba9c4f16214 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=85=9A=F0=9F=85=90=F0=9F=85=A1=F0=9F=85=90?= =?UTF-8?q?=F0=9F=85=9D?= Date: Tue, 17 Jun 2025 01:33:25 +0530 Subject: [PATCH 10/21] updated contract --- contracts/src/Chainvoice.sol | 171 ++++++++++++++--------------------- 1 file changed, 69 insertions(+), 102 deletions(-) diff --git a/contracts/src/Chainvoice.sol b/contracts/src/Chainvoice.sol index 9b25397e..ec9c8cee 100644 --- a/contracts/src/Chainvoice.sol +++ b/contracts/src/Chainvoice.sol @@ -1,174 +1,141 @@ // SPDX-License-Identifier: Unlicense pragma solidity ^0.8.13; -import {Test} from "../lib/forge-std/src/Test.sol"; -import {console} from "../lib/forge-std/src/console.sol"; contract Chainvoice { - struct UserDetails { - string fname; - string lname; - string email; - string country; - string city; - string postalcode; - } - struct ItemData { - string description; - int256 qty; - int256 unitPrice; - int256 discount; - int256 tax; - int256 amount; - } struct InvoiceDetails { uint256 id; address from; - string dueDate; - string issueDate; - UserDetails user; // Struct to store user details address to; - UserDetails client; // Struct to store client details - uint256 amountDue; bool isPaid; + string encryptedInvoiceData; // Base64-encoded ciphertext + string encryptedHash; } InvoiceDetails[] public invoices; + mapping(address => uint256[]) public sentInvoices; mapping(address => uint256[]) public receivedInvoices; - mapping(uint256 => ItemData[]) public itemDatas; address public owner; address public treasuryAddress; uint256 public fee; + uint256 public accumulatedFees; - constructor() { - owner = msg.sender; - fee = 500000000000000 ; //0.0005 ether - } - - modifier OnlyOwner() { - require(msg.sender == owner, "Only Owner is accessible"); - _; - } event InvoiceCreated( - uint256 id, + uint256 indexed id, address indexed from, - address indexed to, - uint256 amountDue + address indexed to ); + event InvoicePaid( - uint256 id, + uint256 indexed id, address indexed from, address indexed to, - uint256 amountPaid + uint256 amount ); + constructor() { + owner = msg.sender; + fee = 0.0005 ether; + } + + modifier onlyOwner() { + require(msg.sender == owner, "Only owner can call"); + _; + } + function createInvoice( - uint256 amountDue, address to, - string memory _dueDate, - string memory _issueDate, - UserDetails memory user, - UserDetails memory client, - ItemData[] memory _items + string memory encryptedInvoiceData, + string memory encryptedHash ) external { - require(amountDue > 0, "Amount must be greater than zero"); - require(to != address(0), "Receiver address cannot be zero"); - require(to != msg.sender, "Cannot create invoice for yourself"); + require(to != address(0), "Recipient address is zero"); + require(to != msg.sender, "Self-invoicing not allowed"); uint256 invoiceId = invoices.length; + invoices.push( InvoiceDetails({ id: invoiceId, from: msg.sender, - dueDate:_dueDate, - issueDate: _issueDate, - user: user, to: to, - client: client, - amountDue: amountDue, - isPaid: false + isPaid: false, + encryptedInvoiceData: encryptedInvoiceData, + encryptedHash:encryptedHash }) ); - for (uint256 i = 0; i < _items.length; i++) { - itemDatas[invoiceId].push(_items[i]); - } - sentInvoices[msg.sender].push(invoiceId); receivedInvoices[to].push(invoiceId); - emit InvoiceCreated(invoiceId, msg.sender, to, amountDue); + emit InvoiceCreated(invoiceId, msg.sender, to); } - uint256 public accumulatedFees; - function payInvoice(uint256 invoiceId) external payable { require(invoiceId < invoices.length, "Invalid invoice ID"); + InvoiceDetails storage invoice = invoices[invoiceId]; - require(msg.sender == invoice.to, "Not authorized to pay this invoice"); - require(!invoice.isPaid, "Invoice already paid"); - require( - msg.value >= invoice.amountDue + fee, - "Payment must cover the invoice amount and fee" - ); - uint256 amountToRecipient = msg.value - fee; - (bool success, ) = payable(invoice.from).call{value: amountToRecipient}( - "" - ); - require(success, "Payment transfer failed"); + require(msg.sender == invoice.to, "Not authorized"); + require(!invoice.isPaid, "Already paid"); + require(msg.value > fee, "Insufficient payment for invoice + fee"); + + uint256 amountToSender = msg.value - fee; accumulatedFees += fee; + + (bool sent, ) = payable(invoice.from).call{value: amountToSender}(""); + require(sent, "Transfer failed"); + invoice.isPaid = true; + + emit InvoicePaid(invoiceId, invoice.from, invoice.to, msg.value); } - function getSentInvoices(address _address) - external - view - returns (InvoiceDetails[] memory, ItemData[][] memory) - { - return _getInvoices(sentInvoices[_address]); + function getSentInvoices( + address user + ) external view returns (InvoiceDetails[] memory) { + return _getInvoices(sentInvoices[user]); } function getReceivedInvoices( - address _address - ) external view returns (InvoiceDetails[] memory, ItemData[][] memory) { - return _getInvoices(receivedInvoices[_address]); + address user + ) external view returns (InvoiceDetails[] memory) { + return _getInvoices(receivedInvoices[user]); } function _getInvoices( - uint256[] storage invoiceIds - ) internal view returns (InvoiceDetails[] memory, ItemData[][] memory) { - InvoiceDetails[] memory userInvoices = new InvoiceDetails[]( - invoiceIds.length - ); - ItemData[][] memory items = new ItemData[][](invoiceIds.length); - - for (uint256 i = 0; i < invoiceIds.length; i++) { - userInvoices[i] = invoices[invoiceIds[i]]; - items[i] = itemDatas[invoiceIds[i]]; + uint256[] storage ids + ) internal view returns (InvoiceDetails[] memory) { + InvoiceDetails[] memory result = new InvoiceDetails[](ids.length); + for (uint256 i = 0; i < ids.length; i++) { + result[i] = invoices[ids[i]]; } - - return (userInvoices, items); + return result; } - function setTreasuryAddress(address newTreauserAdd) public OnlyOwner { - require(newTreauserAdd != address(0), "Treasury Address cannot be equal to zero"); - treasuryAddress = newTreauserAdd; + function getInvoice( + uint256 invoiceId + ) external view returns (InvoiceDetails memory) { + require(invoiceId < invoices.length, "Invalid ID"); + return invoices[invoiceId]; } - - function setFeeAmount(uint256 _fee) public OnlyOwner { + function setFeeAmount(uint256 _fee) external onlyOwner { fee = _fee; } - function withdraw() external { - require(treasuryAddress != address(0), "Treasury address not set"); - require(accumulatedFees > 0, "No fees to withdraw"); - + function setTreasuryAddress(address newTreasury) external onlyOwner { + require(newTreasury != address(0), "Zero address"); + treasuryAddress = newTreasury; + } + + function withdrawFees() external { + require(treasuryAddress != address(0), "Treasury not set"); + require(accumulatedFees > 0, "No fees available"); + uint256 amount = accumulatedFees; accumulatedFees = 0; (bool success, ) = payable(treasuryAddress).call{value: amount}(""); - require(success, "Fee withdrawal failed"); + require(success, "Withdraw failed"); } } From 3c9220a2b93abe56f094dd027ab794a2e54f862b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=85=9A=F0=9F=85=90=F0=9F=85=A1=F0=9F=85=90?= =?UTF-8?q?=F0=9F=85=9D?= Date: Tue, 17 Jun 2025 01:34:46 +0530 Subject: [PATCH 11/21] updated UI and contract ABI --- frontend/package.json | 4 + frontend/src/components/CreateInvoice.jsx | 601 ++++++++++++++------- frontend/src/contractsABI/ChainvoiceABI.js | 564 +++---------------- frontend/src/page/ReceivedInvoice.jsx | 461 ++++++++++++---- frontend/src/page/SentInvoice.jsx | 490 ++++++++++++----- frontend/vite.config.js | 2 +- 6 files changed, 1186 insertions(+), 936 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index f2069cb9..ce268161 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,6 +12,10 @@ "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", + "@lit-protocol/auth-helpers": "^7.2.0", + "@lit-protocol/constants": "^7.2.0", + "@lit-protocol/encryption": "^7.2.0", + "@lit-protocol/lit-node-client": "^7.2.0", "@mui/icons-material": "^6.4.6", "@mui/material": "^6.4.6", "@radix-ui/react-label": "^2.1.1", diff --git a/frontend/src/components/CreateInvoice.jsx b/frontend/src/components/CreateInvoice.jsx index eda38146..c37f5141 100644 --- a/frontend/src/components/CreateInvoice.jsx +++ b/frontend/src/components/CreateInvoice.jsx @@ -1,20 +1,30 @@ -import React, { useEffect, useState } from 'react'; -import { Input } from './ui/input'; -import { Button } from './ui/button'; -import { BrowserProvider, Contract, ethers } from 'ethers'; -import { useAccount, useWalletClient } from 'wagmi'; -import { ChainvoiceABI } from '../contractsABI/ChainvoiceABI'; +import React, { useEffect, useState } from "react"; +import { Input } from "./ui/input"; +import { Button } from "./ui/button"; +import { BrowserProvider, Contract, ethers } from "ethers"; +import { useAccount, useWalletClient } from "wagmi"; +import { ChainvoiceABI } from "../contractsABI/ChainvoiceABI"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { Calendar } from "@/components/ui/calendar"; -import { CalendarIcon, Loader2 } from 'lucide-react'; -import { cn } from '@/lib/utils'; -import { format } from 'date-fns'; -import { Label } from './ui/label'; -import { useNavigate } from 'react-router-dom'; +import { CalendarIcon, Loader2 } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { format } from "date-fns"; +import { Label } from "./ui/label"; +import { useNavigate } from "react-router-dom"; + +import { LitNodeClient } from "@lit-protocol/lit-node-client"; +import { encryptString } from "@lit-protocol/encryption/src/lib/encryption.js"; +import { LIT_ABILITY, LIT_NETWORK } from "@lit-protocol/constants"; +import { + createSiweMessageWithRecaps, + generateAuthSig, + LitAccessControlConditionResource, +} from "@lit-protocol/auth-helpers"; + function CreateInvoice() { const { data: walletClient } = useWalletClient(); const { isConnected } = useAccount(); @@ -23,6 +33,74 @@ function CreateInvoice() { const [issueDate, setIssueDate] = useState(new Date()); const [loading, setLoading] = useState(false); const navigate = useNavigate(); + + const [itemData, setItemData] = useState([ + { + description: "", + qty: "", + unitPrice: "", + discount: "", + tax: "", + amount: "", + }, + ]); + + const [totalAmountDue, setTotalAmountDue] = useState(0); + + useEffect(() => { + let total = itemData.reduce((sum, item) => { + return ( + sum + + ((parseFloat(item.qty) || 0) * (parseFloat(item.unitPrice) || 0) - + (parseFloat(item.discount) || 0) + + (parseFloat(item.tax) || 0)) + ); + }, 0); + + setTotalAmountDue(total); + }, [itemData]); + + const handleItemData = (e, index) => { + const { name, value } = e.target; + + setItemData((prevItemData) => + prevItemData.map((item, i) => { + if (i === index) { + const updatedItem = { ...item, [name]: value }; + if ( + name === "qty" || + name === "unitPrice" || + name === "discount" || + name === "tax" + ) { + const qty = parseFloat(updatedItem.qty) || 0; + const unitPrice = parseFloat(updatedItem.unitPrice) || 0; + const discount = parseFloat(updatedItem.discount) || 0; + const tax = parseFloat(updatedItem.tax) || 0; + updatedItem.amount = (qty * unitPrice - discount + tax).toString(); + } + return updatedItem; + } + return item; + }) + ); + }; + + const addItem = () => { + setItemData((prev) => [ + ...prev, + { + description: "", + qty: "", + unitPrice: "", + discount: "", + tax: "", + amount: "", + }, + ]); + console.log(itemData.length); + }; + const createInvoiceRequest = async (data) => { if (!isConnected || !walletClient) { alert("Please connect your wallet"); @@ -33,68 +111,135 @@ function CreateInvoice() { setLoading(true); const provider = new BrowserProvider(walletClient); const signer = await provider.getSigner(); + + console.log("data >>>>>> ", data); + console.log("acc :", account.address); + // 1. Prepare invoice data + const invoicePayload = { + amountDue: totalAmountDue, + dueDate, + issueDate, + user: { + address: account?.address.toString(), + fname: data.userFname, + lname: data.userLname, + email: data.userEmail, + country: data.userCountry, + city: data.userCity, + postalcode: data.userPostalcode, + }, + client: { + address: data.clientAddress, + fname: data.clientFname, + lname: data.clientLname, + email: data.clientEmail, + country: data.clientCountry, + city: data.clientCity, + postalcode: data.clientPostalcode, + }, + items: itemData, + }; + + const invoiceString = JSON.stringify(invoicePayload); + + // 2. Setup Lit + const litNodeClient = new LitNodeClient({ + litNetwork: LIT_NETWORK.DatilDev, + debug: false, + }); + await litNodeClient.connect(); + + const accessControlConditions = [ + { + contractAddress: "", + standardContractType: "", + chain: "ethereum", + method: "", + parameters: [":userAddress"], + returnValueTest: { + comparator: "=", + value: account.address.toLowerCase(), + }, + }, + { operator: "or" }, + { + contractAddress: "", + standardContractType: "", + chain: "ethereum", + method: "", + parameters: [":userAddress"], + returnValueTest: { + comparator: "=", + value: data.clientAddress.toLowerCase(), + }, + }, + ]; + + // 3. Encrypt + const { ciphertext, dataToEncryptHash } = await encryptString( + { + accessControlConditions, + dataToEncrypt: invoiceString, + }, + litNodeClient + ); + + const sessionSigs = await litNodeClient.getSessionSigs({ + chain: "ethereum", + resourceAbilityRequests: [ + { + resource: new LitAccessControlConditionResource("*"), + ability: LIT_ABILITY.AccessControlConditionDecryption, + }, + ], + authNeededCallback: async ({ + uri, + expiration, + resourceAbilityRequests, + }) => { + const nonce = await litNodeClient.getLatestBlockhash(); + const toSign = await createSiweMessageWithRecaps({ + uri, + expiration, + resources: resourceAbilityRequests, + walletAddress: account.address, + nonce, + litNodeClient, + }); + + return await generateAuthSig({ + signer, + toSign, + }); + }, + }); + + const encryptedStringBase64 = btoa(ciphertext); + + // 4. Send to contract const contract = new Contract( import.meta.env.VITE_CONTRACT_ADDRESS, ChainvoiceABI, signer ); - const sanitizedItemData = data.itemData.map((item) => ({ - ...item, - qty: item.qty ? String(item.qty) : "0", - unitPrice: item.unitPrice ? ethers.parseUnits(String(item.unitPrice), 18) : ethers.parseUnits("0", 18), - discount: item.discount ? String(item.discount) : "0", - tax: item.tax ? String(item.tax) : "0", - amount: item.amount ? ethers.parseUnits(String(item.amount), 18) : ethers.parseUnits("0", 18), - })); - - console.log(sanitizedItemData); - const res = await contract.createInvoice( - ethers.parseUnits(String(totalAmountDue), 18), + + const tx = await contract.createInvoice( data.clientAddress, - dueDate.toLocaleString(undefined,{timeZone:"Asia/Kolkata"}).toString(), - issueDate.toLocaleString(undefined,{timeZone:"Asia/Kolkata"}).toString(), - [ - data.userFname, - data.userLname, - data.userEmail, - data.userCountry, - data.userCity, - data.userPostalcode, - ], - [ - data.clientFname, - data.clientLname, - data.clientEmail, - data.clientCountry, - data.clientCity, - data.clientPostalcode, - ], - sanitizedItemData + encryptedStringBase64, + dataToEncryptHash ); - setTimeout(()=>{ - navigate('/home/sent'); - },4000); - console.log("Transaction successful", res); - } catch (error) { - console.error("Invoice creation error:", error); - setLoading(false); - alert("Failed to create invoice"); + + console.log("Invoice created:", tx); + setTimeout(() => navigate("/home/sent"), 4000); + } catch (err) { + console.error("Encryption or transaction failed:", err); + alert("Failed to create invoice."); } finally { setLoading(false); } }; - - const [itemData, setItemData] = useState([{ - description: '', - qty: '', - unitPrice: '', - discount: '', - tax: '', - amount: '' - }]); - - const handleSubmit = (e) => { + const handleSubmit = async (e) => { e.preventDefault(); const formData = new FormData(e.target); @@ -131,60 +276,18 @@ function CreateInvoice() { clientCountry, clientCity, clientPostalcode, - itemData + itemData, }; console.log(data); - createInvoiceRequest(data); + await createInvoiceRequest(data); }; - const [totalAmountDue, setTotalAmountDue] = useState(0); - const handleItemData = (e, index) => { - const { name, value } = e.target; - - setItemData((prevItemData) => - prevItemData.map((item, i) => { - if (i === index) { - const updatedItem = { ...item, [name]: value }; - if (name === 'qty' || name === 'unitPrice' || name === 'discount' || name === 'tax') { - const qty = parseFloat(updatedItem.qty) || 0; - const unitPrice = parseFloat(updatedItem.unitPrice) || 0; - const discount = parseFloat(updatedItem.discount) || 0; - const tax = parseFloat(updatedItem.tax) || 0; - updatedItem.amount = (qty * unitPrice - discount + tax).toString(); - } - return updatedItem; - } - return item; - }) - ); - }; - - const addItem = () => { - setItemData((prev) => [...prev, { - description: '', - qty: '', - unitPrice: '', - discount: '', - tax: '', - amount: '', - }]); - console.log(itemData.length); - }; - - useEffect(() => { - let total = itemData.reduce((sum, item) => { - return sum + ((parseFloat(item.qty) || 0) * (parseFloat(item.unitPrice) || 0) - (parseFloat(item.discount) || 0) + (parseFloat(item.tax) || 0)); - }, 0); - - setTotalAmountDue(total); - }, [itemData]); - return ( -
+

Create New Invoice Request

- - + +

Issued Date

@@ -223,106 +329,217 @@ function CreateInvoice() {
-
- -
-

From (Your Information)

- -
-

Add Your Info

-
- - +
+
+

From (Your Information)

+ +
+

Add Your Info

+
+ +
-
- - +
+ +
-
- - +
+ +
-
-

Client Information

- -
-

Add Client Info

-
- - +
+

Client Information

+ +
+

Add Client Info

+
+ +
-
- - +
+ +
-
- - +
+ +
-
-
-
DESCRIPTION
-
QTY
-
UNIT PRICE
-
DISCOUNT
-
TAX(%)
-
AMOUNT
+
+
+
DESCRIPTION
+
QTY
+
UNIT PRICE
+
DISCOUNT
+
TAX(%)
+
AMOUNT
-
- { - itemData - .map((_, index) => ( -
- handleItemData(e, index)} /> - handleItemData(e, index)} /> - handleItemData(e, index)} /> - handleItemData(e, index)} /> - handleItemData(e, index)} /> - handleItemData(e, index)} - /> - -
- )) - } +
+ {itemData.map((_, index) => ( +
+ handleItemData(e, index)} + /> + handleItemData(e, index)} + /> + handleItemData(e, index)} + /> + handleItemData(e, index)} + /> + handleItemData(e, index)} + /> + handleItemData(e, index)} + /> +
+ ))}
-

Total : {totalAmountDue}

-
- - { - loading ? ( - - ) : ( - - ) - } +

Total : {totalAmountDue}

+
+ + {loading ? ( + + ) : ( + + )}
-
- ) + ); } -export default CreateInvoice - +export default CreateInvoice; diff --git a/frontend/src/contractsABI/ChainvoiceABI.js b/frontend/src/contractsABI/ChainvoiceABI.js index 07fb7160..93e18b37 100644 --- a/frontend/src/contractsABI/ChainvoiceABI.js +++ b/frontend/src/contractsABI/ChainvoiceABI.js @@ -21,136 +21,20 @@ export const ChainvoiceABI = [ "type": "function", "name": "createInvoice", "inputs": [ - { - "name": "amountDue", - "type": "uint256", - "internalType": "uint256" - }, { "name": "to", "type": "address", "internalType": "address" }, { - "name": "_dueDate", + "name": "encryptedInvoiceData", "type": "string", "internalType": "string" }, { - "name": "_issueDate", + "name": "encryptedHash", "type": "string", "internalType": "string" - }, - { - "name": "user", - "type": "tuple", - "internalType": "struct Chainvoice.UserDetails", - "components": [ - { - "name": "fname", - "type": "string", - "internalType": "string" - }, - { - "name": "lname", - "type": "string", - "internalType": "string" - }, - { - "name": "email", - "type": "string", - "internalType": "string" - }, - { - "name": "country", - "type": "string", - "internalType": "string" - }, - { - "name": "city", - "type": "string", - "internalType": "string" - }, - { - "name": "postalcode", - "type": "string", - "internalType": "string" - } - ] - }, - { - "name": "client", - "type": "tuple", - "internalType": "struct Chainvoice.UserDetails", - "components": [ - { - "name": "fname", - "type": "string", - "internalType": "string" - }, - { - "name": "lname", - "type": "string", - "internalType": "string" - }, - { - "name": "email", - "type": "string", - "internalType": "string" - }, - { - "name": "country", - "type": "string", - "internalType": "string" - }, - { - "name": "city", - "type": "string", - "internalType": "string" - }, - { - "name": "postalcode", - "type": "string", - "internalType": "string" - } - ] - }, - { - "name": "_items", - "type": "tuple[]", - "internalType": "struct Chainvoice.ItemData[]", - "components": [ - { - "name": "description", - "type": "string", - "internalType": "string" - }, - { - "name": "qty", - "type": "int256", - "internalType": "int256" - }, - { - "name": "unitPrice", - "type": "int256", - "internalType": "int256" - }, - { - "name": "discount", - "type": "int256", - "internalType": "int256" - }, - { - "name": "tax", - "type": "int256", - "internalType": "int256" - }, - { - "name": "amount", - "type": "int256", - "internalType": "int256" - } - ] } ], "outputs": [], @@ -171,19 +55,19 @@ export const ChainvoiceABI = [ }, { "type": "function", - "name": "getReceivedInvoices", + "name": "getInvoice", "inputs": [ { - "name": "_address", - "type": "address", - "internalType": "address" + "name": "invoiceId", + "type": "uint256", + "internalType": "uint256" } ], "outputs": [ { "name": "", - "type": "tuple[]", - "internalType": "struct Chainvoice.InvoiceDetails[]", + "type": "tuple", + "internalType": "struct Chainvoice.InvoiceDetails", "components": [ { "name": "id", @@ -195,141 +79,25 @@ export const ChainvoiceABI = [ "type": "address", "internalType": "address" }, - { - "name": "dueDate", - "type": "string", - "internalType": "string" - }, - { - "name": "issueDate", - "type": "string", - "internalType": "string" - }, - { - "name": "user", - "type": "tuple", - "internalType": "struct Chainvoice.UserDetails", - "components": [ - { - "name": "fname", - "type": "string", - "internalType": "string" - }, - { - "name": "lname", - "type": "string", - "internalType": "string" - }, - { - "name": "email", - "type": "string", - "internalType": "string" - }, - { - "name": "country", - "type": "string", - "internalType": "string" - }, - { - "name": "city", - "type": "string", - "internalType": "string" - }, - { - "name": "postalcode", - "type": "string", - "internalType": "string" - } - ] - }, { "name": "to", "type": "address", "internalType": "address" }, - { - "name": "client", - "type": "tuple", - "internalType": "struct Chainvoice.UserDetails", - "components": [ - { - "name": "fname", - "type": "string", - "internalType": "string" - }, - { - "name": "lname", - "type": "string", - "internalType": "string" - }, - { - "name": "email", - "type": "string", - "internalType": "string" - }, - { - "name": "country", - "type": "string", - "internalType": "string" - }, - { - "name": "city", - "type": "string", - "internalType": "string" - }, - { - "name": "postalcode", - "type": "string", - "internalType": "string" - } - ] - }, - { - "name": "amountDue", - "type": "uint256", - "internalType": "uint256" - }, { "name": "isPaid", "type": "bool", "internalType": "bool" - } - ] - }, - { - "name": "", - "type": "tuple[][]", - "internalType": "struct Chainvoice.ItemData[][]", - "components": [ + }, { - "name": "description", + "name": "encryptedInvoiceData", "type": "string", "internalType": "string" }, { - "name": "qty", - "type": "int256", - "internalType": "int256" - }, - { - "name": "unitPrice", - "type": "int256", - "internalType": "int256" - }, - { - "name": "discount", - "type": "int256", - "internalType": "int256" - }, - { - "name": "tax", - "type": "int256", - "internalType": "int256" - }, - { - "name": "amount", - "type": "int256", - "internalType": "int256" + "name": "encryptedHash", + "type": "string", + "internalType": "string" } ] } @@ -338,10 +106,10 @@ export const ChainvoiceABI = [ }, { "type": "function", - "name": "getSentInvoices", + "name": "getReceivedInvoices", "inputs": [ { - "name": "_address", + "name": "user", "type": "address", "internalType": "address" } @@ -362,141 +130,25 @@ export const ChainvoiceABI = [ "type": "address", "internalType": "address" }, - { - "name": "dueDate", - "type": "string", - "internalType": "string" - }, - { - "name": "issueDate", - "type": "string", - "internalType": "string" - }, - { - "name": "user", - "type": "tuple", - "internalType": "struct Chainvoice.UserDetails", - "components": [ - { - "name": "fname", - "type": "string", - "internalType": "string" - }, - { - "name": "lname", - "type": "string", - "internalType": "string" - }, - { - "name": "email", - "type": "string", - "internalType": "string" - }, - { - "name": "country", - "type": "string", - "internalType": "string" - }, - { - "name": "city", - "type": "string", - "internalType": "string" - }, - { - "name": "postalcode", - "type": "string", - "internalType": "string" - } - ] - }, { "name": "to", "type": "address", "internalType": "address" }, - { - "name": "client", - "type": "tuple", - "internalType": "struct Chainvoice.UserDetails", - "components": [ - { - "name": "fname", - "type": "string", - "internalType": "string" - }, - { - "name": "lname", - "type": "string", - "internalType": "string" - }, - { - "name": "email", - "type": "string", - "internalType": "string" - }, - { - "name": "country", - "type": "string", - "internalType": "string" - }, - { - "name": "city", - "type": "string", - "internalType": "string" - }, - { - "name": "postalcode", - "type": "string", - "internalType": "string" - } - ] - }, - { - "name": "amountDue", - "type": "uint256", - "internalType": "uint256" - }, { "name": "isPaid", "type": "bool", "internalType": "bool" - } - ] - }, - { - "name": "", - "type": "tuple[][]", - "internalType": "struct Chainvoice.ItemData[][]", - "components": [ + }, { - "name": "description", + "name": "encryptedInvoiceData", "type": "string", "internalType": "string" }, { - "name": "qty", - "type": "int256", - "internalType": "int256" - }, - { - "name": "unitPrice", - "type": "int256", - "internalType": "int256" - }, - { - "name": "discount", - "type": "int256", - "internalType": "int256" - }, - { - "name": "tax", - "type": "int256", - "internalType": "int256" - }, - { - "name": "amount", - "type": "int256", - "internalType": "int256" + "name": "encryptedHash", + "type": "string", + "internalType": "string" } ] } @@ -505,136 +157,59 @@ export const ChainvoiceABI = [ }, { "type": "function", - "name": "invoices", + "name": "getSentInvoices", "inputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "id", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "from", - "type": "address", - "internalType": "address" - }, - { - "name": "dueDate", - "type": "string", - "internalType": "string" - }, - { - "name": "issueDate", - "type": "string", - "internalType": "string" - }, { "name": "user", - "type": "tuple", - "internalType": "struct Chainvoice.UserDetails", - "components": [ - { - "name": "fname", - "type": "string", - "internalType": "string" - }, - { - "name": "lname", - "type": "string", - "internalType": "string" - }, - { - "name": "email", - "type": "string", - "internalType": "string" - }, - { - "name": "country", - "type": "string", - "internalType": "string" - }, - { - "name": "city", - "type": "string", - "internalType": "string" - }, - { - "name": "postalcode", - "type": "string", - "internalType": "string" - } - ] - }, - { - "name": "to", "type": "address", "internalType": "address" - }, + } + ], + "outputs": [ { - "name": "client", - "type": "tuple", - "internalType": "struct Chainvoice.UserDetails", + "name": "", + "type": "tuple[]", + "internalType": "struct Chainvoice.InvoiceDetails[]", "components": [ { - "name": "fname", - "type": "string", - "internalType": "string" + "name": "id", + "type": "uint256", + "internalType": "uint256" }, { - "name": "lname", - "type": "string", - "internalType": "string" + "name": "from", + "type": "address", + "internalType": "address" }, { - "name": "email", - "type": "string", - "internalType": "string" + "name": "to", + "type": "address", + "internalType": "address" }, { - "name": "country", - "type": "string", - "internalType": "string" + "name": "isPaid", + "type": "bool", + "internalType": "bool" }, { - "name": "city", + "name": "encryptedInvoiceData", "type": "string", "internalType": "string" }, { - "name": "postalcode", + "name": "encryptedHash", "type": "string", "internalType": "string" } ] - }, - { - "name": "amountDue", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "isPaid", - "type": "bool", - "internalType": "bool" } ], "stateMutability": "view" }, { "type": "function", - "name": "itemDatas", + "name": "invoices", "inputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - }, { "name": "", "type": "uint256", @@ -643,34 +218,34 @@ export const ChainvoiceABI = [ ], "outputs": [ { - "name": "description", - "type": "string", - "internalType": "string" + "name": "id", + "type": "uint256", + "internalType": "uint256" }, { - "name": "qty", - "type": "int256", - "internalType": "int256" + "name": "from", + "type": "address", + "internalType": "address" }, { - "name": "unitPrice", - "type": "int256", - "internalType": "int256" + "name": "to", + "type": "address", + "internalType": "address" }, { - "name": "discount", - "type": "int256", - "internalType": "int256" + "name": "isPaid", + "type": "bool", + "internalType": "bool" }, { - "name": "tax", - "type": "int256", - "internalType": "int256" + "name": "encryptedInvoiceData", + "type": "string", + "internalType": "string" }, { - "name": "amount", - "type": "int256", - "internalType": "int256" + "name": "encryptedHash", + "type": "string", + "internalType": "string" } ], "stateMutability": "view" @@ -767,7 +342,7 @@ export const ChainvoiceABI = [ "name": "setTreasuryAddress", "inputs": [ { - "name": "newTreauserAdd", + "name": "newTreasury", "type": "address", "internalType": "address" } @@ -790,7 +365,7 @@ export const ChainvoiceABI = [ }, { "type": "function", - "name": "withdraw", + "name": "withdrawFees", "inputs": [], "outputs": [], "stateMutability": "nonpayable" @@ -802,7 +377,7 @@ export const ChainvoiceABI = [ { "name": "id", "type": "uint256", - "indexed": false, + "indexed": true, "internalType": "uint256" }, { @@ -816,12 +391,6 @@ export const ChainvoiceABI = [ "type": "address", "indexed": true, "internalType": "address" - }, - { - "name": "amountDue", - "type": "uint256", - "indexed": false, - "internalType": "uint256" } ], "anonymous": false @@ -833,7 +402,7 @@ export const ChainvoiceABI = [ { "name": "id", "type": "uint256", - "indexed": false, + "indexed": true, "internalType": "uint256" }, { @@ -849,7 +418,7 @@ export const ChainvoiceABI = [ "internalType": "address" }, { - "name": "amountPaid", + "name": "amount", "type": "uint256", "indexed": false, "internalType": "uint256" @@ -857,4 +426,5 @@ export const ChainvoiceABI = [ ], "anonymous": false } - ] \ No newline at end of file + ] + \ No newline at end of file diff --git a/frontend/src/page/ReceivedInvoice.jsx b/frontend/src/page/ReceivedInvoice.jsx index 9a113606..7c157e5e 100644 --- a/frontend/src/page/ReceivedInvoice.jsx +++ b/frontend/src/page/ReceivedInvoice.jsx @@ -1,37 +1,41 @@ -import Paper from '@mui/material/Paper'; -import Table from '@mui/material/Table'; -import TableBody from '@mui/material/TableBody'; -import TableCell from '@mui/material/TableCell'; -import TableContainer from '@mui/material/TableContainer'; -import TableHead from '@mui/material/TableHead'; -import TablePagination from '@mui/material/TablePagination'; -import TableRow from '@mui/material/TableRow'; -import { ChainvoiceABI } from '@/contractsABI/ChainvoiceABI'; -import { BrowserProvider, Contract, ethers } from 'ethers' -import React, { useEffect, useState } from 'react' -import { useAccount, useWalletClient } from 'wagmi' -import DescriptionIcon from '@mui/icons-material/Description'; -import SwipeableDrawer from '@mui/material/SwipeableDrawer'; -import Button from '@mui/material/Button'; +import Paper from "@mui/material/Paper"; +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableCell from "@mui/material/TableCell"; +import TableContainer from "@mui/material/TableContainer"; +import TableHead from "@mui/material/TableHead"; +import TablePagination from "@mui/material/TablePagination"; +import TableRow from "@mui/material/TableRow"; +import { ChainvoiceABI } from "@/contractsABI/ChainvoiceABI"; +import { BrowserProvider, Contract, ethers } from "ethers"; +import React, { useEffect, useState } from "react"; +import { useAccount, useWalletClient } from "wagmi"; +import DescriptionIcon from "@mui/icons-material/Description"; +import SwipeableDrawer from "@mui/material/SwipeableDrawer"; import { useRef } from "react"; -import jsPDF from "jspdf"; import html2canvas from "html2canvas"; +import { LitNodeClient } from "@lit-protocol/lit-node-client"; +import { decryptToString } from "@lit-protocol/encryption/src/lib/encryption.js"; +import { LIT_ABILITY, LIT_NETWORK } from "@lit-protocol/constants"; +import { + createSiweMessageWithRecaps, + generateAuthSig, + LitAccessControlConditionResource, +} from "@lit-protocol/auth-helpers"; const columns = [ - { id: 'fname', label: 'First Name', minWidth: 100 }, - { id: 'lname', label: 'Last Name', minWidth: 100 }, - { id: 'to', label: 'Address', minWidth: 200 }, - { id: 'email', label: 'Email', minWidth: 170 }, + { id: "fname", label: "First Name", minWidth: 100 }, + { id: "lname", label: "Last Name", minWidth: 100 }, + { id: "to", label: "Sender's address", minWidth: 200 }, + { id: "email", label: "Email", minWidth: 170 }, // { id: 'country', label: 'Country', minWidth: 100 }, - { id: 'amountDue', label: 'Total Amount', minWidth: 100, align: 'right' }, - { id: 'isPaid', label: 'Status', minWidth: 100 }, - { id: 'detail', label: 'Detail Invoice', minWidth: 100 }, - { id: 'pay', label: 'Pay / Paid' , minWidth: 100 }, + { id: "amountDue", label: "Total Amount", minWidth: 100, align: "right" }, + { id: "isPaid", label: "Status", minWidth: 100 }, + { id: "detail", label: "Detail Invoice", minWidth: 100 }, + { id: "pay", label: "Pay / Paid", minWidth: 100 }, ]; - function ReceivedInvoice() { - const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(10); @@ -51,30 +55,141 @@ function ReceivedInvoice() { const [invoiceItems, setInvoiceItems] = useState([]); const [fee, setFee] = useState(0); + useEffect(() => { if (!walletClient) return; + const fetchReceivedInvoices = async () => { try { setLoading(true); - if (!walletClient) return; + const provider = new BrowserProvider(walletClient); const signer = await provider.getSigner(); - const contract = new Contract(import.meta.env.VITE_CONTRACT_ADDRESS, ChainvoiceABI, signer); - console.log(address); + + // 1. Setup Lit Node + const litNodeClient = new LitNodeClient({ + litNetwork: LIT_NETWORK.DatilDev, + }); + await litNodeClient.connect(); + + // 2. Get data from contract + const contract = new Contract( + import.meta.env.VITE_CONTRACT_ADDRESS, + ChainvoiceABI, + signer + ); + const res = await contract.getReceivedInvoices(address); - const [invoiceDetails, itemData] = res; - setReceivedInvoice(invoiceDetails); - setInvoiceItems(itemData); - setLoading(false); + console.log("getReceivedInvoices raw response:", res); + + console.log(typeof res); + // if (!res || !Array.isArray(res) || res.length !== 2) { + // console.warn("Unexpected contract response format."); + // setReceivedInvoice([]); + // setInvoiceItems([]); + // setLoading(false); + // return; + // } + + const decryptedInvoices = []; + const allItems = []; + + for (const invoice of res) { + const id = invoice[0]; + const isPaid = invoice[3]; + const encryptedStringBase64 = invoice[4]; // encryptedData + const dataToEncryptHash = invoice[5]; + + if (!encryptedStringBase64 || !dataToEncryptHash) continue; + + const ciphertext = atob(encryptedStringBase64); + + const accessControlConditions = [ + { + contractAddress: "", + standardContractType: "", + chain: "ethereum", + method: "", + parameters: [":userAddress"], + returnValueTest: { + comparator: "=", + value: invoice[1].toLowerCase(), // from + }, + }, + { operator: "or" }, + { + contractAddress: "", + standardContractType: "", + chain: "ethereum", + method: "", + parameters: [":userAddress"], + returnValueTest: { + comparator: "=", + value: invoice[2].toLowerCase(), // to + }, + }, + ]; + + const sessionSigs = await litNodeClient.getSessionSigs({ + chain: "ethereum", + resourceAbilityRequests: [ + { + resource: new LitAccessControlConditionResource("*"), + ability: LIT_ABILITY.AccessControlConditionDecryption, + }, + ], + authNeededCallback: async ({ + uri, + expiration, + resourceAbilityRequests, + }) => { + const nonce = await litNodeClient.getLatestBlockhash(); + const toSign = await createSiweMessageWithRecaps({ + uri, + expiration, + resources: resourceAbilityRequests, + walletAddress: address, + nonce, + litNodeClient, + }); + return await generateAuthSig({ signer, toSign }); + }, + }); + + const decryptedString = await decryptToString( + { + accessControlConditions, + chain: "ethereum", + ciphertext, + dataToEncryptHash, + sessionSigs, + }, + litNodeClient + ); + + const parsed = JSON.parse(decryptedString); + parsed["id"] = id; + parsed["isPaid"] = isPaid; + decryptedInvoices.push(parsed); + allItems.push(null); // placeholder + } + + console.log("decrypted : ", decryptedInvoices); + setReceivedInvoice(decryptedInvoices); + setInvoiceItems(allItems); + const fee = await contract.fee(); - console.log(ethers.formatUnits(fee)); setFee(fee); - console.log(res); } catch (error) { - console.log(error); + console.error("Decryption error:", error); + alert("Failed to fetch or decrypt received invoices."); + } finally { + setLoading(false); } - } + }; + fetchReceivedInvoices(); + console.log("invoices : ", receivedInvoices); }, [walletClient]); const payInvoice = async (id, amountDue) => { @@ -82,25 +197,43 @@ function ReceivedInvoice() { if (!walletClient) return; const provider = new BrowserProvider(walletClient); const signer = await provider.getSigner(); - const contract = new Contract(import.meta.env.VITE_CONTRACT_ADDRESS, ChainvoiceABI, signer); - console.log(amountDue); + const contract = new Contract( + import.meta.env.VITE_CONTRACT_ADDRESS, + ChainvoiceABI, + signer + ); + console.log(ethers.parseUnits(String(amountDue), 18)); const fee = await contract.fee(); console.log(fee); - const res = await contract.payInvoice(ethers.toBigInt(id), { - value: amountDue + fee + const amountDueInWei = ethers.parseUnits(String(amountDue), 18); + const feeInWei = BigInt(fee); + const total = amountDueInWei + feeInWei; + + const res = await contract.payInvoice(BigInt(id), { + value: total, }); } catch (error) { console.log(error); } - } + }; - const [drawerState, setDrawerState] = useState({ open: false, selectedInvoice: null }); + const [drawerState, setDrawerState] = useState({ + open: false, + selectedInvoice: null, + }); const toggleDrawer = (invoice) => (event) => { - if (event && event.type === "keydown" && (event.key === "Tab" || event.key === "Shift")) { + if ( + event && + event.type === "keydown" && + (event.key === "Tab" || event.key === "Shift") + ) { return; } - setDrawerState({ open: !drawerState.open, selectedInvoice: invoice || null }); + setDrawerState({ + open: !drawerState.open, + selectedInvoice: invoice || null, + }); }; const contentRef = useRef(); @@ -134,20 +267,37 @@ function ReceivedInvoice() {

Received Invoice Request

Pay to your client request

- + {loading ? (

loading........

- ) : receivedInvoices.length > 0 ? ( + ) : receivedInvoices?.length > 0 ? ( <> - +
- + {columns.map((column) => ( {column.label} @@ -158,41 +308,69 @@ function ReceivedInvoice() { {receivedInvoices .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) .map((invoice, index) => ( - {columns.map((column) => { - console.log(invoice.to); - const value = invoice.user[column.id] || ''; - if (column.id === 'to') { + const value = invoice.user[column.id] || ""; + if (column.id === "to") { return ( - - {invoice.to.substring(0, 10)}...{invoice.to.substring(invoice.to.length - 10)} + + {invoice.user.address + ? `${invoice.user.address.substring( + 0, + 10 + )}...${invoice.user.address.substring( + invoice.user.address.length - 10 + )}` + : "N/A"} ); } - if (column.id === 'amountDue') { + if (column.id === "amountDue") { return ( - - {ethers.formatUnits(invoice.amountDue)} ETH + + {/* {ethers.formatUnits(invoice.amountDue)} ETH */} + {invoice.amountDue} ETH ); } - if (column.id === 'isPaid') { + if (column.id === "isPaid") { return ( - + - ) + ); } if (column.id === "detail") { return ( - + ); } - if(column.id==='pay' && invoice.isPaid) { + if (column.id === "pay" && invoice.isPaid) { return ( - +
@@ -313,27 +525,45 @@ function ReceivedInvoice() { - { - invoiceItems[drawerState.selectedInvoice.id].map((item, index) => ( - - - - - - - - - - - )) - } + {drawerState.selectedInvoice?.items?.map((item, index) => ( + + + + + + + + + + + ))}
Amount
{item.description}{item.qty.toString()}{ethers.formatUnits(item.unitPrice)}{item.discount.toString()}{item.tax.toString()} - {ethers.formatUnits(item.amount)} ETH -
{item.description}{item.qty.toString()} + {/* {ethers.formatUnits(item.unitPrice)} */} + {item.unitPrice} + {item.discount.toString()}{item.tax.toString()} + {item.amount} ETH + {/* {ethers.formatUnits(item.amount)} ETH */} +
-

Fee for invoice pay : {ethers.formatUnits(fee)} ETH

-

Amount: {ethers.formatUnits(drawerState.selectedInvoice.amountDue)} ETH

-

Total Amount: {ethers.formatUnits(drawerState.selectedInvoice.amountDue+fee)} ETH

+

+ {/* Fee for invoice pay : {ethers.formatUnits(fee)} ETH */} + Fee for invoice pay : {parseFloat( + ethers.formatUnits(fee) + )}{" "} + ETH +

+

+ {" "} + Amount: {drawerState.selectedInvoice.amountDue}{" "} + {/* {ethers.formatUnits(drawerState.selectedInvoice.amountDue)}{" "} */} + ETH +

+

+ Total Amount:{" "} + {parseFloat(drawerState.selectedInvoice.amountDue) + + parseFloat(ethers.formatUnits(fee))}{" "} + ETH +

Powered by

@@ -341,11 +571,10 @@ function ReceivedInvoice() {
- ) - } - + )} +
- ) + ); } -export default ReceivedInvoice \ No newline at end of file +export default ReceivedInvoice; diff --git a/frontend/src/page/SentInvoice.jsx b/frontend/src/page/SentInvoice.jsx index 2455da2d..3eac96f0 100644 --- a/frontend/src/page/SentInvoice.jsx +++ b/frontend/src/page/SentInvoice.jsx @@ -1,38 +1,42 @@ -import Paper from '@mui/material/Paper'; -import Table from '@mui/material/Table'; -import TableBody from '@mui/material/TableBody'; -import TableCell from '@mui/material/TableCell'; -import TableContainer from '@mui/material/TableContainer'; -import TableHead from '@mui/material/TableHead'; -import TablePagination from '@mui/material/TablePagination'; -import TableRow from '@mui/material/TableRow'; -import { BrowserProvider, Contract, ethers } from 'ethers'; -import React, { useEffect, useState, Fragment } from 'react'; -import { useAccount, useWalletClient } from 'wagmi'; -import { ChainvoiceABI } from '../contractsABI/ChainvoiceABI'; -import DescriptionIcon from '@mui/icons-material/Description'; - -import SwipeableDrawer from '@mui/material/SwipeableDrawer'; -import Button from '@mui/material/Button'; -import { useRef } from "react"; -import jsPDF from "jspdf"; +import Paper from "@mui/material/Paper"; +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableCell from "@mui/material/TableCell"; +import TableContainer from "@mui/material/TableContainer"; +import TableHead from "@mui/material/TableHead"; +import TablePagination from "@mui/material/TablePagination"; +import TableRow from "@mui/material/TableRow"; +import { BrowserProvider, Contract, ethers } from "ethers"; +import React, { useEffect, useState, Fragment, useRef } from "react"; +import { useAccount, useWalletClient } from "wagmi"; +import { ChainvoiceABI } from "../contractsABI/ChainvoiceABI"; +import DescriptionIcon from "@mui/icons-material/Description"; + +import SwipeableDrawer from "@mui/material/SwipeableDrawer"; import html2canvas from "html2canvas"; +import { LitNodeClient } from "@lit-protocol/lit-node-client"; +import { decryptToString } from "@lit-protocol/encryption/src/lib/encryption.js"; +import { LIT_ABILITY, LIT_NETWORK } from "@lit-protocol/constants"; +import { + createSiweMessageWithRecaps, + generateAuthSig, + LitAccessControlConditionResource, +} from "@lit-protocol/auth-helpers"; + const columns = [ - { id: 'fname', label: 'First Name', minWidth: 100 }, - { id: 'lname', label: 'Last Name', minWidth: 100 }, - { id: 'to', label: 'Address', minWidth: 200 }, - { id: 'email', label: 'Email', minWidth: 170 }, - { id: 'country', label: 'Country', minWidth: 100 }, - { id: 'city', label: 'City', minWidth: 100 }, - { id: 'amountDue', label: 'Total Amount', minWidth: 100, align: 'right' }, - { id: 'isPaid', label: 'Status', minWidth: 100 }, - { id: 'detail', label: 'Detail Invoice', minWidth: 100 } + { id: "fname", label: "First Name", minWidth: 100 }, + { id: "lname", label: "Last Name", minWidth: 100 }, + { id: "to", label: "Receiver's Address", minWidth: 200 }, + { id: "email", label: "Email", minWidth: 170 }, + { id: "country", label: "Country", minWidth: 100 }, + { id: "city", label: "City", minWidth: 100 }, + { id: "amountDue", label: "Total Amount", minWidth: 100, align: "right" }, + { id: "isPaid", label: "Status", minWidth: 100 }, + { id: "detail", label: "Detail Invoice", minWidth: 100 }, ]; - function SentInvoice() { - const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(10); const handleChangePage = (event, newPage) => { @@ -48,38 +52,160 @@ function SentInvoice() { const [invoiceItems, setInvoiceItems] = useState([]); const [loading, setLoading] = useState(false); const [fee, setFee] = useState(0); - const {address}=useAccount(); + const { address } = useAccount(); + useEffect(() => { if (!walletClient) return; + const fetchSentInvoices = async () => { try { setLoading(true); - if (!walletClient) return; + + // 1. Setup signer const provider = new BrowserProvider(walletClient); const signer = await provider.getSigner(); - const contract = new Contract(import.meta.env.VITE_CONTRACT_ADDRESS, ChainvoiceABI, signer); + + // 2. Connect to Lit Node + const litNodeClient = new LitNodeClient({ + litNetwork: LIT_NETWORK.DatilDev, + }); + await litNodeClient.connect(); + + // 3. Contract call to get encrypted invoice + const contract = new Contract( + import.meta.env.VITE_CONTRACT_ADDRESS, + ChainvoiceABI, + signer + ); + const res = await contract.getSentInvoices(address); - const [invoiceDetails, itemData] = res; - setSentInvoices(invoiceDetails); - setInvoiceItems(itemData); + console.log(res); + + if (!res || !Array.isArray(res) || res.length === 0) { + console.warn("No invoices found."); + setSentInvoices([]); + setInvoiceItems([]); + setLoading(false); + return; + } + + const decryptedInvoices = []; + const allItems = []; + + for (const invoice of res) { + const id = invoice[0]; + const isPaid = invoice[3]; + const encryptedStringBase64 = invoice[4]; // encryptedData + const dataToEncryptHash = invoice[5]; + + if (!encryptedStringBase64 || !dataToEncryptHash) continue; + + const ciphertext = atob(encryptedStringBase64); + const accessControlConditions = [ + { + contractAddress: "", + standardContractType: "", + chain: "ethereum", + method: "", + parameters: [":userAddress"], + returnValueTest: { + comparator: "=", + value: invoice[1].toLowerCase(), // from + }, + }, + { operator: "or" }, + { + contractAddress: "", + standardContractType: "", + chain: "ethereum", + method: "", + parameters: [":userAddress"], + returnValueTest: { + comparator: "=", + value: invoice[2].toLowerCase(), // to + }, + }, + ]; + + const sessionSigs = await litNodeClient.getSessionSigs({ + chain: "ethereum", + resourceAbilityRequests: [ + { + resource: new LitAccessControlConditionResource("*"), + ability: LIT_ABILITY.AccessControlConditionDecryption, + }, + ], + authNeededCallback: async ({ + uri, + expiration, + resourceAbilityRequests, + }) => { + const nonce = await litNodeClient.getLatestBlockhash(); + const toSign = await createSiweMessageWithRecaps({ + uri, + expiration, + resources: resourceAbilityRequests, + walletAddress: address, + nonce, + litNodeClient, + }); + return await generateAuthSig({ signer, toSign }); + }, + }); + + const decryptedString = await decryptToString( + { + accessControlConditions, + chain: "ethereum", + ciphertext, + dataToEncryptHash, + sessionSigs, + }, + litNodeClient + ); + + const parsed = JSON.parse(decryptedString); + parsed["id"] = id; + parsed["isPaid"] = isPaid; + decryptedInvoices.push(parsed); + allItems.push(null); // placeholder + } + + setSentInvoices(decryptedInvoices); + setInvoiceItems(allItems); // if items are separate, replace nulls const fee = await contract.fee(); - console.log(ethers.formatUnits(fee)); setFee(fee); - setLoading(false); } catch (error) { - alert(error); + console.error("Decryption error:", error); + alert("Failed to decrypt invoice."); + } finally { + setLoading(false); } - } + }; + fetchSentInvoices(); + + console.log("invoices : ", sentInvoices); }, [walletClient]); - const [drawerState, setDrawerState] = useState({ open: false, selectedInvoice: null }); + const [drawerState, setDrawerState] = useState({ + open: false, + selectedInvoice: null, + }); const toggleDrawer = (invoice) => (event) => { - if (event && event.type === "keydown" && (event.key === "Tab" || event.key === "Shift")) { + console.log(invoice); + if ( + event && + event.type === "keydown" && + (event.key === "Tab" || event.key === "Shift") + ) { return; } - setDrawerState({ open: !drawerState.open, selectedInvoice: invoice || null }); + setDrawerState({ + open: !drawerState.open, + selectedInvoice: invoice || null, + }); }; const contentRef = useRef(); @@ -113,20 +239,37 @@ function SentInvoice() { return (

Your Sent Invoice Request

- + {loading ? (

Loading...

- ) : sentInvoices.length > 0 ? ( + ) : sentInvoices?.length > 0 ? ( <> - +
{columns.map((column) => ( {column.label} @@ -134,55 +277,108 @@ function SentInvoice() { - {sentInvoices.length > 0 && sentInvoices.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage).map((invoice, index) => ( - - {columns.map((column) => { - const value = invoice?.client[column.id]; - if (column.id === "to") { - return ( - - {invoice.to - ? `${invoice.to.substring(0, 10)}...${invoice.to.substring(invoice.to.length - 10)}` - : "N/A"} - - ); - } - if (column.id === "amountDue") { - return ( - - {ethers.formatUnits(invoice.amountDue)} ETH - - ); - } - if (column.id === "isPaid") { - return ( - - - - ); - } - if (column.id === "detail") { - return ( - - + + ); + } + if (column.id === "detail") { + return ( + + + + ); + } + return ( + - - - - ); - } - return ( - - {value} - - ); - })} - - ))} + {value} + + ); + })} + + ))}
@@ -207,9 +403,10 @@ function SentInvoice() { "& .MuiInputBase-root": { color: "white", }, - "& .MuiTablePagination-selectLabel, & .MuiTablePagination-displayedRows": { - color: "white", - }, + "& .MuiTablePagination-selectLabel, & .MuiTablePagination-displayedRows": + { + color: "white", + }, }} /> @@ -218,35 +415,54 @@ function SentInvoice() { )}
- + {drawerState.selectedInvoice && (
none
-

Issued by {drawerState.selectedInvoice.issueDate}

-

Payment Due by {drawerState.selectedInvoice.dueDate}

+

+ Issued by {drawerState.selectedInvoice.issueDate} +

+

+ Payment Due by {drawerState.selectedInvoice.dueDate} +

-

Invoice #{drawerState.selectedInvoice.id}

+

+ Invoice # {drawerState.selectedInvoice.id.toString()} +

From

-

{drawerState.selectedInvoice.from}

+

+ {drawerState.selectedInvoice.user.address} +

{`${drawerState.selectedInvoice.user.fname} ${drawerState.selectedInvoice.user.lname}`}

-

{drawerState.selectedInvoice.user.email}

+

+ {drawerState.selectedInvoice.user.email} +

{`${drawerState.selectedInvoice.user.city}, ${drawerState.selectedInvoice.user.country} (${drawerState.selectedInvoice.user.postalcode})`}

Billed to

-

{drawerState.selectedInvoice.from}

+

+ {drawerState.selectedInvoice.client.address} +

{`${drawerState.selectedInvoice.client.fname} ${drawerState.selectedInvoice.client.lname}`}

-

{drawerState.selectedInvoice.client.email}

+

+ {drawerState.selectedInvoice.client.email} +

{`${drawerState.selectedInvoice.client.city}, ${drawerState.selectedInvoice.client.country} (${drawerState.selectedInvoice.client.postalcode})`}

@@ -260,27 +476,42 @@ function SentInvoice() { - { - invoiceItems[drawerState.selectedInvoice.id].map((item, index) => ( - - - - - - - - - - - )) - } + {drawerState.selectedInvoice?.items?.map((item, index) => ( + + + + + + + + + + + ))}
Amount
{item.description}{item.qty.toString()}{ethers.formatUnits(item.unitPrice)}{item.discount.toString()}{item.tax.toString()} - {ethers.formatUnits(item.amount)} -
{item.description}{item.qty.toString()} + {/* {ethers.formatUnits(item.unitPrice)} */} + {item.unitPrice} + {item.discount.toString()}{item.tax.toString()} + {/* {ethers.formatUnits(item.amount)} */} + {item.amount} +
-

Fee for invoice pay : {ethers.formatUnits(fee)} ETH

-

Amount: {ethers.formatUnits(drawerState.selectedInvoice.amountDue)} ETH

-

Total Amount: {ethers.formatUnits(drawerState.selectedInvoice.amountDue + fee)} ETH

+

+ Fee for invoice pay : {fee} ETH + {/* Fee for invoice pay : {ethers.formatUnits(fee)} ETH */} +

+

+ {" "} + Amount:{" "} + {/* {ethers.formatUnits(drawerState.selectedInvoice.amountDue)}{" "} */} + {drawerState.selectedInvoice.amountDue} ETH +

+

+ Total Amount:{" "} + {parseFloat(drawerState.selectedInvoice.amountDue) + + parseFloat(ethers.formatUnits(fee))}{" "} + ETH +

Powered by

@@ -288,11 +519,10 @@ function SentInvoice() {
- ) - } -
-
- ) + )} + +
+ ); } -export default SentInvoice \ No newline at end of file +export default SentInvoice; diff --git a/frontend/vite.config.js b/frontend/vite.config.js index f6d8b315..c8482d2d 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -1,7 +1,7 @@ import path from "path" import react from "@vitejs/plugin-react" import { defineConfig } from "vite" -import { nodePolyfills } from "vite-plugin-node-polyfills" +import { nodePolyfills } from 'vite-plugin-node-polyfills' export default defineConfig({ plugins: [react(),nodePolyfills()], From 1b7aecd74bec04fb9d245da72a4b987d14a0ffec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=85=9A=F0=9F=85=90=F0=9F=85=A1=F0=9F=85=90?= =?UTF-8?q?=F0=9F=85=9D?= Date: Thu, 3 Jul 2025 19:51:35 +0530 Subject: [PATCH 12/21] refactor: optimize Lit client init and invoice fetch --- frontend/src/components/CreateInvoice.jsx | 29 +++++++++++--- frontend/src/page/ReceivedInvoice.jsx | 47 +++++++++++++---------- frontend/src/page/SentInvoice.jsx | 38 +++++++++++++----- 3 files changed, 78 insertions(+), 36 deletions(-) diff --git a/frontend/src/components/CreateInvoice.jsx b/frontend/src/components/CreateInvoice.jsx index c37f5141..8fbc1b6b 100644 --- a/frontend/src/components/CreateInvoice.jsx +++ b/frontend/src/components/CreateInvoice.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { Input } from "./ui/input"; import { Button } from "./ui/button"; import { BrowserProvider, Contract, ethers } from "ethers"; @@ -33,6 +33,7 @@ function CreateInvoice() { const [issueDate, setIssueDate] = useState(new Date()); const [loading, setLoading] = useState(false); const navigate = useNavigate(); + const litClientRef = useRef(null); const [itemData, setItemData] = useState([ { @@ -60,6 +61,21 @@ function CreateInvoice() { setTotalAmountDue(total); }, [itemData]); + useEffect(() => { + const initLit = async () => { + if (!litClientRef.current) { + const client = new LitNodeClient({ + litNetwork: LIT_NETWORK.DatilDev, + debug: false, + }); + await client.connect(); + litClientRef.current = client; + console.log(litClientRef.current); + } + }; + initLit(); + }, []); + const handleItemData = (e, index) => { const { name, value } = e.target; @@ -143,11 +159,12 @@ function CreateInvoice() { const invoiceString = JSON.stringify(invoicePayload); // 2. Setup Lit - const litNodeClient = new LitNodeClient({ - litNetwork: LIT_NETWORK.DatilDev, - debug: false, - }); - await litNodeClient.connect(); + + const litNodeClient = litClientRef.current; + if (!litNodeClient) { + alert("Lit client not initialized"); + return; + } const accessControlConditions = [ { diff --git a/frontend/src/page/ReceivedInvoice.jsx b/frontend/src/page/ReceivedInvoice.jsx index 7c157e5e..4263c1e8 100644 --- a/frontend/src/page/ReceivedInvoice.jsx +++ b/frontend/src/page/ReceivedInvoice.jsx @@ -52,12 +52,29 @@ function ReceivedInvoice() { const { address } = useAccount(); const [loading, setLoading] = useState(false); const [receivedInvoices, setReceivedInvoice] = useState([]); - const [invoiceItems, setInvoiceItems] = useState([]); - const [fee, setFee] = useState(0); + const [litReady, setLitReady] = useState(false); + const litClientRef = useRef(null); + + useEffect(() => { + const initLit = async () => { + if (!litClientRef.current) { + const client = new LitNodeClient({ + litNetwork: LIT_NETWORK.DatilDev, + debug: false, + }); + await client.connect(); + litClientRef.current = client; + setLitReady(true); + console.log(litClientRef.current); + } + }; + setLoading(true); + initLit(); + }, []); useEffect(() => { - if (!walletClient) return; + if (!walletClient || !litReady) return; const fetchReceivedInvoices = async () => { try { @@ -67,10 +84,12 @@ function ReceivedInvoice() { const signer = await provider.getSigner(); // 1. Setup Lit Node - const litNodeClient = new LitNodeClient({ - litNetwork: LIT_NETWORK.DatilDev, - }); - await litNodeClient.connect(); + + const litNodeClient = litClientRef.current; + if (!litNodeClient) { + alert("Lit client not initialized"); + return; + } // 2. Get data from contract const contract = new Contract( @@ -82,17 +101,7 @@ function ReceivedInvoice() { const res = await contract.getReceivedInvoices(address); console.log("getReceivedInvoices raw response:", res); - console.log(typeof res); - // if (!res || !Array.isArray(res) || res.length !== 2) { - // console.warn("Unexpected contract response format."); - // setReceivedInvoice([]); - // setInvoiceItems([]); - // setLoading(false); - // return; - // } - const decryptedInvoices = []; - const allItems = []; for (const invoice of res) { const id = invoice[0]; @@ -171,12 +180,10 @@ function ReceivedInvoice() { parsed["id"] = id; parsed["isPaid"] = isPaid; decryptedInvoices.push(parsed); - allItems.push(null); // placeholder } console.log("decrypted : ", decryptedInvoices); setReceivedInvoice(decryptedInvoices); - setInvoiceItems(allItems); const fee = await contract.fee(); setFee(fee); @@ -190,7 +197,7 @@ function ReceivedInvoice() { fetchReceivedInvoices(); console.log("invoices : ", receivedInvoices); - }, [walletClient]); + }, [walletClient,litReady]); const payInvoice = async (id, amountDue) => { try { diff --git a/frontend/src/page/SentInvoice.jsx b/frontend/src/page/SentInvoice.jsx index 3eac96f0..3d6a2d55 100644 --- a/frontend/src/page/SentInvoice.jsx +++ b/frontend/src/page/SentInvoice.jsx @@ -7,7 +7,7 @@ import TableHead from "@mui/material/TableHead"; import TablePagination from "@mui/material/TablePagination"; import TableRow from "@mui/material/TableRow"; import { BrowserProvider, Contract, ethers } from "ethers"; -import React, { useEffect, useState, Fragment, useRef } from "react"; +import React, { useEffect, useState, useRef } from "react"; import { useAccount, useWalletClient } from "wagmi"; import { ChainvoiceABI } from "../contractsABI/ChainvoiceABI"; import DescriptionIcon from "@mui/icons-material/Description"; @@ -53,9 +53,28 @@ function SentInvoice() { const [loading, setLoading] = useState(false); const [fee, setFee] = useState(0); const { address } = useAccount(); + const [litReady, setLitReady] = useState(false); + const litClientRef = useRef(null); useEffect(() => { - if (!walletClient) return; + const initLit = async () => { + if (!litClientRef.current) { + const client = new LitNodeClient({ + litNetwork: LIT_NETWORK.DatilDev, + debug: false, + }); + await client.connect(); + litClientRef.current = client; + setLitReady(true); + console.log(litClientRef.current); + } + }; + setLoading(true); + initLit(); + }, []); + + useEffect(() => { + if (!walletClient || !litReady) return; const fetchSentInvoices = async () => { try { @@ -66,10 +85,12 @@ function SentInvoice() { const signer = await provider.getSigner(); // 2. Connect to Lit Node - const litNodeClient = new LitNodeClient({ - litNetwork: LIT_NETWORK.DatilDev, - }); - await litNodeClient.connect(); + + const litNodeClient = litClientRef.current; + if (!litNodeClient) { + alert("Lit client not initialized"); + return; + } // 3. Contract call to get encrypted invoice const contract = new Contract( @@ -90,7 +111,6 @@ function SentInvoice() { } const decryptedInvoices = []; - const allItems = []; for (const invoice of res) { const id = invoice[0]; @@ -168,11 +188,9 @@ function SentInvoice() { parsed["id"] = id; parsed["isPaid"] = isPaid; decryptedInvoices.push(parsed); - allItems.push(null); // placeholder } setSentInvoices(decryptedInvoices); - setInvoiceItems(allItems); // if items are separate, replace nulls const fee = await contract.fee(); setFee(fee); } catch (error) { @@ -186,7 +204,7 @@ function SentInvoice() { fetchSentInvoices(); console.log("invoices : ", sentInvoices); - }, [walletClient]); + }, [walletClient, litReady]); const [drawerState, setDrawerState] = useState({ open: false, From ce51cbe0b3b25f64082352e43f2c5bbe77f1a7d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=85=9A=F0=9F=85=90=F0=9F=85=A1=F0=9F=85=90?= =?UTF-8?q?=F0=9F=85=9D?= Date: Thu, 3 Jul 2025 20:16:15 +0530 Subject: [PATCH 13/21] update readme --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4984b471..05c34c01 100644 --- a/README.md +++ b/README.md @@ -71,17 +71,17 @@ Prerequisites 4. Deploy the Contract using forge create ``` -forge create src/Contract.sol:Contract \ - --rpc-url $SEPOLIA_RPC_URL \ +forge create contracts/src/Chainvoice.sol:Chainvoice \ + --rpc-url $ETC_RPC_URL \ --private-key $PRIVATE_KEY \ - --broadcast \ + --broadcast ``` 5. Finally add Contract Address to Frontend `.env` - Create a new .env file by copying .env.example: `cp frontend/.env.example frontend/.env` - Open the new .env file and update the variables, especially: - `VITE_CONTRACT_ADDRESS=your_deployed_contract_address_here` + `VITE_TRACT_ADDRESS=your_deployed_contract_address_here` Replace your_deployed_contract_address_here with the actual contract address you got after deployment. From e412537c4afd7cd02983f8c2d3c4a6dfe4112dfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=85=9A=F0=9F=85=90=F0=9F=85=A1=F0=9F=85=90?= =?UTF-8?q?=F0=9F=85=9D?= Date: Thu, 3 Jul 2025 23:27:04 +0530 Subject: [PATCH 14/21] updated contract --- contracts/src/Chainvoice.sol | 12 +- frontend/src/components/CreateInvoice.jsx | 4 +- frontend/src/contractsABI/ChainvoiceABI.js | 882 +++++++++++---------- frontend/src/page/ReceivedInvoice.jsx | 42 +- frontend/src/page/SentInvoice.jsx | 18 +- 5 files changed, 496 insertions(+), 462 deletions(-) diff --git a/contracts/src/Chainvoice.sol b/contracts/src/Chainvoice.sol index ec9c8cee..a729f8e8 100644 --- a/contracts/src/Chainvoice.sol +++ b/contracts/src/Chainvoice.sol @@ -6,6 +6,7 @@ contract Chainvoice { uint256 id; address from; address to; + uint256 amountDue; bool isPaid; string encryptedInvoiceData; // Base64-encoded ciphertext string encryptedHash; @@ -46,6 +47,7 @@ contract Chainvoice { function createInvoice( address to, + uint256 amountDue, string memory encryptedInvoiceData, string memory encryptedHash ) external { @@ -59,9 +61,10 @@ contract Chainvoice { id: invoiceId, from: msg.sender, to: to, + amountDue: amountDue, isPaid: false, encryptedInvoiceData: encryptedInvoiceData, - encryptedHash:encryptedHash + encryptedHash: encryptedHash }) ); @@ -77,16 +80,15 @@ contract Chainvoice { InvoiceDetails storage invoice = invoices[invoiceId]; require(msg.sender == invoice.to, "Not authorized"); require(!invoice.isPaid, "Already paid"); - require(msg.value > fee, "Insufficient payment for invoice + fee"); + require(msg.value == invoice.amountDue + fee, "Incorrect payment amount"); - uint256 amountToSender = msg.value - fee; accumulatedFees += fee; + invoice.isPaid = true; + uint256 amountToSender = msg.value - fee; (bool sent, ) = payable(invoice.from).call{value: amountToSender}(""); require(sent, "Transfer failed"); - invoice.isPaid = true; - emit InvoicePaid(invoiceId, invoice.from, invoice.to, msg.value); } diff --git a/frontend/src/components/CreateInvoice.jsx b/frontend/src/components/CreateInvoice.jsx index 8fbc1b6b..af4872f4 100644 --- a/frontend/src/components/CreateInvoice.jsx +++ b/frontend/src/components/CreateInvoice.jsx @@ -114,7 +114,6 @@ function CreateInvoice() { amount: "", }, ]); - console.log(itemData.length); }; const createInvoiceRequest = async (data) => { @@ -239,9 +238,10 @@ function CreateInvoice() { ChainvoiceABI, signer ); - + console.log("amt : ",ethers.parseEther(totalAmountDue.toString())); const tx = await contract.createInvoice( data.clientAddress, + ethers.parseEther(totalAmountDue.toString()), encryptedStringBase64, dataToEncryptHash ); diff --git a/frontend/src/contractsABI/ChainvoiceABI.js b/frontend/src/contractsABI/ChainvoiceABI.js index 93e18b37..9baa2a01 100644 --- a/frontend/src/contractsABI/ChainvoiceABI.js +++ b/frontend/src/contractsABI/ChainvoiceABI.js @@ -1,430 +1,454 @@ export const ChainvoiceABI = [ - { - "type": "constructor", - "inputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "accumulatedFees", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "createInvoice", - "inputs": [ - { - "name": "to", - "type": "address", - "internalType": "address" - }, - { - "name": "encryptedInvoiceData", - "type": "string", - "internalType": "string" - }, - { - "name": "encryptedHash", - "type": "string", - "internalType": "string" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "fee", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "getInvoice", - "inputs": [ - { - "name": "invoiceId", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "", - "type": "tuple", - "internalType": "struct Chainvoice.InvoiceDetails", - "components": [ - { - "name": "id", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "from", - "type": "address", - "internalType": "address" - }, - { - "name": "to", - "type": "address", - "internalType": "address" - }, - { - "name": "isPaid", - "type": "bool", - "internalType": "bool" - }, - { - "name": "encryptedInvoiceData", - "type": "string", - "internalType": "string" - }, - { - "name": "encryptedHash", - "type": "string", - "internalType": "string" - } - ] - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "getReceivedInvoices", - "inputs": [ - { - "name": "user", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "", - "type": "tuple[]", - "internalType": "struct Chainvoice.InvoiceDetails[]", - "components": [ - { - "name": "id", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "from", - "type": "address", - "internalType": "address" - }, - { - "name": "to", - "type": "address", - "internalType": "address" - }, - { - "name": "isPaid", - "type": "bool", - "internalType": "bool" - }, - { - "name": "encryptedInvoiceData", - "type": "string", - "internalType": "string" - }, - { - "name": "encryptedHash", - "type": "string", - "internalType": "string" - } - ] - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "getSentInvoices", - "inputs": [ - { - "name": "user", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "", - "type": "tuple[]", - "internalType": "struct Chainvoice.InvoiceDetails[]", - "components": [ - { - "name": "id", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "from", - "type": "address", - "internalType": "address" - }, - { - "name": "to", - "type": "address", - "internalType": "address" - }, - { - "name": "isPaid", - "type": "bool", - "internalType": "bool" - }, - { - "name": "encryptedInvoiceData", - "type": "string", - "internalType": "string" - }, - { - "name": "encryptedHash", - "type": "string", - "internalType": "string" - } - ] - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "invoices", - "inputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "id", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "from", - "type": "address", - "internalType": "address" - }, - { - "name": "to", - "type": "address", - "internalType": "address" - }, - { - "name": "isPaid", - "type": "bool", - "internalType": "bool" - }, - { - "name": "encryptedInvoiceData", - "type": "string", - "internalType": "string" - }, - { - "name": "encryptedHash", - "type": "string", - "internalType": "string" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "owner", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "address" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "payInvoice", - "inputs": [ - { - "name": "invoiceId", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [], - "stateMutability": "payable" - }, - { - "type": "function", - "name": "receivedInvoices", - "inputs": [ - { - "name": "", - "type": "address", - "internalType": "address" - }, - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "sentInvoices", - "inputs": [ - { - "name": "", - "type": "address", - "internalType": "address" - }, - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "setFeeAmount", - "inputs": [ - { - "name": "_fee", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "setTreasuryAddress", - "inputs": [ - { - "name": "newTreasury", - "type": "address", - "internalType": "address" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "treasuryAddress", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "address" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "withdrawFees", - "inputs": [], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "event", - "name": "InvoiceCreated", - "inputs": [ - { - "name": "id", - "type": "uint256", - "indexed": true, - "internalType": "uint256" - }, - { - "name": "from", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "to", - "type": "address", - "indexed": true, - "internalType": "address" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "InvoicePaid", - "inputs": [ - { - "name": "id", - "type": "uint256", - "indexed": true, - "internalType": "uint256" - }, - { - "name": "from", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "to", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "amount", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - } - ], - "anonymous": false - } - ] - \ No newline at end of file + { + type: "constructor", + inputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "accumulatedFees", + inputs: [], + outputs: [ + { + name: "", + type: "uint256", + internalType: "uint256", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "createInvoice", + inputs: [ + { + name: "to", + type: "address", + internalType: "address", + }, + { + name: "amountDue", + type: "uint256", + internalType: "uint256", + }, + { + name: "encryptedInvoiceData", + type: "string", + internalType: "string", + }, + { + name: "encryptedHash", + type: "string", + internalType: "string", + }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "fee", + inputs: [], + outputs: [ + { + name: "", + type: "uint256", + internalType: "uint256", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "getInvoice", + inputs: [ + { + name: "invoiceId", + type: "uint256", + internalType: "uint256", + }, + ], + outputs: [ + { + name: "", + type: "tuple", + internalType: "struct Chainvoice.InvoiceDetails", + components: [ + { + name: "id", + type: "uint256", + internalType: "uint256", + }, + { + name: "from", + type: "address", + internalType: "address", + }, + { + name: "to", + type: "address", + internalType: "address", + }, + { + name: "amountDue", + type: "uint256", + internalType: "uint256", + }, + { + name: "isPaid", + type: "bool", + internalType: "bool", + }, + { + name: "encryptedInvoiceData", + type: "string", + internalType: "string", + }, + { + name: "encryptedHash", + type: "string", + internalType: "string", + }, + ], + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "getReceivedInvoices", + inputs: [ + { + name: "user", + type: "address", + internalType: "address", + }, + ], + outputs: [ + { + name: "", + type: "tuple[]", + internalType: "struct Chainvoice.InvoiceDetails[]", + components: [ + { + name: "id", + type: "uint256", + internalType: "uint256", + }, + { + name: "from", + type: "address", + internalType: "address", + }, + { + name: "to", + type: "address", + internalType: "address", + }, + { + name: "amountDue", + type: "uint256", + internalType: "uint256", + }, + { + name: "isPaid", + type: "bool", + internalType: "bool", + }, + { + name: "encryptedInvoiceData", + type: "string", + internalType: "string", + }, + { + name: "encryptedHash", + type: "string", + internalType: "string", + }, + ], + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "getSentInvoices", + inputs: [ + { + name: "user", + type: "address", + internalType: "address", + }, + ], + outputs: [ + { + name: "", + type: "tuple[]", + internalType: "struct Chainvoice.InvoiceDetails[]", + components: [ + { + name: "id", + type: "uint256", + internalType: "uint256", + }, + { + name: "from", + type: "address", + internalType: "address", + }, + { + name: "to", + type: "address", + internalType: "address", + }, + { + name: "amountDue", + type: "uint256", + internalType: "uint256", + }, + { + name: "isPaid", + type: "bool", + internalType: "bool", + }, + { + name: "encryptedInvoiceData", + type: "string", + internalType: "string", + }, + { + name: "encryptedHash", + type: "string", + internalType: "string", + }, + ], + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "invoices", + inputs: [ + { + name: "", + type: "uint256", + internalType: "uint256", + }, + ], + outputs: [ + { + name: "id", + type: "uint256", + internalType: "uint256", + }, + { + name: "from", + type: "address", + internalType: "address", + }, + { + name: "to", + type: "address", + internalType: "address", + }, + { + name: "amountDue", + type: "uint256", + internalType: "uint256", + }, + { + name: "isPaid", + type: "bool", + internalType: "bool", + }, + { + name: "encryptedInvoiceData", + type: "string", + internalType: "string", + }, + { + name: "encryptedHash", + type: "string", + internalType: "string", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "owner", + inputs: [], + outputs: [ + { + name: "", + type: "address", + internalType: "address", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "payInvoice", + inputs: [ + { + name: "invoiceId", + type: "uint256", + internalType: "uint256", + }, + ], + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + name: "receivedInvoices", + inputs: [ + { + name: "", + type: "address", + internalType: "address", + }, + { + name: "", + type: "uint256", + internalType: "uint256", + }, + ], + outputs: [ + { + name: "", + type: "uint256", + internalType: "uint256", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "sentInvoices", + inputs: [ + { + name: "", + type: "address", + internalType: "address", + }, + { + name: "", + type: "uint256", + internalType: "uint256", + }, + ], + outputs: [ + { + name: "", + type: "uint256", + internalType: "uint256", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "setFeeAmount", + inputs: [ + { + name: "_fee", + type: "uint256", + internalType: "uint256", + }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "setTreasuryAddress", + inputs: [ + { + name: "newTreasury", + type: "address", + internalType: "address", + }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "treasuryAddress", + inputs: [], + outputs: [ + { + name: "", + type: "address", + internalType: "address", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "withdrawFees", + inputs: [], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "event", + name: "InvoiceCreated", + inputs: [ + { + name: "id", + type: "uint256", + indexed: true, + internalType: "uint256", + }, + { + name: "from", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "to", + type: "address", + indexed: true, + internalType: "address", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "InvoicePaid", + inputs: [ + { + name: "id", + type: "uint256", + indexed: true, + internalType: "uint256", + }, + { + name: "from", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "to", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "amount", + type: "uint256", + indexed: false, + internalType: "uint256", + }, + ], + anonymous: false, + }, +]; \ No newline at end of file diff --git a/frontend/src/page/ReceivedInvoice.jsx b/frontend/src/page/ReceivedInvoice.jsx index 4263c1e8..4be168b6 100644 --- a/frontend/src/page/ReceivedInvoice.jsx +++ b/frontend/src/page/ReceivedInvoice.jsx @@ -57,21 +57,25 @@ function ReceivedInvoice() { const litClientRef = useRef(null); useEffect(() => { - const initLit = async () => { - if (!litClientRef.current) { - const client = new LitNodeClient({ - litNetwork: LIT_NETWORK.DatilDev, - debug: false, - }); - await client.connect(); - litClientRef.current = client; - setLitReady(true); - console.log(litClientRef.current); - } - }; - setLoading(true); - initLit(); - }, []); + const initLit = async () => { + try { + setLoading(true); + if (!litClientRef.current) { + const client = new LitNodeClient({ + litNetwork: LIT_NETWORK.DatilDev, + debug: false, + }); + await client.connect(); + litClientRef.current = client; + setLitReady(true); + console.log(litClientRef.current); + } + } catch (error) { + console.log("error while lit client initialization"); + } + }; + initLit(); + }, []); useEffect(() => { if (!walletClient || !litReady) return; @@ -105,9 +109,9 @@ function ReceivedInvoice() { for (const invoice of res) { const id = invoice[0]; - const isPaid = invoice[3]; - const encryptedStringBase64 = invoice[4]; // encryptedData - const dataToEncryptHash = invoice[5]; + const isPaid = invoice[4]; + const encryptedStringBase64 = invoice[5]; // encryptedData + const dataToEncryptHash = invoice[6]; if (!encryptedStringBase64 || !dataToEncryptHash) continue; @@ -197,7 +201,7 @@ function ReceivedInvoice() { fetchReceivedInvoices(); console.log("invoices : ", receivedInvoices); - }, [walletClient,litReady]); + }, [walletClient, litReady]); const payInvoice = async (id, amountDue) => { try { diff --git a/frontend/src/page/SentInvoice.jsx b/frontend/src/page/SentInvoice.jsx index 3d6a2d55..f9aa73e7 100644 --- a/frontend/src/page/SentInvoice.jsx +++ b/frontend/src/page/SentInvoice.jsx @@ -57,7 +57,9 @@ function SentInvoice() { const litClientRef = useRef(null); useEffect(() => { - const initLit = async () => { + const initLit = async () => { + try { + setLoading(true); if (!litClientRef.current) { const client = new LitNodeClient({ litNetwork: LIT_NETWORK.DatilDev, @@ -68,9 +70,11 @@ function SentInvoice() { setLitReady(true); console.log(litClientRef.current); } - }; - setLoading(true); - initLit(); + } catch (error) { + console.log("error while lit client initialization"); + } + }; + initLit(); }, []); useEffect(() => { @@ -114,9 +118,9 @@ function SentInvoice() { for (const invoice of res) { const id = invoice[0]; - const isPaid = invoice[3]; - const encryptedStringBase64 = invoice[4]; // encryptedData - const dataToEncryptHash = invoice[5]; + const isPaid = invoice[4]; + const encryptedStringBase64 = invoice[5]; // encryptedData + const dataToEncryptHash = invoice[6]; if (!encryptedStringBase64 || !dataToEncryptHash) continue; From 54fefc308df1c7734d4c964f2c1f40e21fb727cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=85=9A=F0=9F=85=90=F0=9F=85=A1=F0=9F=85=90?= =?UTF-8?q?=F0=9F=85=9D?= Date: Fri, 4 Jul 2025 00:33:39 +0530 Subject: [PATCH 15/21] removed calculation error --- frontend/src/components/CreateInvoice.jsx | 26 +++++++++++++---------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/frontend/src/components/CreateInvoice.jsx b/frontend/src/components/CreateInvoice.jsx index af4872f4..48cdb3cf 100644 --- a/frontend/src/components/CreateInvoice.jsx +++ b/frontend/src/components/CreateInvoice.jsx @@ -1,7 +1,7 @@ import React, { useEffect, useRef, useState } from "react"; import { Input } from "./ui/input"; import { Button } from "./ui/button"; -import { BrowserProvider, Contract, ethers } from "ethers"; +import { BrowserProvider, Contract, ethers, formatUnits, parseUnits } from "ethers"; import { useAccount, useWalletClient } from "wagmi"; import { ChainvoiceABI } from "../contractsABI/ChainvoiceABI"; import { @@ -49,16 +49,20 @@ function CreateInvoice() { const [totalAmountDue, setTotalAmountDue] = useState(0); useEffect(() => { - let total = itemData.reduce((sum, item) => { - return ( - sum + - ((parseFloat(item.qty) || 0) * (parseFloat(item.unitPrice) || 0) - - (parseFloat(item.discount) || 0) + - (parseFloat(item.tax) || 0)) - ); - }, 0); - - setTotalAmountDue(total); + const total = itemData.reduce((sum, item) => { + const qty = parseUnits(item.qty || "0", 18); + const unitPrice = parseUnits(item.unitPrice || "0", 18); + const discount = parseUnits(item.discount || "0", 18); + const tax = parseUnits(item.tax || "0", 18); + // qty * price (then divide by 1e18 to cancel double scaling) + const lineTotal = qty * unitPrice / parseUnits("1", 18); + const adjusted = lineTotal - discount + tax; + + return sum + adjusted; + }, 0n); + + setTotalAmountDue(formatUnits(total, 18)); + }, [itemData]); useEffect(() => { From 3aa274ff0a4398b6bdaecd1a97398253c94a3684 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=85=9A=F0=9F=85=90=F0=9F=85=A1=F0=9F=85=90?= =?UTF-8?q?=F0=9F=85=9D?= Date: Fri, 4 Jul 2025 00:37:22 +0530 Subject: [PATCH 16/21] removed calculation error --- frontend/src/page/ReceivedInvoice.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/page/ReceivedInvoice.jsx b/frontend/src/page/ReceivedInvoice.jsx index 4be168b6..182e7f2d 100644 --- a/frontend/src/page/ReceivedInvoice.jsx +++ b/frontend/src/page/ReceivedInvoice.jsx @@ -537,7 +537,7 @@ function ReceivedInvoice() { {drawerState.selectedInvoice?.items?.map((item, index) => ( - + {item.description} {item.qty.toString()} From 4bc2776dfcbd4597d3ef7f473081302eac496393 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=85=9A=F0=9F=85=90=F0=9F=85=A1=F0=9F=85=90?= =?UTF-8?q?=F0=9F=85=9D?= Date: Fri, 4 Jul 2025 00:38:11 +0530 Subject: [PATCH 17/21] correct typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 05c34c01..c0df07d3 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ forge create contracts/src/Chainvoice.sol:Chainvoice \ `cp frontend/.env.example frontend/.env` - Open the new .env file and update the variables, especially: - `VITE_TRACT_ADDRESS=your_deployed_contract_address_here` + `VITE_CONTRACT_ADDRESS=your_deployed_contract_address_here` Replace your_deployed_contract_address_here with the actual contract address you got after deployment. From ab1bafcc78c6ac80c28e2caa6e8dd0c7f9089174 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=85=9A=F0=9F=85=90=F0=9F=85=A1=F0=9F=85=90?= =?UTF-8?q?=F0=9F=85=9D?= Date: Fri, 4 Jul 2025 01:27:42 +0530 Subject: [PATCH 18/21] Security concern: Insufficient access control validation --- frontend/src/components/CreateInvoice.jsx | 14 ++++-- frontend/src/page/ReceivedInvoice.jsx | 60 +++++++++++++---------- frontend/src/page/SentInvoice.jsx | 23 ++++++--- 3 files changed, 58 insertions(+), 39 deletions(-) diff --git a/frontend/src/components/CreateInvoice.jsx b/frontend/src/components/CreateInvoice.jsx index 48cdb3cf..8053bb37 100644 --- a/frontend/src/components/CreateInvoice.jsx +++ b/frontend/src/components/CreateInvoice.jsx @@ -93,11 +93,15 @@ function CreateInvoice() { name === "discount" || name === "tax" ) { - const qty = parseFloat(updatedItem.qty) || 0; - const unitPrice = parseFloat(updatedItem.unitPrice) || 0; - const discount = parseFloat(updatedItem.discount) || 0; - const tax = parseFloat(updatedItem.tax) || 0; - updatedItem.amount = (qty * unitPrice - discount + tax).toString(); + const qty = parseUnits(updatedItem.qty || "0", 18); + const unitPrice = parseUnits(updatedItem.unitPrice || "0", 18); + const discount = parseUnits(updatedItem.discount || "0", 18); + const tax = parseUnits(updatedItem.tax || "0", 18); + + const lineTotal = (qty * unitPrice) / parseUnits("1", 18); + const finalAmount = lineTotal - discount + tax; + + updatedItem.amount = formatUnits(finalAmount, 18); } return updatedItem; } diff --git a/frontend/src/page/ReceivedInvoice.jsx b/frontend/src/page/ReceivedInvoice.jsx index 182e7f2d..eb679cfa 100644 --- a/frontend/src/page/ReceivedInvoice.jsx +++ b/frontend/src/page/ReceivedInvoice.jsx @@ -57,25 +57,27 @@ function ReceivedInvoice() { const litClientRef = useRef(null); useEffect(() => { - const initLit = async () => { - try { - setLoading(true); - if (!litClientRef.current) { - const client = new LitNodeClient({ - litNetwork: LIT_NETWORK.DatilDev, - debug: false, - }); - await client.connect(); - litClientRef.current = client; - setLitReady(true); - console.log(litClientRef.current); - } - } catch (error) { - console.log("error while lit client initialization"); - } - }; - initLit(); - }, []); + const initLit = async () => { + try { + setLoading(true); + if (!litClientRef.current) { + const client = new LitNodeClient({ + litNetwork: LIT_NETWORK.DatilDev, + debug: false, + }); + await client.connect(); + litClientRef.current = client; + setLitReady(true); + console.log(litClientRef.current); + } + } catch (error) { + console.error("Error while lit client initialization:", error); + } finally { + setLoading(false); + } + }; + initLit(); + }, []); useEffect(() => { if (!walletClient || !litReady) return; @@ -109,14 +111,21 @@ function ReceivedInvoice() { for (const invoice of res) { const id = invoice[0]; + const from = invoice[1].toLowerCase(); + const to = invoice[2].toLowerCase(); const isPaid = invoice[4]; const encryptedStringBase64 = invoice[5]; // encryptedData const dataToEncryptHash = invoice[6]; if (!encryptedStringBase64 || !dataToEncryptHash) continue; - + const currentUserAddress = address.toLowerCase(); + if (currentUserAddress !== from && currentUserAddress !== to) { + console.warn( + `User ${currentUserAddress} not authorized to decrypt invoice ${id}` + ); + continue; + } const ciphertext = atob(encryptedStringBase64); - const accessControlConditions = [ { contractAddress: "", @@ -324,7 +333,7 @@ function ReceivedInvoice() { className="hover:bg-[#32363F] transition duration-300" > {columns.map((column) => { - const value = invoice.user[column.id] || ""; + const value = invoice?.user[column.id] || ""; if (column.id === "to") { return ( - {invoice.user.address + {invoice.user?.address ? `${invoice.user.address.substring( 0, 10 @@ -558,10 +567,7 @@ function ReceivedInvoice() {

{/* Fee for invoice pay : {ethers.formatUnits(fee)} ETH */} - Fee for invoice pay : {parseFloat( - ethers.formatUnits(fee) - )}{" "} - ETH + Fee for invoice pay : {ethers.formatUnits(fee)} ETH

{" "} diff --git a/frontend/src/page/SentInvoice.jsx b/frontend/src/page/SentInvoice.jsx index f9aa73e7..277de25b 100644 --- a/frontend/src/page/SentInvoice.jsx +++ b/frontend/src/page/SentInvoice.jsx @@ -71,12 +71,14 @@ function SentInvoice() { console.log(litClientRef.current); } } catch (error) { - console.log("error while lit client initialization"); - } + console.error("Error while lit client initialization:", error); + } finally { + setLoading(false); + } }; initLit(); }, []); - + useEffect(() => { if (!walletClient || !litReady) return; @@ -118,12 +120,20 @@ function SentInvoice() { for (const invoice of res) { const id = invoice[0]; + const from = invoice[1].toLowerCase(); + const to = invoice[2].toLowerCase(); const isPaid = invoice[4]; const encryptedStringBase64 = invoice[5]; // encryptedData const dataToEncryptHash = invoice[6]; if (!encryptedStringBase64 || !dataToEncryptHash) continue; - + const currentUserAddress = address.toLowerCase(); + if (currentUserAddress !== from && currentUserAddress !== to) { + console.warn( + `User ${currentUserAddress} not authorized to decrypt invoice ${id}` + ); + continue; + } const ciphertext = atob(encryptedStringBase64); const accessControlConditions = [ { @@ -322,7 +332,7 @@ function SentInvoice() { borderColor: "#25272b", }} > - {invoice.client.address + {invoice.client?.address ? `${invoice.client.address.substring( 0, 10 @@ -519,8 +529,7 @@ function SentInvoice() {

- Fee for invoice pay : {fee} ETH - {/* Fee for invoice pay : {ethers.formatUnits(fee)} ETH */} + Fee for invoice pay : {ethers.formatUnits(fee)} ETH

{" "} From 673ffa10fc1f820d1c0d9d36ec6b6ff9b77fb9cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=85=9A=F0=9F=85=90=F0=9F=85=A1=F0=9F=85=90?= =?UTF-8?q?=F0=9F=85=9D?= Date: Fri, 4 Jul 2025 16:33:45 +0530 Subject: [PATCH 19/21] Updated dashboard,treasury account and routes --- frontend/package.json | 3 + frontend/public/logo.png | Bin 0 -> 25311 bytes frontend/src/App.jsx | 115 ++--- frontend/src/components/CreateInvoice.jsx | 593 ++++++++++++++-------- frontend/src/components/Navbar.jsx | 306 +++++++---- frontend/src/page/Applayout.jsx | 23 +- frontend/src/page/Home.jsx | 186 ++++--- frontend/src/page/Landing.jsx | 357 +++++++------ frontend/src/page/ReceivedInvoice.jsx | 57 ++- frontend/src/page/SentInvoice.jsx | 200 ++++---- frontend/src/page/Treasure.jsx | 250 ++++++--- frontend/{ | 0 12 files changed, 1295 insertions(+), 795 deletions(-) create mode 100644 frontend/public/logo.png create mode 100644 frontend/{ diff --git a/frontend/package.json b/frontend/package.json index ce268161..84beb46c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -30,6 +30,7 @@ "eth-crypto": "^2.7.0", "ethereum-unit-converter": "^0.0.17", "ethers": "^6.13.5", + "framer-motion": "^12.23.0", "html2canvas": "^1.4.1", "jspdf": "^3.0.0", "lucide-react": "^0.471.1", @@ -37,6 +38,8 @@ "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", "react-hook-form": "^7.54.2", + "react-hot-toast": "^2.5.2", + "react-icons": "^5.5.0", "react-router-dom": "^7.1.1", "react-to-print": "^3.0.5", "tailwind-merge": "^2.6.0", diff --git a/frontend/public/logo.png b/frontend/public/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..46809fdde63a420f1ea002156bb6e5d99fb06b12 GIT binary patch literal 25311 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

j&?*{ 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 - - - -

- - - }> - } /> - } > - }/> - }/> - }/> +
+ + + + +
+ + + }> + } /> + }> + } /> + } /> + } /> + + } /> + } /> + } /> + } /> - } /> - } /> - } /> - } /> - - - -
-
-
-
+ + +
+ + +
- ) + ); } -export default App; \ No newline at end of file +export default App; diff --git a/frontend/src/components/CreateInvoice.jsx b/frontend/src/components/CreateInvoice.jsx index 8053bb37..a496f2a2 100644 --- a/frontend/src/components/CreateInvoice.jsx +++ b/frontend/src/components/CreateInvoice.jsx @@ -10,7 +10,7 @@ import { PopoverTrigger, } from "@/components/ui/popover"; import { Calendar } from "@/components/ui/calendar"; -import { CalendarIcon, Loader2 } from "lucide-react"; +import { CalendarIcon, Loader2, PlusIcon } from "lucide-react"; import { cn } from "@/lib/utils"; import { format } from "date-fns"; import { Label } from "./ui/label"; @@ -168,11 +168,12 @@ function CreateInvoice() { // 2. Setup Lit const litNodeClient = litClientRef.current; + if (!litNodeClient) { alert("Lit client not initialized"); return; } - + const accessControlConditions = [ { contractAddress: "", @@ -198,7 +199,7 @@ function CreateInvoice() { }, }, ]; - + // 3. Encrypt const { ciphertext, dataToEncryptHash } = await encryptString( { @@ -207,7 +208,7 @@ function CreateInvoice() { }, litNodeClient ); - + const sessionSigs = await litNodeClient.getSessionSigs({ chain: "ethereum", resourceAbilityRequests: [ @@ -239,7 +240,7 @@ function CreateInvoice() { }); const encryptedStringBase64 = btoa(ciphertext); - + // 4. Send to contract const contract = new Contract( import.meta.env.VITE_CONTRACT_ADDRESS, @@ -254,8 +255,10 @@ function CreateInvoice() { dataToEncryptHash ); - console.log("Invoice created:", tx); - setTimeout(() => navigate("/home/sent"), 4000); + const receipt = await tx.wait(); + + console.log("Transaction receipt:", receipt); + setTimeout(() => navigate("/dashboard/sent"), 4000); } catch (err) { console.error("Encryption or transaction failed:", err); alert("Failed to create invoice."); @@ -309,258 +312,408 @@ function CreateInvoice() { return (
-

Create New Invoice Request

-
- - -

Issued Date

- - -

Due Date

- - - - - - - - +

+ Create New Invoice Request +

+ +
+
+ + +
+ +
+ + +
+ +
+ + + + + + + { + if (date) { + setDueDate(date); + document.dispatchEvent( + new KeyboardEvent("keydown", { key: "Escape" }) + ); + } + }} + initialFocus + disabled={(date) => date < new Date()} + /> + + +
+
-
-
-

From (Your Information)

+
+ {/* Your Information */} +
+

+ From (Your Information) +

-
-

Add Your Info

-
- - + +
+
+
+ + +
+
+ + +
-
- - +
+
+ + +
+
+ + +
-
- - + +
+
+ + +
+
+ + +
-
-

Client Information

+ {/* Client Information */} +
+

+ Client Information +

-
-

Add Client Info

-
- - + +
+
+
+ + +
+
+ + +
-
- - +
+
+ + +
+
+ + +
-
- - + +
+
+ + +
+
+ + +
-
-
-
DESCRIPTION
-
QTY
-
UNIT PRICE
-
DISCOUNT
-
TAX(%)
-
AMOUNT
+ {/* Invoice Items Section */} +
+
+
DESCRIPTION
+
QTY
+
UNIT PRICE
+
DISCOUNT
+
TAX(%)
+
AMOUNT
+ {/*
AMOUNT
*/}
-
+ +
{itemData.map((_, index) => ( -
- handleItemData(e, index)} - /> - handleItemData(e, index)} - /> - handleItemData(e, index)} - /> - handleItemData(e, index)} - /> - handleItemData(e, index)} - /> - handleItemData(e, index)} - /> +
+ {/* Item Fields */} +
+ handleItemData(e, index)} + /> +
+
+ handleItemData(e, index)} + /> +
+
+ handleItemData(e, index)} + /> +
+
+ handleItemData(e, index)} + /> +
+
+ handleItemData(e, index)} + /> +
+
+ +
+ + {index > 0 && ( + + )}
))}
-

Total : {totalAmountDue}

-
+
+ +
+
+ Total: + + {/* {totalAmountDue} ETH */} + {totalAmountDue} cBTC + +
+
+
+
+ + {/* Form Actions */} +
+ +
+ + Creating Invoice... +
) : ( - + "Create Invoice" )} -
+
diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx index a8067e25..08cd5d7d 100644 --- a/frontend/src/components/Navbar.jsx +++ b/frontend/src/components/Navbar.jsx @@ -1,103 +1,237 @@ -import { ConnectButton } from '@rainbow-me/rainbowkit'; -import React, { useEffect } from 'react' -import { Link, useNavigate } from 'react-router-dom'; -import { useAccount } from 'wagmi'; -import AccountBalanceWalletIcon from '@mui/icons-material/AccountBalanceWallet'; -import HomeIcon from '@mui/icons-material/Home'; +// Updated Navbar.js +import { ConnectButton } from "@rainbow-me/rainbowkit"; +import React, { useEffect, useState } from "react"; +import { Link, useNavigate, useLocation } from "react-router-dom"; +import { useAccount } from "wagmi"; +import AccountBalanceWalletIcon from "@mui/icons-material/AccountBalanceWallet"; +import HomeIcon from "@mui/icons-material/Home"; +import DashboardIcon from "@mui/icons-material/Dashboard"; +import FeaturedPlayListIcon from "@mui/icons-material/FeaturedPlayList"; +import DesignServicesIcon from "@mui/icons-material/DesignServices"; +import { motion } from "framer-motion"; function Navbar() { - const { address } = useAccount(); - const navigate = useNavigate() - useEffect(() => { - if (!address) navigate('/') - }, [address]); - const handleScrollToFeature = () => { - const featureSection = document.getElementById("feature-section"); - const offset = 200; - const position = featureSection.offsetTop - offset; + const { address, isConnected } = useAccount(); + const navigate = useNavigate(); + const location = useLocation(); + const [hasConnected, setHasConnected] = useState(false); + const [isScrolled, setIsScrolled] = useState(false); - window.scrollTo({ - top: position, - behavior: "smooth", - }); + // Improved active route detection + const isActive = (path) => { + if (path.startsWith("/#")) { + return location.pathname === "/" && location.hash === path.substring(1); + } + return ( + location.pathname.startsWith(path) && + (path !== "/" || location.pathname === "/") + ); }; - const handleScrollToHome = () => { - const featureSection = document.getElementById("home-section"); - const offset = 200; - const position = featureSection.offsetTop - offset; + useEffect(() => { + if ( + address && + !hasConnected && + !location.pathname.startsWith("/dashboard") + ) { + navigate("/dashboard/create"); + setHasConnected(true); + } + if (!address) { + navigate("/"); + setHasConnected(false); + } + + const handleScroll = () => { + setIsScrolled(window.scrollY > 10); + }; + window.addEventListener("scroll", handleScroll); + return () => window.removeEventListener("scroll", handleScroll); + }, [isConnected, hasConnected, navigate, location.pathname]); - window.scrollTo({ - top: position, - behavior: "smooth", - }); + const handleScroll = (sectionId) => { + if (location.pathname !== "/") { + navigate("/"); + setTimeout(() => { + const section = document.getElementById(sectionId); + if (section) { + window.scrollTo({ + top: section.offsetTop - 200, + behavior: "smooth", + }); + } + }, 100); + } else { + const section = document.getElementById(sectionId); + if (section) { + window.scrollTo({ + top: section.offsetTop - 200, + behavior: "smooth", + }); + } + } }; - const handleScrollToService = () => { - const featureSection = document.getElementById("service-section"); - const offset = 200; - const position = featureSection.offsetTop - offset; - window.scrollTo({ - top: position, - behavior: "smooth", - }); - }; + const navItems = [ + { + name: "Home", + icon: , + action: () => handleScroll("home-section"), + path: "/", + }, + { + name: "Features", + icon: , + action: () => handleScroll("feature-section"), + path: "/#feature-section", + }, + { + name: "Services", + icon: , + action: () => handleScroll("service-section"), + path: "/#service-section", + }, + ]; + + const appItems = [ + { + name: "Dashboard", + icon: , + path: "/dashboard", + activePaths: [ + "/dashboard/create", + "/dashboard/sent", + "/dashboard/pending", + ], + }, + { + name: "Treasure", + icon: , + path: "/treasure", + }, + ]; return ( - <> -
-
navigate('/')} >Chainvoice
-
-
    - { - address && ( -
  • - - Home -
  • - ) - } - { - !address && ( -
    -
  • - -
  • -
  • - -
  • -
  • - -
  • -
    + +
    +
    + navigate("/")} + > + logo +

    + Chainvoice +

    +
    + +
    + {navItems.map((item) => ( + + {item.icon} + {item.name} + + ))} - ) - } - {/*
  • - How It Works -
  • */} + {isConnected && + appItems.map((item) => ( + + {item?.path === "/dashboard" ? ( + + {item.icon} + {item.name} + + ) : ( + + {item.icon} + {item.name} + + )} + + ))} - { - address && ( -
  • - - Treasure -
  • - ) - } - {/*
  • - About Us -
  • */} - + + + +
    -
+ {/* Mobile menu button */} +
+ + +
- - {/* Add padding to prevent content from being hidden behind the navbar */} -
- - - ) + + ); } export default Navbar; diff --git a/frontend/src/page/Applayout.jsx b/frontend/src/page/Applayout.jsx index 41ae62e2..8b459d0c 100644 --- a/frontend/src/page/Applayout.jsx +++ b/frontend/src/page/Applayout.jsx @@ -1,20 +1,19 @@ +// AppLayout.js import Footer from "@/components/Footer"; import Navbar from "@/components/Navbar"; import React from "react"; import { Outlet } from "react-router-dom"; function Applayout() { - return ( - <> -
- -
- -
-
-
- - ) + return ( +
+ +
+ +
+
+
+ ); } -export default Applayout; \ No newline at end of file +export default Applayout; diff --git a/frontend/src/page/Home.jsx b/frontend/src/page/Home.jsx index 24381b36..14b602ec 100644 --- a/frontend/src/page/Home.jsx +++ b/frontend/src/page/Home.jsx @@ -1,111 +1,141 @@ -import * as React from 'react'; -import Box from '@mui/material/Box'; -import Drawer from '@mui/material/Drawer'; -import List from '@mui/material/List'; -import ListItem from '@mui/material/ListItem'; -import ListItemButton from '@mui/material/ListItemButton'; -import ListItemIcon from '@mui/material/ListItemIcon'; -import ListItemText from '@mui/material/ListItemText'; -import AddCircleIcon from '@mui/icons-material/AddCircle'; -import PendingIcon from '@mui/icons-material/Pending'; -import MarkEmailReadIcon from '@mui/icons-material/MarkEmailRead'; -import { Outlet, useNavigate } from 'react-router-dom'; +// Home.js +import * as React from "react"; +import Box from "@mui/material/Box"; +import Drawer from "@mui/material/Drawer"; +import List from "@mui/material/List"; +import ListItem from "@mui/material/ListItem"; +import ListItemButton from "@mui/material/ListItemButton"; +import ListItemIcon from "@mui/material/ListItemIcon"; +import ListItemText from "@mui/material/ListItemText"; +import MailOutlineIcon from "@mui/icons-material/MailOutline"; +import DraftsIcon from "@mui/icons-material/Drafts"; +import AddCircleOutlineIcon from "@mui/icons-material/AddCircleOutline"; +import { Outlet, useNavigate, useLocation } from "react-router-dom"; + export default function Home() { const navigate = useNavigate(); + const location = useLocation(); - const list = [ + const menuItems = [ { - text: 'Sent Payment Requests', - icon: , - route: 'sent' + text: "Sent Requests", + icon: , + route: "sent", + color: "#4ade80", }, { - text: 'Received Payment Requests', - icon: , - route: 'pending' + text: "Received Requests", + icon: , + route: "pending", + color: "#60a5fa", }, { - text: 'Create New Invoice Request', - icon: , - route: 'create' + text: "New Invoice", + icon: , + route: "create", + color: "#f472b6", }, - ] - return ( - <> -
+ ]; -

- Welcome Back! -

+ return ( +
+
+ {" "} +

+ {" "} + Welcome Back! +

+
- - - + {/* Sidebar Navigation */} + + + -
- +
); } diff --git a/frontend/src/page/Landing.jsx b/frontend/src/page/Landing.jsx index 5636c72c..e52d7b45 100644 --- a/frontend/src/page/Landing.jsx +++ b/frontend/src/page/Landing.jsx @@ -2,161 +2,220 @@ import { ConnectButton } from "@rainbow-me/rainbowkit"; import React, { useEffect } from "react"; import { useNavigate } from "react-router-dom"; import { useAccount } from "wagmi"; -import ShieldIcon from '@mui/icons-material/Shield'; -import EmailIcon from '@mui/icons-material/Email'; -import GavelIcon from '@mui/icons-material/Gavel'; -import LeaderboardIcon from '@mui/icons-material/Leaderboard'; +import ShieldIcon from "@mui/icons-material/Shield"; +import EmailIcon from "@mui/icons-material/Email"; +import GavelIcon from "@mui/icons-material/Gavel"; +import LeaderboardIcon from "@mui/icons-material/Leaderboard"; function Landing() { - const Account = useAccount(); - const navigate = useNavigate(); + const Account = useAccount(); + const navigate = useNavigate(); - useEffect(() => { - if (Account.address) navigate('/home/sent'); - }, [Account, Account.address]) - return ( - <> -
-
-

Decentralized Payment Requests &
Invoice Automation

-

One click to effortless invoicing — process payments in any ERC20 token with transparency and trustless efficiency!

- {/*

Only with

-
Chainvoice
*/} + // useEffect(() => { + // if (Account.address) navigate("/home/sent"); + // }, [Account, Account.address]); -

Connect with your wallet and Get Started!

-
- -
+ return ( + <> +
+
+

+ Decentralized Payment Requests & +
+ Invoice Automation +

+

+ One click to effortless invoicing — process payments in any ERC20 + token with transparency and trustless efficiency! +

+ {/*

Only with

+
Chainvoice
*/} -
-

Trusted by 1000+ users

-

Smart Contract Driven 100% Secure

-
-
-
- -
-
-
+

+ Connect with your wallet and Get Started! +

+
+ +
-
-

- A powerful and secure{" "} - - invoicing solution - {" "} - designed for growing businesses. -

-
- {[ - { - title: "Secure and Transparent Transactions", - description: - "Leverage blockchain technology to ensure encrypted, tamper-proof, and immutable transactions. Provide complete transparency for invoice verification.", - icon: , - }, - { - title: "Send and Receive Invoices", - description: - "Effortlessly create and manage invoices with a few clicks. Track real-time status and maintain a comprehensive invoice dashboard.", - icon: , - }, - { - title: "Smart Contract Integration", - description: - "Automate payment processes with secure smart contracts. Ensure funds are released only when invoice conditions are met, reducing intermediary dependencies.", - icon: , - }, - { - title: "Comprehensive Invoice Tracking", - description: - "Gain complete visibility into your invoice lifecycle. Monitor payment statuses, track financial performance, and manage all transactions seamlessly.", - icon: , - }, - ].map((feature, index) => ( -
-
- {feature.icon} -
-

{feature.title}

-

{feature.description}

-
- ))} -
-
-
- Invoice Illustration +
+

+ + Trusted by 1000+ users +

+

+ + Smart Contract Driven 100% Secure +

+
+
+
+ +
+
+
+
+

+ A powerful and secure{" "} + + invoicing solution + {" "} + designed for growing businesses. +

+
+ {[ + { + title: "Secure and Transparent Transactions", + description: + "Leverage blockchain technology to ensure encrypted, tamper-proof, and immutable transactions. Provide complete transparency for invoice verification.", + icon: , + }, + { + title: "Send and Receive Invoices", + description: + "Effortlessly create and manage invoices with a few clicks. Track real-time status and maintain a comprehensive invoice dashboard.", + icon: , + }, + { + title: "Smart Contract Integration", + description: + "Automate payment processes with secure smart contracts. Ensure funds are released only when invoice conditions are met, reducing intermediary dependencies.", + icon: , + }, + { + title: "Comprehensive Invoice Tracking", + description: + "Gain complete visibility into your invoice lifecycle. Monitor payment statuses, track financial performance, and manage all transactions seamlessly.", + icon: , + }, + ].map((feature, index) => ( +
+
+ {feature.icon}
-
-
-
- Aeroplane 2 -
-

Start Sending Your

-

Invoice Today!

-
- Aeroplane 1 -
- -
-
-
-
Chainvoice
-

Secure & Smart Invoicing

-
-
- -
-

Quick Links

-
    - {["Home", "Feature", "Treasure", "Service", "Invoice"].map((link) => ( -
  • - - {link} - -
  • - ))} -
-
-
-

Services

-
    - {["Blog & Article", "Terms ans Conditions", "Privacy Policy", "Contact Us", "Invoice"].map((link) => ( -
  • - - {link} - -
  • - ))} -
-
-
-

Contact

-
    - {["chainvoice@gmail.com"].map((link) => ( -
  • - - {link} - -
  • - ))} -
-
-
-
-
-
+

+ {feature.title} +

+

{feature.description}

+
+ ))} +
+
+
+ Invoice Illustration +
+ +
+
+ Aeroplane 2 +
+

+ {" "} + Start Sending Your +

+

+ Invoice Today! +

+
+ Aeroplane 1 +
- - - ) +
+
+
+
+ logo +

+ Chain + voice +

+
+

+ Secure & Smart Invoicing +

+
+
+
+

Quick Links

+
    + {["Home", "Feature", "Treasure", "Service", "Invoice"].map( + (link) => ( +
  • + + {link} + +
  • + ) + )} +
+
+
+

Services

+
    + {[ + "Blog & Article", + "Terms ans Conditions", + "Privacy Policy", + "Contact Us", + "Invoice", + ].map((link) => ( +
  • + + {link} + +
  • + ))} +
+
+
+

Contact

+
    + {["chainvoice@gmail.com"].map((link) => ( +
  • + + {link} + +
  • + ))} +
+
+
+
+
+
+ + ); } export default Landing; diff --git a/frontend/src/page/ReceivedInvoice.jsx b/frontend/src/page/ReceivedInvoice.jsx index eb679cfa..8a2b2f3c 100644 --- a/frontend/src/page/ReceivedInvoice.jsx +++ b/frontend/src/page/ReceivedInvoice.jsx @@ -38,6 +38,14 @@ const columns = [ function ReceivedInvoice() { const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(10); + const { data: walletClient } = useWalletClient(); + const { address } = useAccount(); + const [loading, setLoading] = useState(false); + const [receivedInvoices, setReceivedInvoice] = useState([]); + const [fee, setFee] = useState(0); + const [error, setError] = useState(null); + const [litReady, setLitReady] = useState(false); + const litClientRef = useRef(null); const handleChangePage = (event, newPage) => { setPage(newPage); @@ -48,14 +56,6 @@ function ReceivedInvoice() { setPage(0); }; - const { data: walletClient } = useWalletClient(); - const { address } = useAccount(); - const [loading, setLoading] = useState(false); - const [receivedInvoices, setReceivedInvoice] = useState([]); - const [fee, setFee] = useState(0); - const [litReady, setLitReady] = useState(false); - const litClientRef = useRef(null); - useEffect(() => { const initLit = async () => { try { @@ -80,15 +80,24 @@ function ReceivedInvoice() { }, []); useEffect(() => { - if (!walletClient || !litReady) return; + if (!walletClient || !address || !litReady) return; const fetchReceivedInvoices = async () => { try { setLoading(true); - + setError(null); const provider = new BrowserProvider(walletClient); const signer = await provider.getSigner(); + const network = await provider.getNetwork(); + if (network.chainId != 5115) { + setError( + `Failed to load invoices. You're connected to the "${network.name}" network, but your invoices are on the "Citrea" testnet. Please switch to Sepolia and try again.` + ); + + setLoading(false); + return; + } // 1. Setup Lit Node const litNodeClient = litClientRef.current; @@ -106,6 +115,13 @@ function ReceivedInvoice() { const res = await contract.getReceivedInvoices(address); console.log("getReceivedInvoices raw response:", res); + // First check if user has any invoices + if (!res || !Array.isArray(res) || res.length === 0) { + setReceivedInvoice([]); + const fee = await contract.fee(); + setFee(fee); + return; + } const decryptedInvoices = []; @@ -297,8 +313,12 @@ function ReceivedInvoice() { }} > {loading ? ( -

loading........

- ) : receivedInvoices?.length > 0 ? ( +

Loading invoices...

+ ) : error ? ( +

{error}

+ ) : receivedInvoices.length === 0 ? ( +

No invoices found

+ ) : ( <> {columns.map((column) => { - const value = invoice?.user[column.id] || ""; + const value = invoice.user[column.id] || ""; if (column.id === "to") { return ( - {invoice.user?.address + {invoice.user.address ? `${invoice.user.address.substring( 0, 10 @@ -479,8 +499,6 @@ function ReceivedInvoice() { }} /> - ) : ( -

No invoices found

)} @@ -546,7 +564,7 @@ function ReceivedInvoice() { {drawerState.selectedInvoice?.items?.map((item, index) => ( -
+ @@ -567,7 +585,10 @@ function ReceivedInvoice() {

{/* Fee for invoice pay : {ethers.formatUnits(fee)} ETH */} - Fee for invoice pay : {ethers.formatUnits(fee)} ETH + Fee for invoice pay : {parseFloat( + ethers.formatUnits(fee) + )}{" "} + ETH

{" "} diff --git a/frontend/src/page/SentInvoice.jsx b/frontend/src/page/SentInvoice.jsx index 277de25b..350b0615 100644 --- a/frontend/src/page/SentInvoice.jsx +++ b/frontend/src/page/SentInvoice.jsx @@ -52,6 +52,7 @@ function SentInvoice() { const [invoiceItems, setInvoiceItems] = useState([]); const [loading, setLoading] = useState(false); const [fee, setFee] = useState(0); + const [error, setError] = useState(null); const { address } = useAccount(); const [litReady, setLitReady] = useState(false); const litClientRef = useRef(null); @@ -270,7 +271,7 @@ function SentInvoice() { return (

-

Your Sent Invoice Request

+

Your Sent Invoice Request

{loading ? ( -

Loading...

- ) : sentInvoices?.length > 0 ? ( +

Loading invoices...

+ ) : error ? ( +

{error}

+ ) : sentInvoices.length === 0 ? ( +

No invoices found

+ ) : ( <>
{item.description} {item.qty.toString()}
- {sentInvoices.length > 0 && - sentInvoices - .slice( - page * rowsPerPage, - page * rowsPerPage + rowsPerPage - ) - .map((invoice, index) => ( - - {columns.map((column) => { - const value = invoice?.client[column.id]; - if (column.id === "to") { - return ( - - {invoice.client?.address - ? `${invoice.client.address.substring( - 0, - 10 - )}...${invoice.client.address.substring( - invoice.client.address.length - 10 - )}` - : "N/A"} - - ); - } - if (column.id === "amountDue") { - return ( - - {invoice.amountDue} ETH - {/* {ethers.formatUnits(invoice.amountDue)} ETH */} - - ); - } - if (column.id === "isPaid") { - return ( - - - - ); - } - if (column.id === "detail") { - return ( - ( + + {columns.map((column) => { + const value = invoice?.client[column.id]; + if (column.id === "to") { + return ( + + {invoice.client.address + ? `${invoice.client.address.substring( + 0, + 10 + )}...${invoice.client.address.substring( + invoice.client.address.length - 10 + )}` + : "N/A"} + + ); + } + if (column.id === "amountDue") { + return ( + + {invoice.amountDue} ETH + + ); + } + if (column.id === "isPaid") { + return ( + + - - ); - } + {invoice.isPaid ? "Paid" : "Not Paid"} + + + ); + } + if (column.id === "detail") { return ( - {value} + ); - })} - - ))} + } + return ( + + {value} + + ); + })} + + ))}
@@ -442,8 +442,6 @@ function SentInvoice() { }} /> - ) : ( -

No invoices found

)} @@ -529,7 +527,9 @@ function SentInvoice() {

- Fee for invoice pay : {ethers.formatUnits(fee)} ETH + Fee for invoice pay : {parseFloat(ethers.formatUnits(fee))}{" "} + ETH + {/* Fee for invoice pay : {ethers.formatUnits(fee)} ETH */}

{" "} diff --git a/frontend/src/page/Treasure.jsx b/frontend/src/page/Treasure.jsx index 8ccac602..9b66ae51 100644 --- a/frontend/src/page/Treasure.jsx +++ b/frontend/src/page/Treasure.jsx @@ -4,11 +4,24 @@ import { ChainvoiceABI } from "@/contractsABI/ChainvoiceABI"; import { BrowserProvider, Contract, ethers } from "ethers"; import { useState, useEffect } from "react"; import { useWalletClient } from "wagmi"; +import { + Loader2, + Shield, + Banknote, + Key, + Wallet, + DollarSignIcon, +} from "lucide-react"; const Treasure = () => { const [treasureAmount, setTreasureAmount] = useState(0); + const [fee, setFee] = useState(0); const { data: walletClient } = useWalletClient(); - const [loading, setLoading] = useState(false); + const [loading, setLoading] = useState({ + fetch: false, + setAddress: false, + withdraw: false, + }); const [treasuryAddress, setTreasuryAddress] = useState(""); const [newTreasuryAddress, setNewTreasuryAddress] = useState(""); @@ -16,18 +29,26 @@ const Treasure = () => { const fetchTreasureAmount = async () => { try { if (!walletClient) return; - setLoading(true); + setLoading((prev) => ({ ...prev, fetch: true })); const provider = new BrowserProvider(walletClient); const signer = await provider.getSigner(); - const contract = new Contract(import.meta.env.VITE_CONTRACT_ADDRESS, ChainvoiceABI, signer); - const amt = await contract.accumulatedFees(); + const contract = new Contract( + import.meta.env.VITE_CONTRACT_ADDRESS, + ChainvoiceABI, + signer + ); + const [amt, add, feeAmt] = await Promise.all([ + contract.accumulatedFees(), + contract.treasuryAddress(), + contract.fee(), + ]); setTreasureAmount(ethers.formatUnits(amt)); - const add = await contract.treasuryAddress(); setTreasuryAddress(add); + setFee(ethers.formatUnits(feeAmt)); } catch (error) { console.error("Error fetching treasure amount:", error); } finally { - setLoading(false); + setLoading((prev) => ({ ...prev, fetch: false })); } }; @@ -36,111 +57,194 @@ const Treasure = () => { const handleSetTreasuryAddress = async () => { if (!ethers.isAddress(newTreasuryAddress)) { - console.error("Invalid address:", newTreasuryAddress); + alert("Please enter a valid Ethereum address"); return; } try { - if (!walletClient || !newTreasuryAddress) return; - setLoading(true); + if (!walletClient) return; + setLoading((prev) => ({ ...prev, setAddress: true })); const provider = new BrowserProvider(walletClient); const signer = await provider.getSigner(); - const contract = new Contract(import.meta.env.VITE_CONTRACT_ADDRESS, ChainvoiceABI, signer); + const contract = new Contract( + import.meta.env.VITE_CONTRACT_ADDRESS, + ChainvoiceABI, + signer + ); const tx = await contract.setTreasuryAddress(newTreasuryAddress); await tx.wait(); setTreasuryAddress(newTreasuryAddress); setNewTreasuryAddress(""); + alert("Treasury address updated successfully!"); } catch (error) { console.error("Error setting treasury address:", error); + alert(error.message || "Failed to update treasury address"); } finally { - setLoading(false); + setLoading((prev) => ({ ...prev, setAddress: false })); } }; const handleWithdrawCollection = async () => { try { if (!walletClient) return; - setLoading(true); + setLoading((prev) => ({ ...prev, withdraw: true })); const provider = new BrowserProvider(walletClient); const signer = await provider.getSigner(); - const contract = new Contract(import.meta.env.VITE_CONTRACT_ADDRESS, ChainvoiceABI, signer); - const amt = await contract.withdraw(); - setTreasureAmount(ethers.formatUnits(amt)); + const contract = new Contract( + import.meta.env.VITE_CONTRACT_ADDRESS, + ChainvoiceABI, + signer + ); + const tx = await contract.withdraw(); + await tx.wait(); + const newAmt = await contract.accumulatedFees(); + setTreasureAmount(ethers.formatUnits(newAmt)); + alert("Funds withdrawn successfully!"); } catch (error) { console.error("Error withdrawing collection:", error); + alert(error.message || "Failed to withdraw funds"); } finally { - setLoading(false); + setLoading((prev) => ({ ...prev, withdraw: false })); } }; return ( -

- -
-

- Treasure Account -

-

- The treasure account is the account where all the fees collected from the platform are stored. -

- -
-

Treasure Amount:

-

- {loading ? "Loading..." : `${treasureAmount} ETH`} -

+
+
+
+
+
+
+
+
+ +
+

+ Treasury Vault +

+
+
+ + + Current Balance: + + + {loading.fetch ? ( + + ) : ( + `${treasureAmount} cBTC` + )} + +
+
+ + + Fee Per Transaction: + + + {loading.fetch ? ( + + ) : ( + `${fee} cBTC` + )} + +
+
+ + + Admin Access Only + +
+
+
+
+
-
-

Treasury Address:

-

- {loading ? "Loading..." : treasuryAddress} + {/* Content */} +

+

+ Treasury Controls +

+

+ Secure management of platform funds and treasury settings

-
-
-

Set New Treasury Address

-
- setNewTreasuryAddress(e.target.value)} - /> +
+
+ +

Current Treasury

+
+
+

+ {loading.fetch ? ( + + ) : treasuryAddress ? ( + treasuryAddress + ) : ( + "Not configured" + )} +

+
+
+ +
+
+ +

+ Update Treasury Address +

+
+
+ setNewTreasuryAddress(e.target.value)} + className="flex-1 bg-gray-800 border-gray-700 text-white font-mono text-sm" + /> + +
+

+ Requires contract owner privileges +

+
+ + {/* Withdraw */} +
+
+ +

Funds Withdrawal

+
+

+ Will be sent to the current treasury address +

-

- Disclaimer: Only the owner of the contract can set the treasury address. -

- -
-

Withdraw Collection

- -
-
- -
- Treasure Illustration
); }; -export default Treasure; \ No newline at end of file +export default Treasure; diff --git a/frontend/{ b/frontend/{ new file mode 100644 index 00000000..e69de29b From 4cd4795bab012833d7e8f315aafedcd17fc2c72c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=85=9A=F0=9F=85=90=F0=9F=85=A1=F0=9F=85=90?= =?UTF-8?q?=F0=9F=85=9D?= Date: Sat, 5 Jul 2025 14:32:50 +0530 Subject: [PATCH 20/21] correct some error --- frontend/src/page/Landing.jsx | 2 +- frontend/src/page/Treasure.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/page/Landing.jsx b/frontend/src/page/Landing.jsx index e52d7b45..4f87f6a5 100644 --- a/frontend/src/page/Landing.jsx +++ b/frontend/src/page/Landing.jsx @@ -180,7 +180,7 @@ function Landing() {
    {[ "Blog & Article", - "Terms ans Conditions", + "Terms and Conditions", "Privacy Policy", "Contact Us", "Invoice", diff --git a/frontend/src/page/Treasure.jsx b/frontend/src/page/Treasure.jsx index 9b66ae51..e2cbaf7b 100644 --- a/frontend/src/page/Treasure.jsx +++ b/frontend/src/page/Treasure.jsx @@ -94,7 +94,7 @@ const Treasure = () => { ChainvoiceABI, signer ); - const tx = await contract.withdraw(); + const tx = await contract.withdrawFees(); await tx.wait(); const newAmt = await contract.accumulatedFees(); setTreasureAmount(ethers.formatUnits(newAmt)); From a54324ccb66f067fe1f3512638369a097358276f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=85=9A=F0=9F=85=90=F0=9F=85=A1=F0=9F=85=90?= =?UTF-8?q?=F0=9F=85=9D?= Date: Tue, 8 Jul 2025 12:27:49 +0530 Subject: [PATCH 21/21] added mobile menu --- frontend/src/components/CreateInvoice.jsx | 11 -- frontend/src/components/Navbar.jsx | 127 ++++++++++++++++++---- frontend/src/page/Home.jsx | 50 +++++---- frontend/src/page/ReceivedInvoice.jsx | 2 +- frontend/src/page/SentInvoice.jsx | 6 - 5 files changed, 134 insertions(+), 62 deletions(-) diff --git a/frontend/src/components/CreateInvoice.jsx b/frontend/src/components/CreateInvoice.jsx index 2d32b94e..642b0868 100644 --- a/frontend/src/components/CreateInvoice.jsx +++ b/frontend/src/components/CreateInvoice.jsx @@ -135,8 +135,6 @@ function CreateInvoice() { const provider = new BrowserProvider(walletClient); const signer = await provider.getSigner(); - console.log("data >>>>>> ", data); - console.log("acc :", account.address); // 1. Prepare invoice data const invoicePayload = { amountDue: totalAmountDue, @@ -243,7 +241,6 @@ function CreateInvoice() { ChainvoiceABI, signer ); - console.log("amt : ",ethers.parseEther(totalAmountDue.toString())); const tx = await contract.createInvoice( data.clientAddress, ethers.parseEther(totalAmountDue.toString()), @@ -252,8 +249,6 @@ function CreateInvoice() { ); const receipt = await tx.wait(); - - console.log("Transaction receipt:", receipt); setTimeout(() => navigate("/dashboard/sent"), 4000); } catch (err) { @@ -450,10 +445,6 @@ function CreateInvoice() { @@ -647,10 +638,8 @@ function CreateInvoice() { + > + + + + {isMobileMenuOpen ? ( + + ) : ( + + )} +
+ + {/* Mobile Menu */} + + {isMobileMenuOpen && ( + +
+ {/* Navigation Links */} + {navItems.map((item) => ( + { + item.action(); + closeMobileMenu(); + }} + className={`w-full flex items-center px-4 py-3 rounded-lg transition-colors ${ + isActive(item.path) + ? "bg-green-900/30 text-green-400 font-medium" + : "text-white hover:bg-gray-800" + }`} + > + {item.icon} + {item.name} + + ))} + + {isConnected && + appItems.map((item) => ( + +
+ {item.icon} + {item.name} +
+ + ))} + +
+ +
+
+
+ )} +
); } -export default Navbar; +export default Navbar; \ No newline at end of file diff --git a/frontend/src/page/Home.jsx b/frontend/src/page/Home.jsx index 14b602ec..cc80256b 100644 --- a/frontend/src/page/Home.jsx +++ b/frontend/src/page/Home.jsx @@ -39,19 +39,27 @@ export default function Home() { return (
-
- {" "} -

- {" "} +
+

Welcome Back!

- + {/* Sidebar Navigation */} - + + {/* Main Content */} diff --git a/frontend/src/page/ReceivedInvoice.jsx b/frontend/src/page/ReceivedInvoice.jsx index cc4e4ec8..daade8b6 100644 --- a/frontend/src/page/ReceivedInvoice.jsx +++ b/frontend/src/page/ReceivedInvoice.jsx @@ -536,7 +536,7 @@ function ReceivedInvoice() {

From

- {drawerState.selectedInvoice.client.from} + {drawerState.selectedInvoice.user.address}

{`${drawerState.selectedInvoice.user.fname} ${drawerState.selectedInvoice.user.lname}`}

diff --git a/frontend/src/page/SentInvoice.jsx b/frontend/src/page/SentInvoice.jsx index 350b0615..f24eb7f4 100644 --- a/frontend/src/page/SentInvoice.jsx +++ b/frontend/src/page/SentInvoice.jsx @@ -217,8 +217,6 @@ function SentInvoice() { }; fetchSentInvoices(); - - console.log("invoices : ", sentInvoices); }, [walletClient, litReady]); const [drawerState, setDrawerState] = useState({ @@ -512,13 +510,11 @@ function SentInvoice() { {item.description} {item.qty.toString()} - {/* {ethers.formatUnits(item.unitPrice)} */} {item.unitPrice} {item.discount.toString()} {item.tax.toString()} - {/* {ethers.formatUnits(item.amount)} */} {item.amount} @@ -529,12 +525,10 @@ function SentInvoice() {

Fee for invoice pay : {parseFloat(ethers.formatUnits(fee))}{" "} ETH - {/* Fee for invoice pay : {ethers.formatUnits(fee)} ETH */}

{" "} Amount:{" "} - {/* {ethers.formatUnits(drawerState.selectedInvoice.amountDue)}{" "} */} {drawerState.selectedInvoice.amountDue} ETH