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/18] - --- .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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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

{" "}