Building a Dynamic Payment Splitter on Conflux eSpace: A Technical Deep Dive
Introduction
In this technical blog, we’ll explore the architecture, implementation, and design decisions behind building a dynamic payment splitter smart contract on Conflux eSpace with a modern Next.js frontend. This project demonstrates how to build a production-ready DeFi application with a gas-efficient smart contract and an intuitive user interface.
Video Link: https://youtu.be/Q_DGS1vqqdE?si=K3c8EQUBdqiJO9vW
GitHub Link: https://github.com/Vikash-8090-Yadav/Cfxpaymentsplit
Project Overview
The Conflux Payment Splitter is a decentralized application (dApp) that enables proportional distribution of CFX (Conflux’s native token) payments among multiple payees based on their assigned shares. Unlike traditional payment splitters that require payee configuration at deployment time, this contract supports dynamic payee management after deployment, making it more flexible for real-world use cases.
Key Capabilities
- Dynamic Payee Management: Add payees and assign shares after contract deployment
- Proportional Distribution: Automatic calculation of each payee’s share based on total shares
- Pull Payment Model: Gas-efficient design where payees claim their payments on-demand
- Full Transparency: All contract state and transactions are publicly verifiable on-chain
- Web3 Integration: Seamless wallet connection and interaction via MetaMask/Fluent Wallet
Architecture & Technical Stack
Smart Contract Layer
Technology Stack:
- Solidity 0.8.19: Latest stable version with modern features and enhanced security
- Hardhat: Development environment for compilation, testing, and deployment
- Ethers.js v6: Interaction with the blockchain
Network:
-
Conflux eSpace Testnet: EVM-compatible testnet for development and testing
- RPC:
https://evmtestnet.confluxrpc.com - Chain ID:
71 - Block Explorer:
https://evmtestnet.confluxscan.net
- RPC:
Frontend Layer
Technology Stack:
- Next.js 14: React framework with App Router for modern web development
- TypeScript: Type-safe development
- Tailwind CSS: Utility-first CSS framework for responsive design
- Ethers.js v6: Blockchain interaction library
- React Hooks: Modern React patterns for state management
Project Structure
Cfxpaymentsplit/
├── SmartContract/
│ ├── contracts/
│ │ └── ConfluxPaymentSplitter.sol # Main smart contract
│ ├── scripts/
│ │ ├── deploy.js # Deployment script
│ │ ├── verify.js # Contract verification
│ │ └── copy-abi.js # ABI extraction
│ ├── test/
│ │ └── ConfluxPaymentSplitter.test.js # Comprehensive test suite
│ └── hardhat.config.js # Hardhat configuration
│
└── frontend/
├── app/
│ ├── page.tsx # Main application page
│ ├── layout.tsx # Root layout
│ └── globals.css # Global styles
├── components/
│ ├── WalletButton.tsx # Wallet connection component
│ ├── ContractInfo.tsx # Contract state display
│ └── PayeeList.tsx # Payee management UI
├── lib/
│ └── web3.ts # Web3 service layer
└── contracts/
└── abi.json # Contract ABI
Smart Contract Deep Dive
Contract Architecture
The ConfluxPaymentSplitter contract is designed with simplicity and gas efficiency in mind. Let’s break down its core components:
State Variables
address public owner; // Contract administrator
uint256 private _totalShares; // Sum of all payee shares
uint256 private _totalReleased; // Total amount released to payees
mapping(address => uint256) private _shares; // Shares per payee
mapping(address => uint256) private _released; // Released amount per payee
address[] private _payees; // List of all payees
Key Design Decisions
1. Pull Payment Model vs Push Payment Model
The contract uses a pull payment model where payees actively claim their payments instead of the contract automatically distributing funds. This design choice offers several advantages:
- Gas Efficiency: Only active payees pay gas fees to claim their payments
- No Failed Transactions: Payees don’t need to be EOA-compatible; contract addresses can also claim
- User Control: Payees decide when to withdraw their funds
- Avoids DoS Risks: No risk of transaction failures blocking the entire payment process
2. Dynamic Payee Addition
Unlike OpenZeppelin’s PaymentSplitter which requires payees at construction time, this contract allows adding payees dynamically:
function addPayee(address account, uint256 shares_) external onlyOwner {
require(account != address(0), "zero address");
require(shares_ > 0, "shares = 0");
require(_shares[account] == 0, "already added");
_payees.push(account);
_shares[account] = shares_;
_totalShares += shares_;
emit PayeeAdded(account, shares_);
}
Benefits:
- More flexible for changing business requirements
- Can add payees as needed without redeploying
- Owner maintains control over payee management
3. Proportional Payment Calculation
The contract calculates pending payments using a proportional formula:
function _pendingPayment(
address account,
uint256 totalReceived,
uint256 alreadyReleased
) private view returns (uint256) {
return (totalReceived * _shares[account]) / _totalShares - alreadyReleased;
}
This ensures that each payee receives exactly their proportional share based on:
- Total funds received by the contract
- Payee’s share percentage
- Amount already released to the payee
4. Dual Deposit Methods
The contract supports two methods for receiving funds:
// Method 1: Direct transfer via receive() fallback
receive() external payable {
emit PaymentReceived(msg.sender, msg.value);
}
// Method 2: Explicit deposit() function
function deposit() external payable {
require(msg.value > 0, "No CFX sent");
emit PaymentReceived(msg.sender, msg.value);
}
This flexibility allows both simple transfers and explicit function calls.
Security Features
-
Access Control: Only the owner can add payees via the
onlyOwnermodifier - Input Validation: All inputs are validated (zero address, zero shares, duplicate payees)
-
Safe Transfers: Uses
callfor native token transfers with error handling - No Reentrancy: Simple state updates prevent reentrancy attacks
Events for Transparency
The contract emits comprehensive events for off-chain monitoring:
event PayeeAdded(address account, uint256 shares);
event PaymentReleased(address to, uint256 amount);
event PaymentReceived(address from, uint256 amount);
Frontend Implementation
Web3 Service Layer
The frontend uses a centralized Web3Service class to handle all blockchain interactions. This abstraction provides several benefits:
- Separation of Concerns: Business logic separated from UI components
- Reusability: Service methods can be used across multiple components
- Error Handling: Centralized error handling and validation
- Type Safety: TypeScript interfaces for contract state
Key Methods
class Web3Service {
// Wallet connection and network management
async connectWallet(): Promise<string>
async switchNetwork(): Promise<void>
// Contract state queries
async getContractState(): Promise<ContractState>
async getPending(account: string): Promise<string>
async getAllPayees(): Promise<Payee[]>
// Contract interactions
async addPayee(account: string, shares: string): Promise<TransactionResponse>
async deposit(amount: string): Promise<TransactionResponse>
async release(account: string): Promise<TransactionResponse>
}
Network Management
The service automatically handles Conflux eSpace Testnet connection:
async switchNetwork() {
try {
await window.ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: '0x47' }], // Chain ID 71 in hex
});
} catch (switchError) {
// Automatically add network if not present
if (switchError.code === 4902) {
await window.ethereum.request({
method: 'wallet_addEthereumChain',
params: [{
chainId: '0x47',
chainName: 'Conflux eSpace Testnet',
nativeCurrency: { name: 'CFX', symbol: 'CFX', decimals: 18 },
rpcUrls: ['https://evmtestnet.confluxrpc.com'],
blockExplorerUrls: ['https://evmtestnet.confluxscan.net'],
}],
});
}
}
}
Features:
- Auto-reconnection on page load
- Address formatting for display
- Loading states during connection
3. ContractInfo Component
Displays real-time contract state:
- Contract address
- Owner address
- Total shares
- Total released amount
- Current contract balance
4. PayeeList Component
Manages the payee list display:
- Fetches all payees from the contract
- Displays shares and pending amounts
- Allows payees to release their payments
- Refresh functionality for real-time updates
User Experience Enhancements
- Responsive Design: Mobile-first approach with Tailwind CSS
- Visual Feedback: Loading states, success/error messages
- Address Formatting: Truncated addresses for readability
- Transaction Confirmation: User-friendly alerts after transactions
- Error Messages: Descriptive error messages for failed operations