Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 126 additions & 4 deletions contracts/test/Chainvoice.t.sol
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-License-Identifier: Unlicense
/* SPDX-License-Identifier: Unlicense */
pragma solidity ^0.8.13;

import {Test} from "forge-std/Test.sol";
Expand All @@ -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);
}

/* ------------------------------------------------------------ */
Expand Down Expand Up @@ -113,4 +113,126 @@ contract ChainvoiceTest is Test {
vm.prank(bob);
chainvoice.payInvoice{value: 1 ether}(0);
}
}

/* ------------------------------------------------------------ */
/* 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);
}
}
37 changes: 30 additions & 7 deletions frontend/src/page/ReceivedInvoice.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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) => {
Expand Down