Open this lesson in your favourite AI. It'll walk you through the why, explain the demo, and quiz you on the try-it list.
Most DeFi engineers focus on contracts; the frontend is where users actually transact. The frontend signs transactions, handles MetaMask, decodes failures, sets allowances, simulates swaps. A good frontend is what makes a protocol usable. Building one well is a distinct discipline.
Frontend integration with a DeFi contract.
Use these three in order. Each builds on the one before.
In one paragraph, explain DeFi frontend integration.
Walk me through the typical user flow: connect → approve → simulate → submit.
Given a deposit flow with high failure rate (15% of tx revert), design the frontend that brings this to <2% — better error handling, pre-flight checks, better UX.
import { createWalletClient, custom, parseUnits } from "viem";
import { mainnet } from "viem/chains";
// 1. Connect to user's wallet
const walletClient = createWalletClient({
chain: mainnet,
transport: custom(window.ethereum),
});
const [account] = await walletClient.requestAddresses();
// 2. Check + set ERC-20 allowance (approve)
async function approveIfNeeded(token, spender, amount) {
const currentAllowance = await publicClient.readContract({
address: token,
abi: ERC20_ABI,
functionName: "allowance",
args: [account, spender],
});
if (currentAllowance < amount) {
const hash = await walletClient.writeContract({
address: token,
abi: ERC20_ABI,
functionName: "approve",
args: [spender, amount],
});
await publicClient.waitForTransactionReceipt({ hash });
}
}
// 3. Simulate before submitting (catches reverts)
async function simulate(contract, fn, args) {
try {
await publicClient.simulateContract({
address: contract,
abi,
functionName: fn,
args,
account,
});
return { ok: true };
} catch (err) {
return { ok: false, reason: decodeRevertReason(err) };
}
}
// 4. Submit the actual transaction
async function depositToVault(vault, amount) {
// Pre-checks
await approveIfNeeded(USDC_ADDRESS, vault.address, amount);
// Simulate (free, no gas; catches issues before user sees prompt)
const sim = await simulate(vault.address, "deposit", [amount, account]);
if (!sim.ok) {
showError(`Would revert: ${sim.reason}`);
return;
}
// Submit
const hash = await walletClient.writeContract({
address: vault.address,
abi: VAULT_ABI,
functionName: "deposit",
args: [amount, account],
});
// Wait for confirmation
const receipt = await publicClient.waitForTransactionReceipt({ hash });
// Decode events
for (const log of receipt.logs) {
// Parse Deposit event from the vault
// Show success to user
}
}
// 5. Common UX patterns
// - Show pending allowance vs deposit as ONE button if needed
// - Estimate gas before submission
// - Show what's about to happen ("You'll deposit $X and receive Y shares")
// - Handle errors gracefully ("Wallet rejected", "Insufficient balance", etc.)
// - Display tx hash + Etherscan link after submission