viem
TypeScript interface for Ethereum and EVM-compatible chains. Provides low-level primitives for blockchain interaction with end-to-end type safety, minimal abstraction overhead, and tree-shakeable modules. The default client library for wagmi.
What You Probably Got Wrong
AI agents trained before 2024 confuse viem with ethers.js patterns. These are the critical differences.
- viem uses native
bigint, notBigNumber-- There is noBigNumberclass. All wei values arebigint. Use100nliteral syntax orBigInt("100"). Never useethers.BigNumber.from(). Addresstype is`0x${string}`, not plainstring-- viem enforces checksummed0x-prefixed addresses at the type level. UsegetAddress()to normalize.- Clients are not providers -- viem has
PublicClient(reads),WalletClient(writes), andTestClient(anvil). There is noProviderorSignerconcept. - Transports replace connection URLs -- You pass
http(),webSocket(), orcustom()transports to clients, not raw RPC URLs as strings. - ABIs must be
as const-- For type inference to work, ABI arrays must useas constassertion. Without it, you lose all parameter/return type safety. simulateContractbeforewriteContract-- viem separates simulation from execution. Always simulate first to catch reverts before sending a transaction.- No default chain -- Every client requires an explicit
chainparameter. There is no implicit mainnet default. parseEtherreturnsbigint, not a wrapper --parseEther("1.0")returns1000000000000000000n, a rawbigint.
Quick Start
Installation
npm install viem
Create a Public Client (read-only)
import { createPublicClient, http } from "viem";
import { mainnet } from "viem/chains";
const client = createPublicClient({
chain: mainnet,
transport: http(process.env.RPC_URL),
});
const blockNumber = await client.getBlockNumber();
Create a Wallet Client (write)
import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { mainnet } from "viem/chains";
const account = privateKeyToAccount(
process.env.PRIVATE_KEY as `0x${string}`
);
const walletClient = createWalletClient({
account,
chain: mainnet,
transport: http(process.env.RPC_URL),
});
Core Concepts
Transports
Transports define how the client communicates with the RPC node.
import { http, webSocket, fallback } from "viem";
const httpTransport = http("https://eth.llamarpc.com");
const wsTransport = webSocket("wss://eth.llamarpc.com");
// Fallback tries transports in order
const resilientTransport = fallback([
http("https://eth.llamarpc.com"),
http("https://rpc.ankr.com/eth"),
]);
Chains
Viem ships chain definitions for 50+ networks. Each chain object contains chain ID, RPC URLs, block explorer, native currency, and ENS registry address.
import { mainnet, arbitrum, optimism, base, polygon } from "viem/chains";
// Chain objects carry all config
mainnet.id; // 1
arbitrum.id; // 42161
base.id; // 8453
Client Types
| Client | Purpose | Created With |
|---|---|---|
PublicClient |
Read blockchain state, call view functions, watch events | createPublicClient |
WalletClient |
Sign and send transactions, deploy contracts | createWalletClient |
TestClient |
Anvil/Hardhat node control (mine, setBalance, impersonate) | createTestClient |
Account Types
// Local account -- private key in memory, signs locally
import { privateKeyToAccount } from "viem/accounts";
const localAccount = privateKeyToAccount("0x...");
// JSON-RPC account -- delegates signing to the wallet (browser, hardware)
import { createWalletClient, custom } from "viem";
const walletClient = createWalletClient({
chain: mainnet,
transport: custom(window.ethereum!),
});
const [address] = await walletClient.getAddresses();
Reading Blockchain State
Basic Reads
import { createPublicClient, http, formatEther } from "viem";
import { mainnet } from "viem/chains";
const client = createPublicClient({
chain: mainnet,
transport: http(process.env.RPC_URL),
});
const balance = await client.getBalance({
address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
});
console.log(`${formatEther(balance)} ETH`);
const block = await client.getBlock({ blockTag: "latest" });
const txCount = await client.getTransactionCount({
address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
});
Read Contract (view/pure functions)
const abi = [
{
name: "balanceOf",
type: "function",
stateMutability: "view",
inputs: [{ name: "account", type: "address" }],
outputs: [{ name: "", type: "uint256" }],
},
] as const;
const balance = await client.readContract({
address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
abi,
functionName: "balanceOf",
args: ["0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"],
});
// balance is typed as bigint
Multicall (batch reads in a single RPC call)
const erc20Abi = [
{
name: "balanceOf",
type: "function",
stateMutability: "view",
inputs: [{ name: "account", type: "address" }],
outputs: [{ name: "", type: "uint256" }],
},
{
name: "decimals",
type: "function",
stateMutability: "view",
inputs: [],
outputs: [{ name: "", type: "uint8" }],
},
] as const;
const results = await client.multicall({
contracts: [
{
address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
abi: erc20Abi,
functionName: "balanceOf",
args: ["0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"],
},
{
address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
abi: erc20Abi,
functionName: "decimals",
},
],
});
// results[0].result is bigint (balance)
// results[1].result is number (decimals)
// Each result has { result, status: "success" | "failure" }
Writing Transactions
Send Native ETH
import { parseEther } from "viem";
const hash = await walletClient.sendTransaction({
to: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
value: parseEther("0.1"),
});
Simulate Then Write Contract
const abi = [
{
name: "transfer",
type: "function",
stateMutability: "nonpayable",
inputs: [
{ name: "to", type: "address" },
{ name: "amount", type: "uint256" },
],
outputs: [{ name: "", type: "bool" }],
},
] as const;
// Simulate first to catch reverts without spending gas
const { request } = await client.simulateContract({
address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
abi,
functionName: "transfer",
args: [
"0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
1000000n, // 1 USDC (6 decimals)
],
account: walletClient.account,
});
// Execute -- pass the validated request directly
const hash = await walletClient.writeContract(request);
Wait for Transaction Receipt
const receipt = await client.waitForTransactionReceipt({ hash });
if (receipt.status === "reverted") {
throw new Error(`Transaction reverted in block ${receipt.blockNumber}`);
}
console.log(`Confirmed in block ${receipt.blockNumber}`);
console.log(`Gas used: ${receipt.gasUsed}`);
Contract Interaction
Typed ABIs with as const
The as const assertion is what enables viem's type inference. Without it, TypeScript widens the ABI to generic types and you lose autocomplete on function names, argument types, and return types.
// Correct -- full type inference
const abi = [
{
name: "approve",
type: "function",
stateMutability: "nonpayable",
inputs: [
{ name: "spender", type: "address" },
{ name: "amount", type: "uint256" },
],
outputs: [{ name: "", type: "bool" }],
},
{
name: "Transfer",
type: "event",
inputs: [
{ name: "from", type: "address", indexed: true },
{ name: "to", type: "address", indexed: true },
{ name: "value", type: "uint256", indexed: false },
],
},
] as const;
// functionName autocompletes to "approve"
// args is typed as [address, bigint]
// return is typed as boolean
Watching Events
const unwatch = client.watchEvent({
address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
event: {
name: "Transfer",
type: "event",
inputs: [
{ name: "from", type: "address", indexed: true },
{ name: "to", type: "address", indexed: true },
{ name: "value", type: "uint256", indexed: false },
],
},
onLogs: (logs) => {
for (const log of logs) {
console.log(`${log.args.from} -> ${log.args.to}: ${log.args.value}`);
}
},
});
// Stop watching
unwatch();
Getting Past Logs
import { parseAbiItem } from "viem";
const logs = await client.getLogs({
address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
event: parseAbiItem(
"event Transfer(address indexed from, address indexed to, uint256 value)"
),
fromBlock: 18000000n,
toBlock: 18001000n,
});
for (const log of logs) {
console.log(log.args.from, log.args.to, log.args.value);
}
Utility Functions
Value Conversion
import {
parseEther,
formatEther,
parseUnits,
formatUnits,
parseGwei,
formatGwei,
} from "viem";
// ETH conversion (18 decimals)
parseEther("1.0"); // 1000000000000000000n
formatEther(1000000000000000000n); // "1"
// Custom decimals (e.g., USDC has 6)
parseUnits("100", 6); // 100000000n
formatUnits(100000000n, 6); // "100"
// Gas price
parseGwei("20"); // 20000000000n
formatGwei(20000000000n); // "20"
ABI Encoding/Decoding
import {
encodeFunctionData,
decodeFunctionData,
encodeAbiParameters,
decodeAbiParameters,
parseAbi,
} from "viem";
const abi = parseAbi([
"function transfer(address to, uint256 amount) returns (bool)",
]);
// Encode calldata for a raw transaction
const data = encodeFunctionData({
abi,
functionName: "transfer",
args: ["0x70997970C51812dc3A010C7d01b50e0d17dc79C8", 1000000n],
});
// Decode calldata back to function name and args
const { functionName, args } = decodeFunctionData({ abi, data });
// Encode raw ABI parameters (for constructors, etc.)
const encoded = encodeAbiParameters(
[
{ name: "x", type: "uint256" },
{ name: "y", type: "address" },
],
[123n, "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"]
);
Address Utilities
import { getAddress, isAddress, isAddressEqual } from "viem";
// Normalize to checksummed address
getAddress("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48");
// "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
// Validate
isAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); // true
isAddress("not-an-address"); // false
// Case-insensitive comparison
isAddressEqual(
"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
); // true
Hashing
import { keccak256, toBytes, toHex } from "viem";
keccak256(toBytes("hello"));
// 0x1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8
toHex(420); // "0x1a4"
toBytes("0x1a4"); // Uint8Array
Chain Configuration
Built-in Chains
import {
mainnet,
sepolia,
arbitrum,
arbitrumSepolia,
optimism,
base,
polygon,
avalanche,
bsc,
gnosis,
zksync,
scroll,
linea,
} from "viem/chains";
Custom Chain Definition
import { defineChain } from "viem";
const myChain = defineChain({
id: 12345,
name: "My Chain",
nativeCurrency: {
name: "Ether",
symbol: "ETH",
decimals: 18,
},
rpcUrls: {
default: {
http: ["https://rpc.mychain.io"],
webSocket: ["wss://rpc.mychain.io"],
},
},
blockExplorers: {
default: {
name: "MyChainScan",
url: "https://scan.mychain.io",
},
},
});
const client = createPublicClient({
chain: myChain,
transport: http(),
});
Error Handling
| Error | Cause | Fix |
|---|---|---|
ContractFunctionExecutionError |
Contract call reverted | Check revert reason in error.cause, verify args and state |
ContractFunctionRevertedError |
Revert with reason string or custom error | Decode error.data for custom error details |
InsufficientFundsError |
Account lacks ETH for value + gas | Check balance before sending |
NonceAlreadyUsedError |
Nonce conflict from concurrent transactions | Use getTransactionCount with blockTag: "pending" |
TransactionReceiptNotFoundError |
Receipt unavailable (dropped or pending) | Retry waitForTransactionReceipt with higher timeout |
InvalidAddressError |
Malformed address passed | Validate with isAddress() before use |
ChainMismatchError |
Wallet is on a different chain than client | Switch chain or create client for correct chain |
HttpRequestError |
RPC node unreachable or rate-limited | Use fallback transport with multiple RPCs |
InvalidAbiError |
ABI is malformed or missing as const |
Verify ABI structure and add as const assertion |
Catching Contract Errors
import { BaseError, ContractFunctionRevertedError } from "viem";
try {
await client.simulateContract({ address, abi, functionName, args, account });
} catch (err) {
if (err instanceof BaseError) {
const revertError = err.walk(
(e) => e instanceof ContractFunctionRevertedError
);
if (revertError instanceof ContractFunctionRevertedError) {
const errorName = revertError.data?.errorName;
console.error(`Contract reverted: ${errorName}`, revertError.data?.args);
}
}
}
Common Patterns
ERC-20 Token Balance Check
import { createPublicClient, http, formatUnits } from "viem";
import { mainnet } from "viem/chains";
const erc20Abi = [
{
name: "balanceOf",
type: "function",
stateMutability: "view",
inputs: [{ name: "account", type: "address" }],
outputs: [{ name: "", type: "uint256" }],
},
{
name: "decimals",
type: "function",
stateMutability: "view",
inputs: [],
outputs: [{ name: "", type: "uint8" }],
},
{
name: "symbol",
type: "function",
stateMutability: "view",
inputs: [],
outputs: [{ name: "", type: "string" }],
},
] as const;
async function getTokenBalance(
client: ReturnType<typeof createPublicClient>,
token: `0x${string}`,
wallet: `0x${string}`
) {
const [balance, decimals, symbol] = await Promise.all([
client.readContract({
address: token,
abi: erc20Abi,
functionName: "balanceOf",
args: [wallet],
}),
client.readContract({
address: token,
abi: erc20Abi,
functionName: "decimals",
}),
client.readContract({
address: token,
abi: erc20Abi,
functionName: "symbol",
}),
]);
return {
raw: balance,
formatted: formatUnits(balance, decimals),
symbol,
decimals,
};
}
Approve + Swap Pattern (simulate-then-execute)
import { maxUint256, parseUnits } from "viem";
const tokenAbi = [
{
name: "approve",
type: "function",
stateMutability: "nonpayable",
inputs: [
{ name: "spender", type: "address" },
{ name: "amount", type: "uint256" },
],
outputs: [{ name: "", type: "bool" }],
},
] as const;
async function approveToken(
publicClient: ReturnType<typeof createPublicClient>,
walletClient: ReturnType<typeof createWalletClient>,
token: `0x${string}`,
spender: `0x${string}`,
amount: bigint
) {
const { request } = await publicClient.simulateContract({
address: token,
abi: tokenAbi,
functionName: "approve",
args: [spender, amount],
account: walletClient.account,
});
const hash = await walletClient.writeContract(request);
const receipt = await publicClient.waitForTransactionReceipt({ hash });
if (receipt.status === "reverted") {
throw new Error("Approval transaction reverted");
}
return receipt;
}
Deploy a Contract
import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { sepolia } from "viem/chains";
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const walletClient = createWalletClient({
account,
chain: sepolia,
transport: http(process.env.RPC_URL),
});
const hash = await walletClient.deployContract({
abi: contractAbi,
bytecode: contractBytecode,
args: [arg1, arg2],
});
const receipt = await publicClient.waitForTransactionReceipt({ hash });
console.log(`Deployed at ${receipt.contractAddress}`);
Gas Estimation
const gasEstimate = await client.estimateGas({
account: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
to: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
value: parseEther("0.1"),
});
const gasPrice = await client.getGasPrice();
const maxFeePerGas = await client.estimateFeesPerGas();
Batch JSON-RPC Requests
const batchClient = createPublicClient({
chain: mainnet,
transport: http(process.env.RPC_URL, {
batch: true,
}),
});
// These fire as a single batched JSON-RPC request
const [balance, blockNumber, gasPrice] = await Promise.all([
batchClient.getBalance({
address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
}),
batchClient.getBlockNumber(),
batchClient.getGasPrice(),
]);