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 data

40.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.