A comprehensive guide to building a blockchain-based invoice system using Solidity, Next.js, and ethers.js.
Video Tutorial: Watch the full tutorial here
GitHub Repository: View the source code
Project Overview
CfxInvoice is a decentralized invoice management system built on Conflux eSpace that enables freelancers to create, manage, and track invoices on the blockchain. The system ensures transparency, security, and automatic payment processing without intermediaries.
Key Benefits
- Transparency: All invoices are stored on-chain and publicly verifiable
- Security: Cryptographic security through smart contracts
- Automation: Automatic payment processing and status updates
- Cost-Effective: Low transaction fees on Conflux eSpace
- Decentralized: No single point of failure
Architecture
The project follows a modern full-stack architecture:
┌─────────────────┐
│ Next.js UI │ ← React Components & Pages
├─────────────────┤
│ Wallet Provider│ ← MetaMask Integration
├─────────────────┤
│ Contract Layer │ ← ethers.js Integration
├─────────────────┤
│ Smart Contract │ ← Solidity on Conflux eSpace
└─────────────────┘
Tech Stack
- Frontend: Next.js 14 (App Router), React, TypeScript, Tailwind CSS
- Blockchain: Solidity, Hardhat, ethers.js v6
- Network: Conflux eSpace Testnet/Mainnet
- Wallet: MetaMask (or compatible Web3 wallet)
Smart Contract Overview
The smart contract manages invoice lifecycle with the following core functions:
Invoice Status Flow
Pending → Paid (via payment)
Pending → Overdue (via time check)
Pending → Cancelled (by freelancer)
Key Contract Functions
-
createInvoice()- Create new invoice -
payInvoice()- Process payment (with automatic fund transfer) -
cancelInvoice()- Cancel invoice (freelancer only) -
markAsOverdue()- Update status to overdue -
getInvoice()- Retrieve invoice details -
getFreelancerInvoices()- Get all invoices created by address -
getClientInvoices()- Get all invoices received by address
Frontend Implementation
1. Contract Integration Layer (lib/contract.ts)
This is the core file that handles all blockchain interactions. It abstracts the complexity of ethers.js and provides clean functions for the UI.
Provider & Contract Setup
export async function getProvider() {
if (typeof window !== 'undefined' && window.ethereum) {
return new ethers.BrowserProvider(window.ethereum);
}
const rpcUrl = process.env.NEXT_PUBLIC_RPC_URL || 'https://evmtestnet.confluxrpc.com';
return new ethers.JsonRpcProvider(rpcUrl);
}
export async function getContract(signer?: ethers.Signer) {
const provider = await getProvider();
const contractProvider = signer || provider;
if (!INVOICE_SYSTEM_ADDRESS) {
throw new Error('Contract address not set');
}
return new ethers.Contract(
INVOICE_SYSTEM_ADDRESS,
INVOICE_SYSTEM_ABI,
contractProvider
);
}
Wallet Connection & Network Switching
export async function connectWallet(): Promise<ethers.Signer | null> {
if (typeof window === 'undefined' || !window.ethereum) {
throw new Error('MetaMask or compatible wallet not found');
}
const provider = new ethers.BrowserProvider(window.ethereum);
await provider.send('eth_requestAccounts', []);
return provider.getSigner();
}
export async function switchNetwork() {
const chainId = process.env.NEXT_PUBLIC_CHAIN_ID || '71';
const hexChainId = `0x${parseInt(chainId).toString(16)}`;
try {
await window.ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: hexChainId }],
});
} catch (switchError: any) {
if (switchError.code === 4902) {
// Chain not added, add it automatically
await window.ethereum.request({
method: 'wallet_addEthereumChain',
params: [{
chainId: hexChainId,
chainName: 'Conflux eSpace Testnet',
nativeCurrency: { name: 'CFX', symbol: 'CFX', decimals: 18 },
rpcUrls: ['https://evmtestnet.confluxrpc.com'],
blockExplorerUrls: ['https://evmtestnet.confluxscan.net/'],
}],
});
}
}
}
Transaction Functions
export async function createInvoice(
client: string,
amount: bigint,
dueDate: bigint,
description: string
) {
const signer = await connectWallet();
if (!signer) throw new Error('Wallet not connected');
const contract = await getContract(signer);
const tx = await contract.createInvoice(client, amount, dueDate, description);
await tx.wait();
return tx;
}
export async function payInvoice(invoiceId: bigint, amount: bigint) {
const signer = await connectWallet();
if (!signer) throw new Error('Wallet not connected');
const contract = await getContract(signer);
const tx = await contract.payInvoice(invoiceId, { value: amount });
await tx.wait();
return tx;
}
Data Fetching Functions
export async function fetchInvoice(invoiceId: bigint): Promise<Invoice> {
const contract = await getContract();
const data = await contract.getInvoice(invoiceId);
return parseInvoice(data);
}
export async function fetchFreelancerInvoices(address: string): Promise<bigint[]> {
const contract = await getContract();
const invoiceIds = await contract.getFreelancerInvoices(address);
return invoiceIds.map((id: any) => BigInt(id.toString()));
}
2. Wallet Provider Context (components/WalletProvider.tsx)
A React Context that manages wallet state globally across the application.
Context Setup
interface WalletContextType {
account: string | null;
connectWallet: () => Promise<void>;
disconnectWallet: () => void;
loading: boolean;
}
const WalletContext = createContext<WalletContextType>({
account: null,
connectWallet: async () => {},
disconnectWallet: () => {},
loading: false,
});
Auto-Connection & Event Listeners
useEffect(() => {
checkWalletConnection();
if (typeof window !== 'undefined' && window.ethereum) {
window.ethereum.on('accountsChanged', handleAccountsChanged);
window.ethereum.on('chainChanged', handleChainChanged);
return () => {
window.ethereum?.removeListener('accountsChanged', handleAccountsChanged);
window.ethereum?.removeListener('chainChanged', handleChainChanged);
};
}
}, []);
const handleAccountsChanged = (accounts: string[]) => {
if (accounts.length === 0) {
setAccount(null);
} else {
setAccount(accounts[0]);
}
};
Connection Handler
const handleConnectWallet = async () => {
try {
setLoading(true);
await switchNetwork();
const signer = await connectWallet();
const address = await signer.getAddress();
setAccount(address);
} catch (error: any) {
console.error('Error connecting wallet:', error);
alert(error.message || 'Failed to connect wallet');
} finally {
setLoading(false);
}
};
3. Create Invoice Page (app/create/page.tsx)
The form that allows freelancers to create new invoices.
Form State & Validation
const [formData, setFormData] = useState({
client: '',
amount: '',
dueDate: '',
description: '',
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!account) {
alert('Please connect your wallet first');
return;
}
if (!isValidAddress(formData.client)) {
alert('Invalid client address');
return;
}
if (formData.client.toLowerCase() === account.toLowerCase()) {
alert('Cannot create invoice for yourself');
return;
}
try {
setLoading(true);
const amount = parseCFXToWei(formData.amount);
const dueDate = Math.floor(new Date(formData.dueDate).getTime() / 1000);
const tx = await createInvoiceTx(
formData.client,
amount,
BigInt(dueDate),
formData.description
);
alert('Invoice created successfully!');
router.push('/invoices');
} catch (error: any) {
alert(error.message || 'Failed to create invoice');
} finally {
setLoading(false);
}
};
4. Invoice Listing Page (app/invoices/page.tsx)
Displays all invoices with filtering capabilities.
Data Fetching Logic
const loadInvoices = async () => {
if (!account) return;
try {
setLoading(true);
const invoiceIds = new Set<bigint>();
if (filter === 'all' || filter === 'sent') {
const freelancerIds = await fetchFreelancerInvoices(account);
freelancerIds.forEach((id) => invoiceIds.add(id));
}
if (filter === 'all' || filter === 'received') {
const clientIds = await fetchClientInvoices(account);
clientIds.forEach((id) => invoiceIds.add(id));
}
const invoicePromises = Array.from(invoiceIds).map((id) => fetchInvoice(id));
const fetchedInvoices = await Promise.all(invoicePromises);
// Sort by creation date, newest first
fetchedInvoices.sort((a, b) => {
const dateA = Number(a.createdAt);
const dateB = Number(b.createdAt);
return dateB - dateA;
});
setInvoices(fetchedInvoices);
} catch (error) {
console.error('Error loading invoices:', error);
} finally {
setLoading(false);
}
};
5. Invoice Card Component (components/InvoiceCard.tsx)
Displays individual invoice with role-based actions.
Role Detection & Action Permissions
const isFreelancer = account?.toLowerCase() === invoice.freelancer.toLowerCase();
const isClient = account?.toLowerCase() === invoice.client.toLowerCase();
const canPay = isClient && (invoice.status === InvoiceStatus.Pending || invoice.status === InvoiceStatus.Overdue);
const canCancel = isFreelancer && (invoice.status === InvoiceStatus.Pending || invoice.status === InvoiceStatus.Overdue);
Payment Handler
const handlePay = async () => {
if (!account) return;
try {
setLoading(true);
await payInvoice(invoice.invoiceId, invoice.amount);
alert('Invoice paid successfully!');
onUpdate();
} catch (error: any) {
console.error('Error paying invoice:', error);
alert(error.message || 'Failed to pay invoice');
} finally {
setLoading(false);
}
};
Status Banner Display
const getStatusBanner = () => {
switch (invoice.status) {
case InvoiceStatus.Paid:
return {
bgColor: 'bg-green-50',
borderColor: 'border-green-300',
icon: '✓',
title: 'PAID',
message: `Payment received on ${formatDateTime(invoice.paidAt)}.`,
};
case InvoiceStatus.Overdue:
return {
bgColor: 'bg-red-50',
borderColor: 'border-red-300',
icon: '⚠️',
title: 'OVERDUE',
message: `Due date was ${formatDateTime(invoice.dueDate)}.`,
};
// ... other statuses
}
};
6. Utility Functions (lib/utils.ts)
Helper functions for formatting and validation.
Currency & Address Formatting
export function formatCFXFromWei(wei: bigint): string {
const cfx = Number(wei) / 1e18;
return cfx.toFixed(4);
}
export function formatAddress(address: string): string {
if (!address) return '';
return `${address.slice(0, 6)}...${address.slice(-4)}`;
}
export function parseCFXToWei(cfx: string): bigint {
const amount = parseFloat(cfx);
if (isNaN(amount)) return BigInt(0);
return BigInt(Math.floor(amount * 1e18));
}
Date Formatting
export function formatDateTime(timestamp: bigint | number): string {
const date = new Date(Number(timestamp) * 1000);
return date.toLocaleString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
}
Address Validation
export function isValidAddress(address: string): boolean {
return /^0x[a-fA-F0-9]{40}$/.test(address);
}
Key Features
1. Automatic Payment Processing
When a client pays an invoice, funds are automatically transferred to the freelancer’s wallet via the smart contract’s payInvoice() function.
2. Role-Based UI
The interface adapts based on whether the user is the freelancer (invoice creator) or client (payer), showing relevant actions and information.
3. Real-Time Status Updates
Invoice status updates are reflected immediately after blockchain transactions are confirmed.
4. Filtering & Organization
Users can filter invoices by:
- All invoices
- Sent invoices (created by me)
- Received invoices (sent to me)
5. Network Auto-Switching
The app automatically switches or adds the Conflux eSpace network to MetaMask if not already configured.
6. Responsive Design
Modern, mobile-friendly UI built with Tailwind CSS.
Setup & Deployment
Prerequisites
- Node.js (v16 or higher)
- MetaMask or compatible Web3 wallet
- Conflux eSpace testnet CFX (from faucet)
Installation Steps
- Clone the repository
git clone https://github.com/Vikash-8090-Yadav/CfxInvoice
cd CfxInvoice
- Install dependencies
npm install
- Configure environment variables
Create .env.local:
NEXT_PUBLIC_CONTRACT_ADDRESS=your_deployed_contract_address
NEXT_PUBLIC_RPC_URL=https://evmtestnet.confluxrpc.com
NEXT_PUBLIC_CHAIN_ID=71
- Deploy smart contract
npm run compile
npm run deploy:testnet
- Run frontend
npm run dev
Network Configuration
Conflux eSpace Testnet:
- RPC URL:
https://evmtestnet.confluxrpc.com - Chain ID:
71 - Explorer: https://evmtestnet.confluxscan.net/
- Faucet: https://faucet.confluxnetwork.org/
Resources:
- Video Tutorial - Step-by-step walkthrough
- GitHub Repository - Full source code
- Conflux Documentation
- ethers.js Documentation
Built with
using Conflux eSpace