The frontend stack
Most DeFi frontends use a combination of:
- ethers.js or viem — Low-level Ethereum interaction library
- wagmi — React hooks for wallet connection and contract interaction
- RainbowKit or ConnectKit — Pre-built wallet connection UI
The ABI
The ABI (Application Binary Interface) is the contract's public interface description — a JSON array that tells ethers.js how to encode function calls and decode responses. You get it from Hardhat's compilation output in artifacts/.
javascript
// Import ABI from compilation artifacts
import MyTokenABI from './artifacts/contracts/MyToken.sol/MyToken.json';
const abi = MyTokenABI.abi;Reading from a contract (ethers.js)
javascript
import { ethers } from 'ethers';
const CONTRACT_ADDRESS = '0x...';
// Read-only provider — no wallet needed
const provider = new ethers.JsonRpcProvider('https://eth-mainnet.g.alchemy.com/v2/KEY');
const contract = new ethers.Contract(CONTRACT_ADDRESS, abi, provider);
// Call a view function
const balance = await contract.balanceOf('0x...');
console.log(ethers.formatEther(balance)); // converts from wei to ETH stringWriting to a contract (ethers.js)
javascript
// Connect to user's wallet
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const contract = new ethers.Contract(CONTRACT_ADDRESS, abi, signer);
// Call a state-changing function
const tx = await contract.transfer('0x...', ethers.parseEther('1.0'));
await tx.wait(); // wait for the transaction to be mined
console.log('Transfer confirmed in block', tx.blockNumber);Using wagmi in React
javascript
// Read contract state
import { useReadContract } from 'wagmi';
function TokenBalance({ address }) {
const { data: balance } = useReadContract({
address: CONTRACT_ADDRESS,
abi,
functionName: 'balanceOf',
args: [address],
});
return <div>{balance ? formatEther(balance) : '...'} tokens</div>;
}
// Write to contract
import { useWriteContract } from 'wagmi';
function TransferButton() {
const { writeContract } = useWriteContract();
return (
<button onClick={() =>
writeContract({
address: CONTRACT_ADDRESS,
abi,
functionName: 'transfer',
args: [recipient, parseEther('1.0')],
})
}>
Transfer 1 Token
</button>
);
}Listening for events
javascript
// Listen for Transfer events in real-time
contract.on('Transfer', (from, to, value, event) => {
console.log(from + ' → ' + to + ': ' + ethers.formatEther(value) + ' tokens');
console.log('Transaction hash:', event.transactionHash);
});
// Clean up listener
contract.off('Transfer', listener);Handling errors
Always handle contract reverts gracefully in your frontend. ethers.js wraps revert reasons in the error message.
javascript
try {
const tx = await contract.withdraw();
await tx.wait();
} catch (err) {
if (err.code === 'ACTION_REJECTED') {
// User rejected the transaction in their wallet
} else if (err.reason) {
// Contract revert: err.reason contains the revert message
console.error('Contract error:', err.reason);
}
}