Web3 Integration
Connect browser → wallet → RPC → contract. If you've ever built a REST frontend, the only new bits are: the "server" is a blockchain, requests are signed, and the SDK is called ethers.js.
balanceOf, and calls transfer on your ERC-20.1. The three players
- Wallet owns the key; signs tx; never exposes the key to your page.
- RPC is just JSON-over-HTTP.
eth_call,eth_sendRawTransaction,eth_getLogs,eth_subscribe. - ethers.js / viem is an SDK that wraps RPC + ABI encoding.
2. ABI — the contract's "OpenAPI"
The Application Binary Interface is a JSON description of a contract's functions and events. Produced by the Solidity compiler. Used by ethers to encode calldata and decode results.
[{
"type": "function", "name": "balanceOf", "stateMutability": "view",
"inputs": [{"name":"account","type":"address"}],
"outputs": [{"type":"uint256"}]
}]
3. ethers.js — the 6 concepts you need
| Name | Think of it as |
|---|---|
Provider | Read-only HTTP client to a node |
Signer | Provider + a key (can send tx) |
Contract | Typed wrapper over provider/signer + ABI |
Interface | Codec: encode/decode calldata & logs |
BigNumber / bigint | 256-bit ints (use JS bigint in ethers v6) |
Event filter | Predicate over indexed topics |
4. Connect-wallet flow (v6)
import { BrowserProvider, Contract, parseUnits } from "ethers";
import abi from "./MyToken.json";
async function connect() {
if (!window.ethereum) throw new Error("install MetaMask");
const provider = new BrowserProvider(window.ethereum);
await provider.send("eth_requestAccounts", []); // triggers the popup
const signer = await provider.getSigner();
const token = new Contract(TOKEN_ADDR, abi, signer);
return { signer, token };
}
async function balance(token, addr) {
const raw = await token.balanceOf(addr); // read, free
return raw; // bigint, in wei
}
async function send(token, to, amount) {
const tx = await token.transfer(to, parseUnits(amount, 18));
const r = await tx.wait(); // 1 confirmation
return r.hash;
}
5. Reads vs writes
Read (view/pure) | Write | |
|---|---|---|
| RPC method | eth_call | eth_sendRawTransaction |
| Gas? | Free (local sim) | Paid by signer |
| Signature? | No | Yes (wallet popup) |
| Returns | Value immediately | Tx hash; wait for receipt |
6. Chain selection & network switching
await window.ethereum.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: "0x89" }] // Polygon
});
Handle the "user on wrong chain" case in UI. This is the Web3 equivalent of "wrong environment" — your contract exists on chain 1 but the user is on chain 137.
7. Events in the UI
// Listen for new Transfer events involving me
const filter = token.filters.Transfer(null, myAddress);
token.on(filter, (from, to, value, ev) => {
toast(`+${formatUnits(value, 18)} MTK from ${from}`);
});
// Or query historical
const logs = await token.queryFilter(filter, -10000); // last 10k blocks
8. UX gotchas worth burning into memory
- Nonce collisions — user sends tx A, then tx B at nonce+1. If A gets dropped, B sits forever. Surface "speed up / cancel" or use
replacement_underpriced. - Reorgs — on L1, a 1-confirmation receipt can disappear. Wait 2-3 blocks for UX, 12+ for money.
- Stale state — cache
balanceOffor 12s max. After a tx, re-fetch on the next block. - Error messages — decode
errortypes from the ABI:iface.parseError(data). - Gas estimation — ethers auto-estimates, but you can force:
contract.foo.estimateGas(...). For L2s, add ~20% buffer.
9. viem and wagmi (the modern alternatives)
viem is a leaner, tree-shakeable, tree-typed successor to ethers. wagmi is React hooks on top of viem (useAccount, useReadContract, useWriteContract). For new projects in 2026, prefer wagmi + viem + RainbowKit for the connect button.
import { useReadContract } from 'wagmi';
const { data: bal } = useReadContract({
address: TOKEN_ADDR, abi, functionName: 'balanceOf', args: [address]
});
10. Project
Quiz
balanceOf call costs the user gas. What went wrong?- The token contract is misconfigured
- MetaMask added a fee
- You called it through a Signer in a way that forced a tx;
viewcalls should go through a Provider (eth_call) with no signature - You used ethers v5 instead of v6
view methods are free when invoked as eth_call. If you end up with a tx/popup, something routed your read through eth_sendTransaction.