Frontend Integration
Wallet UI kits: RainbowKit, ConnectKit, Reown AppKit (formerly Web3Modal), Dynamic, Privy.
40.1 The stack
React/Next.js UI
│ wagmi hooks (React state, caching via TanStack Query)
▼
viem (or ethers.js) ── typed ABI encoding, RPC calls, signing
│
├── Wallet (MetaMask / WalletConnect / Coinbase / Rabby) via EIP-1193 provider (EIP-6963 discovery)
└── RPC node (Alchemy, Infura, own node) ── chain
Indexers (The Graph, Ponder, Envio) for historical/event data40.2 ethers.js v6
import { ethers } from "ethers";
const abi = [
"function balanceOf(address) view returns (uint256)",
"function transfer(address to, uint256 amount) returns (bool)",
"event Transfer(address indexed from, address indexed to, uint256 value)",
];
// Read-only
const provider = new ethers.JsonRpcProvider(process.env.RPC_URL);
const token = new ethers.Contract(TOKEN_ADDRESS, abi, provider);
const bal = await token.balanceOf(user);
console.log(ethers.formatUnits(bal, 18));
// Browser wallet
const browserProvider = new ethers.BrowserProvider(window.ethereum);
const signer = await browserProvider.getSigner();
const tx = await token.connect(signer).transfer(to, ethers.parseUnits("10", 18));
const receipt = await tx.wait(); // wait for 1 confirmation
console.log("mined in block", receipt.blockNumber);
// Handling custom errors
try {
await token.connect(signer).transfer(to, huge);
} catch (e) {
console.log(e.revert?.name, e.revert?.args); // decoded if the error is in the ABI
}40.3 viem
import { createPublicClient, createWalletClient, custom, http, parseUnits, formatUnits, erc20Abi } from "viem";
import { sepolia } from "viem/chains";
const abi = [
{ type: "function", name: "balanceOf", stateMutability: "view",
inputs: [{ name: "a", type: "address" }], outputs: [{ type: "uint256" }] },
{ type: "function", name: "transfer", stateMutability: "nonpayable",
inputs: [{ name: "to", type: "address" }, { name: "amount", type: "uint256" }], outputs: [{ type: "bool" }] },
] as const; // `as const` gives full type inference
const publicClient = createPublicClient({ chain: sepolia, transport: http() });
const balance = await publicClient.readContract({ address: TOKEN, abi, functionName: "balanceOf", args: [user] });
console.log(formatUnits(balance, 18));
const walletClient = createWalletClient({ chain: sepolia, transport: custom(window.ethereum!) });
const [account] = await walletClient.requestAddresses();
// Simulate first (catches reverts before the user signs), then write
const { request } = await publicClient.simulateContract({
account, address: TOKEN, abi, functionName: "transfer", args: [to, parseUnits("10", 18)],
});
const hash = await walletClient.
…40.4 wagmi (React)
Wallet UI kits: RainbowKit, ConnectKit, Reown AppKit (formerly Web3Modal), Dynamic, Privy. Generate typed ABIs from Foundry/Hardhat artifacts with @wagmi/cli.
import { WagmiProvider, createConfig, http, useAccount, useConnect, useReadContract, useWriteContract,
useWaitForTransactionReceipt } from "wagmi";
import { injected } from "wagmi/connectors";
import { mainnet, sepolia } from "wagmi/chains";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { erc20Abi, parseUnits } from "viem";
const config = createConfig({
chains: [mainnet, sepolia],
connectors: [injected()],
transports: { [mainnet.id]: http(), [sepolia.id]: http() },
});
const queryClient = new QueryClient();
function TokenPanel({ token }: { token: `0x${string}` }) {
const { address, isConnected } = useAccount();
const { connect } = useConnect();
const { data: balance } = useReadContract({
address: token, abi: erc20Abi, functionName: "balanceOf", args: address ? [address] : undefined,
query: { enabled: !!address },
});
const { writeContract, data: hash, isPending } = useWriteContract();
const { isLoading: confirming, isSuccess } = useWaitForTransactionReceipt({ hash });
if (!isConnected) return <button onClick={() => connect({ connector: injected() })}>Connect</button>;
return (
<div>
<p>Balance: {
…40.5 UX best practices
Simulate before sending; show decoded errors.
Show pending/confirmed states and explorer links; handle chain switching (wallet_switchEthereumChain).
Handle approvals clearly (exact amounts, or permit/Permit2 signatures).
Format amounts with the token's decimals; use bigint, never JS number, for token amounts.
Use EIP-712 for readable signatures; never ask users to sign opaque hashes.