diff --git a/contracts/test/Chainvoice.t.sol b/contracts/test/Chainvoice.t.sol index ecabfd5b..e89de971 100644 --- a/contracts/test/Chainvoice.t.sol +++ b/contracts/test/Chainvoice.t.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: Unlicense +/* SPDX-License-Identifier: Unlicense */ pragma solidity ^0.8.13; import {Test} from "forge-std/Test.sol"; @@ -13,8 +13,8 @@ contract ChainvoiceTest is Test { function setUp() public { chainvoice = new Chainvoice(); - vm.deal(alice, 10 ether); - vm.deal(bob, 10 ether); + vm.deal(alice, 100 ether); + vm.deal(bob, 100 ether); } /* ------------------------------------------------------------ */ @@ -113,4 +113,126 @@ contract ChainvoiceTest is Test { vm.prank(bob); chainvoice.payInvoice{value: 1 ether}(0); } -} \ No newline at end of file + + /* ------------------------------------------------------------ */ + /* BATCH OPERATIONS */ + /* ------------------------------------------------------------ */ + + function testBatchTooLarge() public { + uint256 batchSize = 51; + address[] memory tos = new address[](batchSize); + uint256[] memory amounts = new uint256[](batchSize); + string[] memory payloads = new string[](batchSize); + string[] memory hashes = new string[](batchSize); + + for (uint256 i = 0; i < batchSize; i++) { + tos[i] = bob; + amounts[i] = 1 ether; + payloads[i] = ""; + hashes[i] = ""; + } + + vm.prank(alice); + vm.expectRevert(Chainvoice.InvalidBatchSize.selector); + chainvoice.createInvoicesBatch(tos, amounts, address(0), payloads, hashes); + } + + function testCreateInvoicesBatch() public { + uint256 batchSize = 3; + address[] memory tos = new address[](batchSize); + uint256[] memory amounts = new uint256[](batchSize); + string[] memory payloads = new string[](batchSize); + string[] memory hashes = new string[](batchSize); + + for (uint256 i = 0; i < batchSize; i++) { + tos[i] = bob; + amounts[i] = 1 ether; + payloads[i] = "batchData"; + hashes[i] = "batchHash"; + } + + vm.prank(alice); + chainvoice.createInvoicesBatch(tos, amounts, address(0), payloads, hashes); + + Chainvoice.InvoiceDetails[] memory sent = chainvoice.getSentInvoices(alice); + Chainvoice.InvoiceDetails[] memory received = chainvoice.getReceivedInvoices(bob); + + assertEq(sent.length, 3); + assertEq(received.length, 3); + assertEq(sent[2].amountDue, 1 ether); + } + + function testPayInvoicesBatch() public { + vm.startPrank(alice); + chainvoice.createInvoice(bob, 1 ether, address(0), "", ""); + chainvoice.createInvoice(bob, 2 ether, address(0), "", ""); + vm.stopPrank(); + + uint256 fee = chainvoice.fee(); + uint256 totalFee = fee * 2; + uint256 totalPrincipal = 3 ether; + + uint256[] memory ids = new uint256[](2); + ids[0] = 0; + ids[1] = 1; + + uint256 bobStart = bob.balance; + uint256 aliceStart = alice.balance; + + vm.prank(bob); + chainvoice.payInvoicesBatch{value: totalPrincipal + totalFee}(ids); + + Chainvoice.InvoiceDetails memory inv0 = chainvoice.getInvoice(0); + Chainvoice.InvoiceDetails memory inv1 = chainvoice.getInvoice(1); + + assertTrue(inv0.isPaid); + assertTrue(inv1.isPaid); + + assertEq(chainvoice.accumulatedFees(), totalFee); + assertEq(bob.balance, bobStart - (totalPrincipal + totalFee)); + assertEq(alice.balance, aliceStart + totalPrincipal); + } + + /* ------------------------------------------------------------ */ + /* FUZZ TESTING */ + /* ------------------------------------------------------------ */ + + function testFuzz_CreateInvoice(address recipient, uint256 amount) public { + vm.assume(recipient != address(0)); + vm.assume(recipient != alice); + vm.assume(amount < 1000000 ether); + + vm.prank(alice); + chainvoice.createInvoice(recipient, amount, address(0), "fuzz", "hash"); + + Chainvoice.InvoiceDetails[] memory sent = chainvoice.getSentInvoices(alice); + Chainvoice.InvoiceDetails memory latest = sent[sent.length - 1]; + + assertEq(latest.to, recipient); + assertEq(latest.amountDue, amount); + } + + /* ------------------------------------------------------------ */ + /* ADMIN / FEES */ + /* ------------------------------------------------------------ */ + + function testWithdrawFees() public { + address treasury = address(0x999); + + chainvoice.setTreasuryAddress(treasury); + + vm.prank(alice); + chainvoice.createInvoice(bob, 1 ether, address(0), "", ""); + + uint256 fee = chainvoice.fee(); + vm.prank(bob); + chainvoice.payInvoice{value: 1 ether + fee}(0); + + assertEq(chainvoice.accumulatedFees(), fee); + + chainvoice.withdrawFees(); + + assertEq(chainvoice.accumulatedFees(), 0); + assertEq(treasury.balance, fee); + } +} diff --git a/frontend/src/page/ReceivedInvoice.jsx b/frontend/src/page/ReceivedInvoice.jsx index 1bac242f..930bff9b 100644 --- a/frontend/src/page/ReceivedInvoice.jsx +++ b/frontend/src/page/ReceivedInvoice.jsx @@ -234,22 +234,40 @@ function ReceivedInvoice() { }; // UNIFORM BALANCE CHECK +// UNIFORM BALANCE CHECK (includes gas estimation) const checkBalance = async (tokenAddress, amount, symbol, signer) => { const userAddress = await signer.getAddress(); + const provider = signer.provider; + + // Estimate gas price + const feeData = await provider.getFeeData(); + const gasPrice = feeData.gasPrice || feeData.maxFeePerGas; + + if (!gasPrice) { + throw new Error("Unable to fetch gas price"); + } + + // Rough gas limit estimation (safe buffer) + const estimatedGasLimit = BigInt(300000); // adjust if needed + const estimatedGasCost = gasPrice * estimatedGasLimit; if (tokenAddress === ethers.ZeroAddress) { - const balance = await signer.provider.getBalance(userAddress); + const balance = await provider.getBalance(userAddress); + + const invoiceAmount = ethers.parseUnits(amount.toString(), 18); const totalRequired = - ethers.parseUnits(amount.toString(), 18) + BigInt(fee); + invoiceAmount + BigInt(fee) + estimatedGasCost; if (balance < totalRequired) { const requiredEth = ethers.formatEther(totalRequired); const availableEth = ethers.formatEther(balance); + throw new Error( - `Insufficient ETH balance. Required: ${requiredEth} ETH, Available: ${availableEth} ETH` + `Insufficient ETH balance. Required (including gas): ${requiredEth} ETH, Available: ${availableEth} ETH` ); } } else { + // ERC20 Balance Check const tokenContract = new Contract(tokenAddress, ERC20_ABI, signer); const balance = await tokenContract.balanceOf(userAddress); const decimals = await tokenContract.decimals(); @@ -262,17 +280,22 @@ function ReceivedInvoice() { ); } - const ethBalance = await signer.provider.getBalance(userAddress); - if (ethBalance < BigInt(fee)) { - const requiredEthFee = ethers.formatEther(fee); + // ETH required for fee + gas + const ethBalance = await provider.getBalance(userAddress); + const totalEthRequired = BigInt(fee) + estimatedGasCost; + + if (ethBalance < totalEthRequired) { + const requiredEth = ethers.formatEther(totalEthRequired); const availableEth = ethers.formatEther(ethBalance); + throw new Error( - `Insufficient ETH for fees. Required: ${requiredEthFee} ETH, Available: ${availableEth} ETH` + `Insufficient ETH for fees and gas. Required: ${requiredEth} ETH, Available: ${availableEth} ETH` ); } } }; + const getGroupedInvoices = () => { const grouped = new Map(); receivedInvoices.forEach((invoice) => {