viem
viem is the modern TypeScript Ethereum client — smaller, type-safer, and faster than ethers. It splits its API into Public, Wallet, and Contract clients, each strict about which calls they support. The type inference for ABIs is the headline feature: the compiler knows the inputs and outputs of every function on every contract you connect to.
Public + wallet clients, typed contract calls, watch
EXAMPLE
// npm i viem
import {
createPublicClient, createWalletClient, custom, http,
parseEther, formatEther, getContract,
} from 'viem';
import { mainnet, sepolia } from 'viem/chains';
// 1) PublicClient — read-only RPC for queries, watches, history
const publicClient = createPublicClient({
chain: mainnet,
transport: http('https://eth-mainnet.example/rpc'),
});
const block = await publicClient.getBlockNumber();
console.log('latest block:', block);
// 2) WalletClient — wraps a signer (injected MetaMask, or a private key for backend)
const walletClient = createWalletClient({
chain: mainnet,
transport: custom(window.ethereum),
});
const [account] = await walletClient.requestAddresses();
// 3) Typed contract — paste the ABI as a const, the compiler does the rest
const erc20Abi = [
{
type: 'function', name: 'balanceOf', stateMutability: 'view',
inputs: [{ name: 'owner', type: 'address' }],
outputs: [{ name: 'balance', type: 'uint256' }],
},
{
type: 'function', name: 'transfer', stateMutability: 'nonpayable',
inputs: [{ name: 'to', type: 'address' }, { name: 'amount', type: 'uint256' }],
outputs: [{ name: 'success', type: 'bool' }],
},
{
type: 'event', name: 'Transfer',
inputs: [
{ indexed: true, name: 'from', type: 'address' },
{ indexed: true, name: 'to', type: 'address' },
{ indexed: false, name: 'value', type: 'uint256' },
],
},
] as const;
const usdc = getContract({
address: '0xA0b86991c6218B36c1d19D4a2e9Eb0cE3606eB48',
abi: erc20Abi,
client: { public: publicClient, wallet: walletClient },
});
// 4) Read — returns a strongly typed bigint
const bal = await usdc.read.balanceOf([account]);
console.log('balance:', formatEther(bal), 'wei (untyped) or convert by decimals');
// 5) Write — wallet prompts the user, returns a tx hash
const hash = await usdc.write.transfer(['0x000000000000000000000000000000000000dEaD', 1_000_000n]);
const receipt = await publicClient.waitForTransactionReceipt({ hash });
console.log('mined in', receipt.blockNumber);
// 6) Watch events — types follow the ABI, no string parsing
const unwatch = usdc.watchEvent.Transfer(
{ from: account }, // filter on the indexed 'from'
{
onLogs: (logs) => {
for (const l of logs) {
console.log('I sent', l.args.value, 'to', l.args.to);
}
},
},
);
// unwatch() to stop
// 7) History — get past events with a block range
const past = await publicClient.getLogs({
address: usdc.address,
event: erc20Abi.find((x) => x.type === 'event' && x.name === 'Transfer'),
fromBlock: block - 10000n,
args: { from: account },
});
// 8) ENS, gas, chain switching, batching are all first-class:
const ens = await publicClient.getEnsName({ address: account });
const gas = await publicClient.estimateGas({ to: account, value: parseEther('0.001') });
await walletClient.switchChain({ id: sepolia.id });
// 9) Multicall in one round trip — for snappy dapp UI
const [bal1, bal2] = await publicClient.multicall({
contracts: [
{ ...usdc, functionName: 'balanceOf', args: [account] },
{ ...usdc, functionName: 'balanceOf', args: ['0x0000000000000000000000000000000000000001'] },
],
});
Why it matters
Mark the ABI as `as const` so the compiler treats it as a literal type. Without the const assertion, viem cannot infer the input/output types of `read`/`write` calls, and you lose the headline feature: full type-safety from the contract to your application code.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
import { createPublicClient, http } from 'viem';
import { mainnet } from 'viem/chains';
const client = createPublicClient({ chain: mainnet, transport: http() });
const block = await client.getBlockNumber();
Try it Yourself »
Discussion
Loading…