
Viem Integration
- 936 installs
- 222 repo stars
- Updated August 4, 2026
- uniswap/uniswap-ai
viem-integration is a Claude Code skill that teaches developers to create Ethereum accounts, manage keys, and sign transactions with the viem TypeScript library in agents and scripts.
About
viem-integration is a Web3 development skill from uniswap/uniswap-ai that documents private key accounts, HD wallets, message signing, and WalletClient setup with viem in TypeScript. The reference covers privateKeyToAccount, createWalletClient, http transport, and mainnet chain imports with production warnings against test keys. Developers reach for viem-integration when building DeFi bots, swap agents, or on-chain automation that must derive addresses, sign messages, and submit transactions through viem instead of ethers.js or web3.js.
- Generates private keys and converts them to account objects
- Creates mnemonic-based HD wallets with standard derivation paths
- Integrates privateKeyToAccount with WalletClient for mainnet and testnet usage
- Provides ready-to-use TypeScript patterns for signing messages and sending transactions
- Includes explicit security warnings for test keys and public mnemonics
Viem Integration by the numbers
- 936 all-time installs (skills.sh)
- +36 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #421 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/uniswap/uniswap-ai --skill viem-integrationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 936 |
|---|---|
| repo stars | ★ 222 |
| Security audit | 2 / 3 scanners passed |
| Last updated | August 4, 2026 |
| Repository | uniswap/uniswap-ai ↗ |
How do you sign Ethereum transactions with viem in TypeScript?
Correctly create and manage Ethereum accounts and sign transactions using the viem library in TypeScript agents and scripts.
Who is it for?
TypeScript developers building Ethereum agents, scripts, or DeFi integrations who standardize on viem for account and transaction handling.
Skip if: Developers who need smart-contract authoring, Solidity audits, or a non-viem stack like ethers.js without migration intent.
When should I use this skill?
A developer asks to create Ethereum accounts, sign messages, or send transactions using viem in TypeScript agents or CLI scripts.
What you get
TypeScript snippets for viem accounts, WalletClient setup, chain config, and signed transaction flows
- viem account setup snippets
- WalletClient transaction signing examples
By the numbers
- Documents privateKeyToAccount, createWalletClient, and mainnet chain wiring in TypeScript
Files
viem Integration
Integrate EVM blockchains using viem for TypeScript/JavaScript applications.
Quick Decision Guide
| Building... | Use This |
|---|---|
| Node.js script/backend | viem with http transport |
| React/Next.js frontend | wagmi hooks (built on viem) |
| Real-time event monitoring | viem with webSocket transport |
| Browser wallet integration | wagmi or viem custom transport |
Installation
# Core library
npm install viem
# For React apps, also install wagmi
npm install wagmi viem @tanstack/react-queryCore Concepts
Clients
viem uses two client types:
| Client | Purpose | Example Use |
|---|---|---|
| PublicClient | Read-only operations | Get balances, read contracts, fetch logs |
| WalletClient | Write operations | Send transactions, sign messages |
Transports
| Transport | Use Case |
|---|---|
http() | Standard RPC calls (most common) |
webSocket() | Real-time event subscriptions |
custom() | Browser wallets (window.ethereum) |
Chains
viem includes 50+ chain definitions. Import from viem/chains:
import { mainnet, arbitrum, optimism, base, polygon } from 'viem/chains';---
Input Validation Rules
Before interpolating ANY user-provided value into generated TypeScript code:
- Ethereum addresses: MUST match
^0x[a-fA-F0-9]{40}$— use viem'sisAddress()for validation - Chain IDs: MUST be from viem's supported chain definitions
- Private keys: MUST NEVER be hardcoded — always use
process.env.PRIVATE_KEYwith runtime validation - RPC URLs: MUST use
https://orwss://protocols only - ABI inputs: Validate types match expected Solidity types before encoding
Quick Start Examples
Read Balance
import { createPublicClient, http, formatEther } from 'viem';
import { mainnet } from 'viem/chains';
const client = createPublicClient({
chain: mainnet,
transport: http(),
});
const balance = await client.getBalance({
address: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
});
console.log(`Balance: ${formatEther(balance)} ETH`);Read Contract
import { createPublicClient, http, parseAbi } from 'viem';
import { mainnet } from 'viem/chains';
const client = createPublicClient({
chain: mainnet,
transport: http(),
});
const abi = parseAbi([
'function balanceOf(address) view returns (uint256)',
'function decimals() view returns (uint8)',
]);
const balance = await client.readContract({
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC
abi,
functionName: 'balanceOf',
args: ['0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'],
});Send Transaction
import { createWalletClient, http, parseEther } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { mainnet } from 'viem/chains';
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const client = createWalletClient({
account,
chain: mainnet,
transport: http(),
});
const hash = await client.sendTransaction({
to: '0x...',
value: parseEther('0.1'),
});
console.log(`Transaction hash: ${hash}`);Write to Contract
import { createWalletClient, createPublicClient, http, parseAbi, parseUnits } 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(),
});
const publicClient = createPublicClient({
chain: mainnet,
transport: http(),
});
const abi = parseAbi(['function transfer(address to, uint256 amount) returns (bool)']);
// Simulate first to catch errors
const { request } = await publicClient.simulateContract({
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
abi,
functionName: 'transfer',
args: ['0x...', parseUnits('100', 6)],
account,
});
// Execute the transaction
const hash = await walletClient.writeContract(request);
// Wait for confirmation
const receipt = await publicClient.waitForTransactionReceipt({ hash });
console.log(`Confirmed in block ${receipt.blockNumber}`);---
Reference Documentation
For deeper coverage of specific topics:
| Topic | Reference File |
|---|---|
| Client setup, transports, chains | Clients & Transports |
| Reading blockchain data | Reading Data |
| Sending transactions | Writing Transactions |
| Private keys, HD wallets | Accounts & Keys |
| ABI handling, multicall | Contract Patterns |
| React/wagmi hooks | Wagmi React |
---
Related Plugins
Once you're comfortable with viem basics, the uniswap-trading plugin provides comprehensive Uniswap swap integration:
- Uniswap Trading API integration
- Universal Router SDK usage
- Token swap implementations
Install it with: claude plugin add @uniswap/uniswap-trading
---
Common Utilities
Unit Conversion
import { parseEther, formatEther, parseUnits, formatUnits } from 'viem';
// ETH
parseEther('1.5'); // 1500000000000000000n (wei)
formatEther(1500000000000000000n); // "1.5"
// Tokens (e.g., USDC with 6 decimals)
parseUnits('100', 6); // 100000000n
formatUnits(100000000n, 6); // "100"Address Utilities
import { getAddress, isAddress } from 'viem';
isAddress('0x...'); // true/false
getAddress('0x...'); // checksummed addressHashing
import { keccak256, toHex } from 'viem';
keccak256(toHex('hello')); // 0x1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8---
Error Handling
viem throws typed errors that can be caught and handled:
import { ContractFunctionExecutionError, InsufficientFundsError } from 'viem'
try {
await client.writeContract(...)
} catch (error) {
if (error instanceof ContractFunctionExecutionError) {
console.error('Contract call failed:', error.shortMessage)
}
if (error instanceof InsufficientFundsError) {
console.error('Not enough ETH for gas')
}
}---
Resources
Accounts and Keys
Reference for private key management, HD wallets, and message signing with viem.
Private Key Account
Basic Usage
import { privateKeyToAccount } from 'viem/accounts';
// ⚠️ Anvil default test key #0 — NEVER use in production!
const account = privateKeyToAccount(
'0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'
);
console.log(account.address); // 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266With WalletClient
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 client = createWalletClient({
account,
chain: mainnet,
transport: http(),
});Generate New Private Key
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
const privateKey = generatePrivateKey();
const account = privateKeyToAccount(privateKey);
console.log('Private Key:', privateKey);
console.log('Address:', account.address);---
Mnemonic / HD Wallet
Create Account from Mnemonic
import { mnemonicToAccount } from 'viem/accounts';
// ⚠️ NEVER use test mnemonics with real funds! This is a well-known example phrase.
const account = mnemonicToAccount(
'legal winner thank year wave sausage worth useful legal winner thank yellow'
);
console.log(account.address); // Default path: m/44'/60'/0'/0/0Custom Derivation Path
import { mnemonicToAccount } from 'viem/accounts';
// Different account indices
const account0 = mnemonicToAccount(mnemonic, { addressIndex: 0 }); // m/44'/60'/0'/0/0
const account1 = mnemonicToAccount(mnemonic, { addressIndex: 1 }); // m/44'/60'/0'/0/1
const account2 = mnemonicToAccount(mnemonic, { addressIndex: 2 }); // m/44'/60'/0'/0/2
// Custom account path
const account = mnemonicToAccount(mnemonic, {
accountIndex: 1, // m/44'/60'/1'/0/0
});
// Full custom path
const account = mnemonicToAccount(mnemonic, {
path: "m/44'/60'/0'/1/5",
});Generate New Mnemonic
import { generateMnemonic, english, mnemonicToAccount } from 'viem/accounts';
// Generate 12-word mnemonic
const mnemonic = generateMnemonic(english);
// Generate 24-word mnemonic
const mnemonic24 = generateMnemonic(english, 256);
const account = mnemonicToAccount(mnemonic);Other Languages
import {
generateMnemonic,
english,
spanish,
french,
italian,
japanese,
korean,
simplifiedChinese,
traditionalChinese,
czech,
portuguese,
} from 'viem/accounts';
const mnemonic = generateMnemonic(spanish);---
HD Key Derivation
From Master Seed
import { HDKey, hdKeyToAccount } from 'viem/accounts';
// From seed (Buffer/Uint8Array)
const hdKey = HDKey.fromMasterSeed(seed);
const account = hdKeyToAccount(hdKey);From Extended Key
import { HDKey, hdKeyToAccount } from 'viem/accounts';
// From xpriv/xpub
const hdKey = HDKey.fromExtendedKey('xprv9s21ZrQH143K...');
const account = hdKeyToAccount(hdKey);Derive Child Keys
import { HDKey, hdKeyToAccount } from 'viem/accounts';
const masterKey = HDKey.fromMasterSeed(seed);
// Derive specific path
const childKey = masterKey.derive("m/44'/60'/0'/0/0");
const account = hdKeyToAccount(childKey);
// Multiple accounts
const accounts = Array.from({ length: 10 }, (_, i) => {
const child = masterKey.derive(`m/44'/60'/0'/0/${i}`);
return hdKeyToAccount(child);
});---
Message Signing
Personal Sign (EIP-191)
import { privateKeyToAccount } from 'viem/accounts';
const account = privateKeyToAccount('0x...');
// Sign a message
const signature = await account.signMessage({
message: 'Hello, World!',
});
// Sign raw bytes
const signature = await account.signMessage({
message: { raw: '0x68656c6c6f' },
});Verify Message Signature
import { createPublicClient, http, verifyMessage } from 'viem';
import { mainnet } from 'viem/chains';
const client = createPublicClient({
chain: mainnet,
transport: http(),
});
const valid = await client.verifyMessage({
address: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
message: 'Hello, World!',
signature: '0x...',
});Typed Data Signing (EIP-712)
import { privateKeyToAccount } from 'viem/accounts';
const account = privateKeyToAccount('0x...');
const signature = await account.signTypedData({
domain: {
name: 'My App',
version: '1',
chainId: 1,
verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
},
types: {
Person: [
{ name: 'name', type: 'string' },
{ name: 'wallet', type: 'address' },
],
Mail: [
{ name: 'from', type: 'Person' },
{ name: 'to', type: 'Person' },
{ name: 'contents', type: 'string' },
],
},
primaryType: 'Mail',
message: {
from: {
name: 'Alice',
wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
},
to: {
name: 'Bob',
wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
},
contents: 'Hello, Bob!',
},
});Verify Typed Data
import { verifyTypedData } from 'viem'
const valid = await client.verifyTypedData({
address: '0x...',
domain: { ... },
types: { ... },
primaryType: 'Mail',
message: { ... },
signature: '0x...'
})---
Transaction Signing
Sign Transaction
import { privateKeyToAccount } from 'viem/accounts';
import { parseEther } from 'viem';
const account = privateKeyToAccount('0x...');
const signedTx = await account.signTransaction({
chainId: 1,
to: '0x...',
value: parseEther('1'),
maxFeePerGas: parseGwei('50'),
maxPriorityFeePerGas: parseGwei('2'),
nonce: 0,
gas: 21000n,
});
// signedTx is a serialized signed transaction---
Account Properties
LocalAccount Interface
const account = privateKeyToAccount('0x...');
account.address; // 0x... (checksummed address)
account.publicKey; // 0x... (uncompressed public key)
account.source; // 'privateKey' | 'mnemonic' | 'hd'
account.type; // 'local'
// Methods
account.signMessage({ message });
account.signTransaction(tx);
account.signTypedData(typedData);---
Security Best Practices
Environment Variables
// NEVER hardcode private keys
// BAD:
const account = privateKeyToAccount('0xac0974bec...');
// GOOD:
if (!process.env.PRIVATE_KEY) {
throw new Error('PRIVATE_KEY environment variable is required');
}
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);.env File
# .env (add to .gitignore!)
PRIVATE_KEY=0x_YOUR_PRIVATE_KEY_HERE
MNEMONIC=your twelve word mnemonic phrase goes here replace with actual words.gitignore
.env
.env.local
.env.*.local
*.key
*.pemSeparate Accounts
// Use different accounts for different purposes
const config = {
development: {
// Use test accounts with test ETH
privateKey: process.env.DEV_PRIVATE_KEY,
},
production: {
// Production account with real funds
privateKey: process.env.PROD_PRIVATE_KEY,
},
};
const account = privateKeyToAccount(
config[process.env.NODE_ENV || 'development'].privateKey as `0x${string}`
);Minimal Permissions
// For read-only operations, don't load private key
const publicClient = createPublicClient({
chain: mainnet,
transport: http(),
});
// Only create wallet client when needed
let walletClient: WalletClient | null = null;
function getWalletClient() {
if (!walletClient) {
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
walletClient = createWalletClient({
account,
chain: mainnet,
transport: http(),
});
}
return walletClient;
}---
Testing Accounts
Foundry/Anvil Test Accounts
These accounts are funded in local development environments:
// Anvil default accounts (DO NOT USE IN PRODUCTION)
const testAccounts = [
{
address: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
privateKey: '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80',
},
{
address: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8',
privateKey: '0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d',
},
// ... more accounts
];
// ⚠️ Test mnemonic — NEVER use with real funds! Any funds sent to these addresses WILL be stolen.
const testMnemonic = 'test test test test test test test test test test test junk';Create Test Account Helper
function createTestAccount(index: number = 0) {
if (process.env.NODE_ENV === 'production') {
throw new Error('Cannot use test accounts in production');
}
return mnemonicToAccount('test test test test test test test test test test test junk', {
addressIndex: index,
});
}Clients and Transports
Detailed reference for viem client setup, transports, and chain configuration.
PublicClient (Read Operations)
The PublicClient is used for all read-only blockchain operations.
Basic Setup
import { createPublicClient, http } from 'viem';
import { mainnet } from 'viem/chains';
const client = createPublicClient({
chain: mainnet,
transport: http(),
});With Custom RPC
const client = createPublicClient({
chain: mainnet,
transport: http('https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY'),
});Configuration Options
const client = createPublicClient({
chain: mainnet,
transport: http(),
batch: {
multicall: true, // Enable automatic batching via Multicall3
},
cacheTime: 4_000, // Cache duration in ms (default: 4000)
pollingInterval: 4_000, // Polling interval for subscriptions
});---
WalletClient (Write Operations)
The WalletClient is used for signing and sending transactions.
With Local Account (Node.js)
import { createWalletClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { mainnet } from 'viem/chains';
const account = privateKeyToAccount('0x...');
const client = createWalletClient({
account,
chain: mainnet,
transport: http(),
});With Browser Wallet (Frontend)
import { createWalletClient, custom } from 'viem';
import { mainnet } from 'viem/chains';
const client = createWalletClient({
chain: mainnet,
transport: custom(window.ethereum!),
});
// Request account access
const [address] = await client.requestAddresses();Get Addresses
// Get all connected addresses
const addresses = await client.getAddresses();
// Request connection (browser wallet)
const addresses = await client.requestAddresses();---
Transport Types
HTTP Transport
Standard RPC transport for most use cases.
import { http } from 'viem';
// Default (uses chain's public RPC)
http();
// Custom RPC URL
http('https://eth-mainnet.g.alchemy.com/v2/KEY');
// With options
http('https://...', {
batch: true, // Enable JSON-RPC batching
fetchOptions: {
// Custom fetch options
headers: {
'x-api-key': 'KEY',
},
},
retryCount: 3, // Retry failed requests
retryDelay: 150, // Delay between retries (ms)
timeout: 10_000, // Request timeout (ms)
});WebSocket Transport
For real-time event subscriptions.
import { webSocket } from 'viem';
// Basic
webSocket('wss://eth-mainnet.g.alchemy.com/v2/KEY');
// With options
webSocket('wss://...', {
keepAlive: true, // Enable keep-alive pings
reconnect: true, // Auto-reconnect on disconnect
retryCount: 3,
});Custom Transport
For browser wallet providers.
import { custom } from 'viem';
// MetaMask / injected wallet
custom(window.ethereum!);
// Any EIP-1193 provider
custom(provider);Fallback Transport
Use multiple transports with automatic failover.
import { fallback, http, webSocket } from 'viem';
const transport = fallback([
webSocket('wss://...'),
http('https://...'),
http(), // Public RPC as last resort
]);---
Chain Configuration
Built-in Chains
viem includes 50+ chain definitions:
import {
// Mainnets
mainnet,
arbitrum,
arbitrumNova,
avalanche,
base,
blast,
bsc,
celo,
fantom,
gnosis,
linea,
mantle,
optimism,
polygon,
polygonZkEvm,
scroll,
zkSync,
zora,
// Testnets
sepolia,
goerli,
arbitrumSepolia,
baseSepolia,
optimismSepolia,
polygonMumbai,
} from 'viem/chains';Chain Properties
Each chain includes:
import { mainnet } from 'viem/chains';
mainnet.id; // 1
mainnet.name; // "Ethereum"
mainnet.nativeCurrency; // { name: "Ether", symbol: "ETH", decimals: 18 }
mainnet.rpcUrls; // { default: { http: [...] } }
mainnet.blockExplorers; // { default: { name: "Etherscan", url: "..." } }Common Chain IDs
| Chain | ID | Import |
|---|---|---|
| Ethereum | 1 | mainnet |
| Arbitrum | 42161 | arbitrum |
| Optimism | 10 | optimism |
| Base | 8453 | base |
| Polygon | 137 | polygon |
| BNB Chain | 56 | bsc |
| Avalanche | 43114 | avalanche |
| Blast | 81457 | blast |
| zkSync Era | 324 | zkSync |
| Linea | 59144 | linea |
| Scroll | 534352 | scroll |
| Sepolia | 11155111 | sepolia |
Custom Chain Definition
import { defineChain } from 'viem';
const myChain = defineChain({
id: 123456,
name: 'My Chain',
nativeCurrency: {
name: 'My Token',
symbol: 'MYT',
decimals: 18,
},
rpcUrls: {
default: {
http: ['https://rpc.mychain.com'],
},
},
blockExplorers: {
default: {
name: 'My Explorer',
url: 'https://explorer.mychain.com',
},
},
});---
RPC Providers
Public RPCs
Each chain has a default public RPC (rate-limited):
const client = createPublicClient({
chain: mainnet,
transport: http(), // Uses public RPC
});Alchemy
const ALCHEMY_KEY = process.env.ALCHEMY_API_KEY;
const client = createPublicClient({
chain: mainnet,
transport: http(`https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}`),
});
// Chain-specific URLs
// Arbitrum: arb-mainnet.g.alchemy.com/v2/KEY
// Optimism: opt-mainnet.g.alchemy.com/v2/KEY
// Base: base-mainnet.g.alchemy.com/v2/KEY
// Polygon: polygon-mainnet.g.alchemy.com/v2/KEYInfura
const INFURA_KEY = process.env.INFURA_API_KEY;
const client = createPublicClient({
chain: mainnet,
transport: http(`https://mainnet.infura.io/v3/${INFURA_KEY}`),
});
// Chain-specific URLs
// Arbitrum: arbitrum-mainnet.infura.io/v3/KEY
// Optimism: optimism-mainnet.infura.io/v3/KEY
// Polygon: polygon-mainnet.infura.io/v3/KEYQuickNode
const client = createPublicClient({
chain: mainnet,
transport: http('https://your-endpoint.quiknode.pro/your-key/'),
});Rate Limits and Best Practices
Public RPCs and free-tier provider plans enforce rate limits. Exceeding them causes 429 Too Many Requests errors or silent throttling.
| Provider | Free Tier Limit | Notes |
|---|---|---|
| Public RPCs | ~5-10 req/s | Shared across all users; avoid for production |
| Alchemy (free) | 330 CU/s (~30 req/s) | Compute Units vary by method |
| Infura (free) | 10 req/s | Per-second burst limit |
| QuickNode (free) | 25 req/s | Varies by plan |
Mitigation strategies:
1. Use `batch.multicall` — Batches multiple eth_call reads into a single Multicall3 contract call, dramatically reducing request count. 2. Use the `fallback` transport — Automatically retries on a backup RPC when the primary is rate-limited. 3. Set `retryCount` and `retryDelay` on transports to handle transient 429s gracefully. 4. Cache aggressively — The cacheTime option on createPublicClient avoids redundant calls for data that doesn't change within the cache window.
// Production-ready client with rate limit resilience
const client = createPublicClient({
chain: mainnet,
transport: fallback([
http('https://eth-mainnet.g.alchemy.com/v2/KEY', {
retryCount: 3,
retryDelay: 500,
}),
http('https://mainnet.infura.io/v3/KEY', {
retryCount: 2,
retryDelay: 1000,
}),
http(), // Public RPC last resort
]),
batch: {
multicall: true,
},
cacheTime: 4_000,
});---
Multi-Chain Setup
Separate Clients
import { createPublicClient, http } from 'viem';
import { mainnet, arbitrum, base } from 'viem/chains';
const clients = {
[mainnet.id]: createPublicClient({
chain: mainnet,
transport: http(),
}),
[arbitrum.id]: createPublicClient({
chain: arbitrum,
transport: http(),
}),
[base.id]: createPublicClient({
chain: base,
transport: http(),
}),
};
// Use by chain ID
const balance = await clients[1].getBalance({ address: '0x...' });Factory Function
import { createPublicClient, http, type Chain } from 'viem';
import { mainnet, arbitrum, base } from 'viem/chains';
const chains: Record<number, Chain> = {
[mainnet.id]: mainnet,
[arbitrum.id]: arbitrum,
[base.id]: base,
};
function getClient(chainId: number) {
const chain = chains[chainId];
if (!chain) throw new Error(`Unsupported chain: ${chainId}`);
return createPublicClient({
chain,
transport: http(),
});
}---
Environment Variables Pattern
Recommended setup for Node.js applications:
// config.ts
import { createPublicClient, createWalletClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { mainnet } from 'viem/chains';
if (!process.env.RPC_URL) {
throw new Error('RPC_URL environment variable is required');
}
export const publicClient = createPublicClient({
chain: mainnet,
transport: http(process.env.RPC_URL),
});
// Only create wallet client if private key is available
export const walletClient = process.env.PRIVATE_KEY
? createWalletClient({
account: privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`),
chain: mainnet,
transport: http(process.env.RPC_URL),
})
: null;Example .env:
RPC_URL=https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY
PRIVATE_KEY=0x...Contract Patterns
Reference for ABI handling, contract instances, multicall, and encoding/decoding with viem.
ABI Formats
JSON ABI (From Compilation)
// Typically from Solidity compiler output
const abi = [
{
inputs: [{ name: 'owner', type: 'address' }],
name: 'balanceOf',
outputs: [{ name: '', type: 'uint256' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [
{ name: 'to', type: 'address' },
{ name: 'amount', type: 'uint256' },
],
name: 'transfer',
outputs: [{ name: '', type: 'bool' }],
stateMutability: 'nonpayable',
type: 'function',
},
] as const;Human-Readable ABI
import { parseAbi, parseAbiItem } from 'viem';
// Parse multiple items
const abi = parseAbi([
'function balanceOf(address owner) view returns (uint256)',
'function transfer(address to, uint256 amount) returns (bool)',
'function approve(address spender, uint256 amount) returns (bool)',
'function allowance(address owner, address spender) view returns (uint256)',
'event Transfer(address indexed from, address indexed to, uint256 value)',
'event Approval(address indexed owner, address indexed spender, uint256 value)',
'error InsufficientBalance(uint256 available, uint256 required)',
]);
// Parse single item
const transferEvent = parseAbiItem(
'event Transfer(address indexed from, address indexed to, uint256 value)'
);ABI Type Inference
viem provides full TypeScript inference for ABIs:
const abi = parseAbi([
'function balanceOf(address) view returns (uint256)',
'function transfer(address to, uint256 amount) returns (bool)',
]);
// TypeScript knows:
// - balanceOf takes 1 address arg, returns bigint
// - transfer takes address + bigint, returns boolean
const balance = await client.readContract({
address: '0x...',
abi,
functionName: 'balanceOf', // Autocomplete works!
args: ['0x...'], // Type-checked: must be [Address]
});
// balance is typed as bigint---
Contract Instance Pattern
getContract
import { getContract } from 'viem';
const contract = getContract({
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
abi: erc20Abi,
client: publicClient,
});
// Read functions
const name = await contract.read.name();
const balance = await contract.read.balanceOf(['0x...']);
const allowance = await contract.read.allowance(['0xOwner', '0xSpender']);
// Get events
const events = await contract.getEvents.Transfer({
fromBlock: 18000000n,
});With Both Clients
const contract = getContract({
address: '0x...',
abi: erc20Abi,
client: {
public: publicClient,
wallet: walletClient,
},
});
// Read
const balance = await contract.read.balanceOf(['0x...']);
// Write
const hash = await contract.write.transfer(['0x...', parseUnits('100', 18)]);
// Simulate
const { result } = await contract.simulate.transfer(['0x...', amount]);Watch Events
const unwatch = contract.watchEvent.Transfer({
onLogs: (logs) => {
console.log('New transfers:', logs);
},
});---
Multicall (Batch Reads)
Basic Multicall
const results = await publicClient.multicall({
contracts: [
{
address: '0xToken1',
abi: erc20Abi,
functionName: 'balanceOf',
args: ['0xUser'],
},
{
address: '0xToken2',
abi: erc20Abi,
functionName: 'balanceOf',
args: ['0xUser'],
},
{
address: '0xToken3',
abi: erc20Abi,
functionName: 'balanceOf',
args: ['0xUser'],
},
],
});
// Results array matches input order
// Each result has: { result, status: 'success' } or { error, status: 'failure' }
for (const res of results) {
if (res.status === 'success') {
console.log('Balance:', res.result);
} else {
console.error('Failed:', res.error);
}
}Reusable Contract Config
const usdcContract = {
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
abi: erc20Abi,
} as const;
const results = await publicClient.multicall({
contracts: [
{ ...usdcContract, functionName: 'name' },
{ ...usdcContract, functionName: 'symbol' },
{ ...usdcContract, functionName: 'decimals' },
{ ...usdcContract, functionName: 'totalSupply' },
],
});
const [name, symbol, decimals, totalSupply] = results.map((r) =>
r.status === 'success' ? r.result : null
);Allow Failures
const results = await publicClient.multicall({
contracts: [...],
allowFailure: true // Default: true, continue on individual failures
})
// With allowFailure: false, throws on any failure
const results = await publicClient.multicall({
contracts: [...],
allowFailure: false
})At Specific Block
const results = await publicClient.multicall({
contracts: [...],
blockNumber: 18000000n
})---
Encoding Functions
encodeFunctionData
Encode a function call to calldata:
import { encodeFunctionData, parseAbi } from 'viem';
const abi = parseAbi(['function transfer(address to, uint256 amount) returns (bool)']);
const data = encodeFunctionData({
abi,
functionName: 'transfer',
args: ['0xRecipient', parseUnits('100', 18)],
});
// data = '0xa9059cbb000000000000000000000000...'For Low-Level Calls
const hash = await walletClient.sendTransaction({
to: '0xContractAddress',
data: encodeFunctionData({
abi,
functionName: 'transfer',
args: ['0x...', amount],
}),
});encodeAbiParameters
Encode raw parameters (no function selector):
import { encodeAbiParameters, parseAbiParameters } from 'viem';
const encoded = encodeAbiParameters(parseAbiParameters('address, uint256'), ['0x...', 123n]);---
Decoding Functions
decodeFunctionResult
Decode return data from a function call:
import { decodeFunctionResult, parseAbi } from 'viem';
const abi = parseAbi(['function balanceOf(address) view returns (uint256)']);
const result = decodeFunctionResult({
abi,
functionName: 'balanceOf',
data: '0x0000000000000000000000000000000000000000000000000000000000000064',
});
// result = 100ndecodeFunctionData
Decode calldata back to function name and args:
import { decodeFunctionData, parseAbi } from 'viem';
const abi = parseAbi(['function transfer(address to, uint256 amount) returns (bool)']);
const { functionName, args } = decodeFunctionData({
abi,
data: '0xa9059cbb...',
});
// functionName = 'transfer'
// args = ['0x...', 100n]decodeAbiParameters
Decode raw ABI-encoded data:
import { decodeAbiParameters, parseAbiParameters } from 'viem';
const decoded = decodeAbiParameters(
parseAbiParameters('address, uint256'),
'0x000000000000000000000000...'
);
// decoded = ['0x...', 100n]---
Event Decoding
decodeEventLog
import { decodeEventLog, parseAbi } from 'viem';
const abi = parseAbi(['event Transfer(address indexed from, address indexed to, uint256 value)']);
const { eventName, args } = decodeEventLog({
abi,
data: log.data,
topics: log.topics,
});
// eventName = 'Transfer'
// args = { from: '0x...', to: '0x...', value: 100n }Parse Log from Receipt
const receipt = await publicClient.getTransactionReceipt({ hash: '0x...' });
for (const log of receipt.logs) {
try {
const { eventName, args } = decodeEventLog({
abi: erc20Abi,
data: log.data,
topics: log.topics,
});
console.log(eventName, args);
} catch {
// Log doesn't match this ABI
}
}---
Error Decoding
decodeErrorResult
import { decodeErrorResult, parseAbi } from 'viem';
const abi = parseAbi(['error InsufficientBalance(uint256 available, uint256 required)']);
const { errorName, args } = decodeErrorResult({
abi,
data: '0x...',
});
// errorName = 'InsufficientBalance'
// args = { available: 50n, required: 100n }---
Common Contract ABIs
ERC-20 (Tokens)
const erc20Abi = parseAbi([
'function name() view returns (string)',
'function symbol() view returns (string)',
'function decimals() view returns (uint8)',
'function totalSupply() view returns (uint256)',
'function balanceOf(address owner) view returns (uint256)',
'function transfer(address to, uint256 amount) returns (bool)',
'function transferFrom(address from, address to, uint256 amount) returns (bool)',
'function approve(address spender, uint256 amount) returns (bool)',
'function allowance(address owner, address spender) view returns (uint256)',
'event Transfer(address indexed from, address indexed to, uint256 value)',
'event Approval(address indexed owner, address indexed spender, uint256 value)',
]);ERC-721 (NFTs)
const erc721Abi = parseAbi([
'function balanceOf(address owner) view returns (uint256)',
'function ownerOf(uint256 tokenId) view returns (address)',
'function safeTransferFrom(address from, address to, uint256 tokenId)',
'function transferFrom(address from, address to, uint256 tokenId)',
'function approve(address to, uint256 tokenId)',
'function getApproved(uint256 tokenId) view returns (address)',
'function setApprovalForAll(address operator, bool approved)',
'function isApprovedForAll(address owner, address operator) view returns (bool)',
'function tokenURI(uint256 tokenId) view returns (string)',
'event Transfer(address indexed from, address indexed to, uint256 indexed tokenId)',
'event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId)',
'event ApprovalForAll(address indexed owner, address indexed operator, bool approved)',
]);ERC-1155 (Multi-Token)
const erc1155Abi = parseAbi([
'function balanceOf(address account, uint256 id) view returns (uint256)',
'function balanceOfBatch(address[] accounts, uint256[] ids) view returns (uint256[])',
'function setApprovalForAll(address operator, bool approved)',
'function isApprovedForAll(address account, address operator) view returns (bool)',
'function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes data)',
'function safeBatchTransferFrom(address from, address to, uint256[] ids, uint256[] amounts, bytes data)',
'function uri(uint256 id) view returns (string)',
'event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value)',
'event TransferBatch(address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values)',
]);Multicall3
const multicall3Abi = parseAbi([
'function aggregate3(tuple(address target, bool allowFailure, bytes callData)[] calls) returns (tuple(bool success, bytes returnData)[])',
'function aggregate3Value(tuple(address target, bool allowFailure, uint256 value, bytes callData)[] calls) payable returns (tuple(bool success, bytes returnData)[])',
]);
// Multicall3 address (same on all chains)
const MULTICALL3_ADDRESS = '0xcA11bde05977b3631167028862bE2a173976CA11';Reading Data
Reference for all read operations from the blockchain using viem.
Account Data
Get Balance
import { createPublicClient, http, formatEther } from 'viem';
import { mainnet } from 'viem/chains';
const client = createPublicClient({
chain: mainnet,
transport: http(),
});
// Get ETH balance
const balance = await client.getBalance({
address: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
});
console.log(`${formatEther(balance)} ETH`);
// At specific block
const historicalBalance = await client.getBalance({
address: '0x...',
blockNumber: 18000000n,
});
// At block tag
const pendingBalance = await client.getBalance({
address: '0x...',
blockTag: 'pending', // 'latest' | 'earliest' | 'pending' | 'safe' | 'finalized'
});Get Transaction Count (Nonce)
const nonce = await client.getTransactionCount({
address: '0x...',
});
// Pending nonce (includes mempool txs)
const pendingNonce = await client.getTransactionCount({
address: '0x...',
blockTag: 'pending',
});Get Bytecode
const bytecode = await client.getCode({
address: '0x...',
});
// Check if address is a contract
const isContract = bytecode && bytecode !== '0x';---
Block Data
Get Block
// Latest block
const block = await client.getBlock();
// By number
const block = await client.getBlock({
blockNumber: 18000000n,
});
// By hash
const block = await client.getBlock({
blockHash: '0x...',
});
// Include transactions
const blockWithTxs = await client.getBlock({
blockNumber: 18000000n,
includeTransactions: true,
});Block Properties
const block = await client.getBlock();
block.number; // bigint - block number
block.hash; // string - block hash
block.timestamp; // bigint - unix timestamp
block.gasUsed; // bigint - gas used
block.gasLimit; // bigint - gas limit
block.baseFeePerGas; // bigint | null - EIP-1559 base fee
block.transactions; // string[] | Transaction[] - tx hashes or full txs
block.parentHash; // string - parent block hash
block.miner; // string - miner/validator addressGet Block Number
const blockNumber = await client.getBlockNumber();Watch Blocks
const unwatch = client.watchBlocks({
onBlock: (block) => {
console.log(`New block: ${block.number}`);
},
});
// Stop watching
unwatch();---
Reading Contracts
readContract
import { parseAbi } from 'viem';
const abi = parseAbi([
'function balanceOf(address owner) view returns (uint256)',
'function name() view returns (string)',
'function symbol() view returns (string)',
'function decimals() view returns (uint8)',
'function totalSupply() view returns (uint256)',
]);
// Read single function
const balance = await client.readContract({
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC
abi,
functionName: 'balanceOf',
args: ['0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'],
});
// Read without args
const name = await client.readContract({
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
abi,
functionName: 'name',
});
// At specific block
const historicalBalance = await client.readContract({
address: '0x...',
abi,
functionName: 'balanceOf',
args: ['0x...'],
blockNumber: 18000000n,
});Using getContract
For multiple reads on the same contract:
import { getContract } from 'viem';
const contract = getContract({
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
abi,
client,
});
// Cleaner syntax for reads
const balance = await contract.read.balanceOf(['0x...']);
const name = await contract.read.name();
const decimals = await contract.read.decimals();---
Fetching Logs/Events
getLogs
import { parseAbiItem } from 'viem';
// Define event
const transferEvent = parseAbiItem(
'event Transfer(address indexed from, address indexed to, uint256 value)'
);
// Get all Transfer events from a contract
const logs = await client.getLogs({
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
event: transferEvent,
fromBlock: 18000000n,
toBlock: 18001000n,
});
// Process logs
for (const log of logs) {
console.log(`Transfer: ${log.args.from} -> ${log.args.to}: ${log.args.value}`);
}Filter by Indexed Parameters
// Get transfers TO a specific address
const logs = await client.getLogs({
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
event: transferEvent,
args: {
to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
},
fromBlock: 18000000n,
toBlock: 'latest',
});
// Get transfers FROM a specific address
const logs = await client.getLogs({
address: '0x...',
event: transferEvent,
args: {
from: '0x...',
},
fromBlock: 18000000n,
});
// Multiple values (OR condition)
const logs = await client.getLogs({
address: '0x...',
event: transferEvent,
args: {
from: ['0xAddress1', '0xAddress2'],
},
fromBlock: 18000000n,
});Get All Events (No Filter)
// Get all events from a contract (use with caution - can be large)
const logs = await client.getLogs({
address: '0x...',
fromBlock: 18000000n,
toBlock: 18000100n,
});getContractEvents
Alternative syntax using ABI:
const logs = await client.getContractEvents({
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
abi,
eventName: 'Transfer',
fromBlock: 18000000n,
toBlock: 18001000n,
});---
Watching Events (Real-time)
watchContractEvent
const unwatch = client.watchContractEvent({
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
abi,
eventName: 'Transfer',
onLogs: (logs) => {
for (const log of logs) {
console.log(`New transfer: ${log.args.from} -> ${log.args.to}`);
}
},
});
// Stop watching
unwatch();Watch with Filters
const unwatch = client.watchContractEvent({
address: '0x...',
abi,
eventName: 'Transfer',
args: {
to: '0xMyAddress...', // Only transfers to me
},
onLogs: (logs) => {
console.log('Received transfer!');
},
});Watch All Events
const unwatch = client.watchEvent({
onLogs: (logs) => {
console.log('New events:', logs);
},
});Error Handling
const unwatch = client.watchContractEvent({
address: '0x...',
abi,
eventName: 'Transfer',
onLogs: (logs) => {
console.log('New logs:', logs);
},
onError: (error) => {
console.error('Watch error:', error);
},
});---
Transaction Data
Get Transaction
const tx = await client.getTransaction({
hash: '0x...',
});
tx.hash; // transaction hash
tx.from; // sender address
tx.to; // recipient address
tx.value; // ETH value in wei
tx.input; // calldata
tx.gas; // gas limit
tx.gasPrice; // gas price (legacy)
tx.maxFeePerGas; // max fee (EIP-1559)
tx.maxPriorityFeePerGas; // priority fee (EIP-1559)
tx.nonce; // sender nonce
tx.blockNumber; // block number (null if pending)
tx.blockHash; // block hash (null if pending)Get Transaction Receipt
const receipt = await client.getTransactionReceipt({
hash: '0x...',
});
receipt.status; // 'success' | 'reverted'
receipt.blockNumber; // block number
receipt.gasUsed; // gas used
receipt.effectiveGasPrice; // actual gas price paid
receipt.logs; // event logs
receipt.contractAddress; // deployed contract address (if deployment)Wait for Transaction
// Wait for confirmation
const receipt = await client.waitForTransactionReceipt({
hash: '0x...',
});
// With options
const receipt = await client.waitForTransactionReceipt({
hash: '0x...',
confirmations: 2, // Wait for N confirmations
timeout: 60_000, // Timeout in ms
pollingInterval: 1_000, // Poll every N ms
});Watch Pending Transactions
const unwatch = client.watchPendingTransactions({
onTransactions: (hashes) => {
console.log('Pending txs:', hashes);
},
});---
Gas Estimation
Estimate Gas
const gas = await client.estimateGas({
account: '0x...',
to: '0x...',
value: parseEther('1'),
});Estimate Contract Gas
const gas = await client.estimateContractGas({
address: '0x...',
abi,
functionName: 'transfer',
args: ['0x...', parseUnits('100', 18)],
account: '0x...',
});Get Fee Data
// EIP-1559 fees
const { maxFeePerGas, maxPriorityFeePerGas } = await client.estimateFeesPerGas();
// Legacy gas price
const gasPrice = await client.getGasPrice();---
Chain Data
Get Chain ID
const chainId = await client.getChainId();Get Block Gas Limit
const block = await client.getBlock();
const gasLimit = block.gasLimit;---
Batching Reads
Use multicall for efficient batch reads (see Contract Patterns for details):
const results = await client.multicall({
contracts: [
{ address: token1, abi, functionName: 'balanceOf', args: [user] },
{ address: token2, abi, functionName: 'balanceOf', args: [user] },
{ address: token3, abi, functionName: 'balanceOf', args: [user] },
],
});---
ENS Resolution
Resolve ENS names to addresses using viem's ENS utilities:
import { createPublicClient, http } from 'viem';
import { mainnet } from 'viem/chains';
import { normalize } from 'viem/ens';
const client = createPublicClient({
chain: mainnet,
transport: http(),
});
// Resolve ENS name to address
const address = await client.getEnsAddress({
name: normalize('vitalik.eth'),
});
// Returns: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'
// Reverse resolve: address to ENS name
const name = await client.getEnsName({
address: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
});
// Returns: 'vitalik.eth'Important: Always use normalize() from viem/ens to normalize ENS names before resolution. This handles Unicode normalization (UTS-46) required by the ENS protocol.
Common Uniswap V3 ABIs
Frequently needed ABIs for Uniswap V3 contract interactions:
import { parseAbi } from 'viem';
// Uniswap V3 Pool events
const poolAbi = parseAbi([
'event Swap(address indexed sender, address indexed recipient, int256 amount0, int256 amount1, uint160 sqrtPriceX96, uint128 liquidity, int24 tick)',
'event Mint(address sender, address indexed owner, int24 indexed tickLower, int24 indexed tickUpper, uint128 amount, uint256 amount0, uint256 amount1)',
'event Burn(address indexed owner, int24 indexed tickLower, int24 indexed tickUpper, uint128 amount, uint256 amount0, uint256 amount1)',
'event Collect(address indexed owner, address recipient, int24 indexed tickLower, int24 indexed tickUpper, uint128 amount0, uint128 amount1)',
'function slot0() view returns (uint160 sqrtPriceX96, int24 tick, uint16 observationIndex, uint16 observationCardinality, uint16 observationCardinalityNext, uint8 feeProtocol, bool unlocked)',
'function liquidity() view returns (uint128)',
'function fee() view returns (uint24)',
'function token0() view returns (address)',
'function token1() view returns (address)',
]);
// Uniswap V3 Factory
const factoryAbi = parseAbi([
'event PoolCreated(address indexed token0, address indexed token1, uint24 indexed fee, int24 tickSpacing, address pool)',
'function getPool(address tokenA, address tokenB, uint24 fee) view returns (address pool)',
]);
// Example: Read pool price
const [sqrtPriceX96, tick] = await client.readContract({
address: '0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640',
abi: poolAbi,
functionName: 'slot0',
});
// Example: Find a pool address
const poolAddress = await client.readContract({
address: '0x1F98431c8aD98523631AE4a59f267346ea31F984', // V3 Factory
abi: factoryAbi,
functionName: 'getPool',
args: [
'0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC
'0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', // WETH
3000, // 0.3% fee tier
],
});Wagmi React
Reference for React/wagmi hooks for building frontend blockchain applications.
Setup
Installation
npm install wagmi viem @tanstack/react-queryConfiguration
// config.ts
import { createConfig, http } from 'wagmi';
import { mainnet, arbitrum, optimism, base, polygon } from 'wagmi/chains';
import { injected, walletConnect, coinbaseWallet } from 'wagmi/connectors';
const projectId = 'YOUR_WALLETCONNECT_PROJECT_ID';
export const config = createConfig({
chains: [mainnet, arbitrum, optimism, base, polygon],
connectors: [injected(), walletConnect({ projectId }), coinbaseWallet({ appName: 'My App' })],
transports: {
[mainnet.id]: http(),
[arbitrum.id]: http(),
[optimism.id]: http(),
[base.id]: http(),
[polygon.id]: http(),
},
});
// Type declaration for TypeScript
declare module 'wagmi' {
interface Register {
config: typeof config;
}
}Provider Setup
// App.tsx
import { WagmiProvider } from 'wagmi';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { config } from './config';
const queryClient = new QueryClient();
function App({ children }: { children: React.ReactNode }) {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</WagmiProvider>
);
}---
Wallet Connection
useAccount
Get current account status and address:
import { useAccount } from 'wagmi';
function Profile() {
const { address, isConnected, isConnecting, isDisconnected, chain } = useAccount();
if (isConnecting) return <div>Connecting...</div>;
if (isDisconnected) return <div>Disconnected</div>;
return (
<div>
<p>Address: {address}</p>
<p>Chain: {chain?.name}</p>
</div>
);
}useConnect
Connect to a wallet:
import { useConnect } from 'wagmi';
function WalletOptions() {
const { connect, connectors, isPending, error } = useConnect();
return (
<div>
{connectors.map((connector) => (
<button key={connector.uid} onClick={() => connect({ connector })} disabled={isPending}>
{connector.name}
</button>
))}
{error && <div>Error: {error.message}</div>}
</div>
);
}useDisconnect
Disconnect wallet:
import { useDisconnect } from 'wagmi';
function DisconnectButton() {
const { disconnect } = useDisconnect();
return <button onClick={() => disconnect()}>Disconnect</button>;
}Complete Connect Flow
import { useAccount, useConnect, useDisconnect } from 'wagmi';
function ConnectWallet() {
const { address, isConnected } = useAccount();
const { connect, connectors, isPending } = useConnect();
const { disconnect } = useDisconnect();
if (isConnected) {
return (
<div>
<p>{address}</p>
<button onClick={() => disconnect()}>Disconnect</button>
</div>
);
}
return (
<div>
{connectors.map((connector) => (
<button key={connector.uid} onClick={() => connect({ connector })} disabled={isPending}>
Connect {connector.name}
</button>
))}
</div>
);
}---
Reading Data
useBalance
Get ETH or token balance:
import { useBalance } from 'wagmi';
function Balance() {
const { data, isLoading, error } = useBalance({
address: '0x...',
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
Balance: {data?.formatted} {data?.symbol}
</div>
);
}
// Token balance
function TokenBalance() {
const { data } = useBalance({
address: '0xUserAddress',
token: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC
});
return <div>USDC: {data?.formatted}</div>;
}useReadContract
Read from a smart contract:
import { useReadContract } from 'wagmi';
import { parseAbi } from 'viem';
const abi = parseAbi([
'function balanceOf(address) view returns (uint256)',
'function totalSupply() view returns (uint256)',
]);
function TokenInfo() {
const { data: balance } = useReadContract({
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
abi,
functionName: 'balanceOf',
args: ['0xUserAddress'],
});
const { data: totalSupply } = useReadContract({
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
abi,
functionName: 'totalSupply',
});
return (
<div>
<p>Balance: {balance?.toString()}</p>
<p>Total Supply: {totalSupply?.toString()}</p>
</div>
);
}useReadContracts (Batch)
Read multiple contracts in one request:
import { useReadContracts } from 'wagmi';
const usdcContract = {
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
abi: erc20Abi,
} as const;
function MultipleReads() {
const { data } = useReadContracts({
contracts: [
{ ...usdcContract, functionName: 'name' },
{ ...usdcContract, functionName: 'symbol' },
{ ...usdcContract, functionName: 'decimals' },
{ ...usdcContract, functionName: 'balanceOf', args: ['0x...'] },
],
});
const [name, symbol, decimals, balance] = data || [];
return (
<div>
<p>Name: {name?.result}</p>
<p>Symbol: {symbol?.result}</p>
<p>Decimals: {decimals?.result}</p>
<p>Balance: {balance?.result?.toString()}</p>
</div>
);
}---
Writing Data
useWriteContract
Write to a smart contract:
import { useWriteContract, useWaitForTransactionReceipt } from 'wagmi';
import { parseUnits } from 'viem';
function TransferToken() {
const { writeContract, data: hash, isPending, error } = useWriteContract();
const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({
hash,
});
const handleTransfer = () => {
writeContract({
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
abi: erc20Abi,
functionName: 'transfer',
args: ['0xRecipient', parseUnits('100', 6)],
});
};
return (
<div>
<button onClick={handleTransfer} disabled={isPending}>
{isPending ? 'Confirming...' : 'Transfer'}
</button>
{isConfirming && <div>Waiting for confirmation...</div>}
{isSuccess && <div>Transaction confirmed!</div>}
{error && <div>Error: {error.message}</div>}
</div>
);
}useSendTransaction
Send native token (ETH):
import { useSendTransaction, useWaitForTransactionReceipt } from 'wagmi';
import { parseEther } from 'viem';
function SendEth() {
const { sendTransaction, data: hash, isPending } = useSendTransaction();
const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({
hash,
});
return (
<button
onClick={() =>
sendTransaction({
to: '0xRecipient',
value: parseEther('0.1'),
})
}
disabled={isPending || isConfirming}
>
{isPending ? 'Confirming...' : isConfirming ? 'Processing...' : 'Send 0.1 ETH'}
</button>
);
}useSimulateContract
Simulate before writing:
import { useSimulateContract, useWriteContract } from 'wagmi';
function SafeTransfer() {
const { data: simulation, error: simError } = useSimulateContract({
address: '0x...',
abi: erc20Abi,
functionName: 'transfer',
args: ['0x...', parseUnits('100', 6)],
});
const { writeContract, isPending } = useWriteContract();
return (
<div>
{simError && <div>Simulation failed: {simError.message}</div>}
<button
onClick={() => simulation && writeContract(simulation.request)}
disabled={!simulation || isPending}
>
Transfer
</button>
</div>
);
}---
Chain Management
useChainId
Get current chain ID:
import { useChainId } from 'wagmi';
function CurrentChain() {
const chainId = useChainId();
return <div>Chain ID: {chainId}</div>;
}useSwitchChain
Switch to a different chain:
import { useSwitchChain, useChainId } from 'wagmi';
function ChainSwitcher() {
const chainId = useChainId();
const { chains, switchChain, isPending, error } = useSwitchChain();
return (
<div>
<p>Current Chain: {chainId}</p>
{chains.map((chain) => (
<button
key={chain.id}
onClick={() => switchChain({ chainId: chain.id })}
disabled={chain.id === chainId || isPending}
>
{chain.name}
</button>
))}
{error && <div>Error: {error.message}</div>}
</div>
);
}---
Message Signing
useSignMessage
Sign a message:
import { useSignMessage } from 'wagmi';
function SignMessage() {
const { signMessage, data: signature, isPending, error } = useSignMessage();
return (
<div>
<button onClick={() => signMessage({ message: 'Hello, World!' })} disabled={isPending}>
Sign Message
</button>
{signature && <div>Signature: {signature}</div>}
{error && <div>Error: {error.message}</div>}
</div>
);
}useSignTypedData
Sign EIP-712 typed data:
import { useSignTypedData } from 'wagmi';
function SignTypedData() {
const { signTypedData, data: signature } = useSignTypedData();
const handleSign = () => {
signTypedData({
domain: {
name: 'My App',
version: '1',
chainId: 1,
verifyingContract: '0x...',
},
types: {
Person: [
{ name: 'name', type: 'string' },
{ name: 'wallet', type: 'address' },
],
},
primaryType: 'Person',
message: {
name: 'Bob',
wallet: '0x...',
},
});
};
return <button onClick={handleSign}>Sign Typed Data</button>;
}---
ENS
useEnsName
Resolve address to ENS name:
import { useEnsName } from 'wagmi';
function EnsName({ address }: { address: `0x${string}` }) {
const { data: ensName } = useEnsName({ address });
return <div>{ensName || address}</div>;
}useEnsAddress
Resolve ENS name to address:
import { useEnsAddress } from 'wagmi';
function EnsAddress({ name }: { name: string }) {
const { data: address } = useEnsAddress({ name });
return <div>{address}</div>;
}useEnsAvatar
Get ENS avatar:
import { useEnsAvatar, useEnsName } from 'wagmi';
function Profile({ address }: { address: `0x${string}` }) {
const { data: ensName } = useEnsName({ address });
const { data: ensAvatar } = useEnsAvatar({ name: ensName! });
return (
<div>
{ensAvatar && <img src={ensAvatar} alt="Avatar" />}
<span>{ensName || address}</span>
</div>
);
}---
Hook Reference Table
| Hook | Purpose |
|---|---|
| Connection | |
useAccount | Get account status, address, chain |
useConnect | Connect to a wallet |
useDisconnect | Disconnect wallet |
useConnectors | Get available connectors |
| Reading | |
useBalance | Get ETH/token balance |
useReadContract | Read contract function |
useReadContracts | Batch read multiple contracts |
useBlockNumber | Get current block number |
useGasPrice | Get current gas price |
| Writing | |
useWriteContract | Write to contract |
useSendTransaction | Send native token |
useSimulateContract | Simulate contract call |
useWaitForTransactionReceipt | Wait for tx confirmation |
| Chain | |
useChainId | Get current chain ID |
useSwitchChain | Switch networks |
usePublicClient | Get viem public client |
useWalletClient | Get viem wallet client |
| Signing | |
useSignMessage | Sign a message |
useSignTypedData | Sign EIP-712 typed data |
| ENS | |
useEnsName | Resolve address to ENS |
useEnsAddress | Resolve ENS to address |
useEnsAvatar | Get ENS avatar |
---
Best Practices
Loading States
function DataComponent() {
const { data, isLoading, isError, error } = useReadContract({...})
if (isLoading) return <Skeleton />
if (isError) return <Error message={error.message} />
if (!data) return null
return <div>{data.toString()}</div>
}Error Boundaries
import { useAccount, useConnect } from 'wagmi';
function WalletStatus() {
const { isConnected } = useAccount();
const { error } = useConnect();
if (error) {
// Handle specific errors
if (error.message.includes('User rejected')) {
return <div>Connection cancelled</div>;
}
return <div>Connection error: {error.message}</div>;
}
return <div>{isConnected ? 'Connected' : 'Not connected'}</div>;
}Refresh Data
import { useQueryClient } from '@tanstack/react-query';
import { useBalance } from 'wagmi';
function BalanceWithRefresh({ address }: { address: `0x${string}` }) {
const queryClient = useQueryClient();
const { data, queryKey } = useBalance({ address });
const refresh = () => {
queryClient.invalidateQueries({ queryKey });
};
return (
<div>
<span>{data?.formatted}</span>
<button onClick={refresh}>Refresh</button>
</div>
);
}Writing Transactions
Reference for sending transactions and writing to contracts using viem.
Simple ETH Transfer
Basic Transfer
import { createWalletClient, http, parseEther } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { mainnet } from 'viem/chains';
const account = privateKeyToAccount('0x...');
const client = createWalletClient({
account,
chain: mainnet,
transport: http(),
});
const hash = await client.sendTransaction({
to: '0xRecipient...',
value: parseEther('0.1'),
});
console.log(`Transaction hash: ${hash}`);With Gas Configuration
const hash = await client.sendTransaction({
to: '0x...',
value: parseEther('0.1'),
// EIP-1559 (recommended)
maxFeePerGas: parseGwei('50'),
maxPriorityFeePerGas: parseGwei('2'),
// Or legacy gas price
// gasPrice: parseGwei('50'),
gas: 21000n, // Optional: override gas limit
});---
Writing to Contracts
Basic Contract Write
import { createWalletClient, http, parseAbi, parseUnits } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { mainnet } from 'viem/chains';
const account = privateKeyToAccount('0x...');
const client = createWalletClient({
account,
chain: mainnet,
transport: http(),
});
const abi = parseAbi([
'function transfer(address to, uint256 amount) returns (bool)',
'function approve(address spender, uint256 amount) returns (bool)',
]);
const hash = await client.writeContract({
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC
abi,
functionName: 'transfer',
args: ['0xRecipient...', parseUnits('100', 6)],
});Using getContract
import { getContract } from 'viem';
const contract = getContract({
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
abi,
client: walletClient,
});
const hash = await contract.write.transfer(['0x...', parseUnits('100', 6)]);---
Simulate Before Sending
Always simulate contract calls to catch errors before spending gas.
simulateContract
import { createPublicClient, createWalletClient, http, parseAbi } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { mainnet } from 'viem/chains';
const account = privateKeyToAccount('0x...');
const publicClient = createPublicClient({
chain: mainnet,
transport: http(),
});
const walletClient = createWalletClient({
account,
chain: mainnet,
transport: http(),
});
const abi = parseAbi(['function transfer(address to, uint256 amount) returns (bool)']);
// Simulate the transaction
const { request, result } = await publicClient.simulateContract({
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
abi,
functionName: 'transfer',
args: ['0x...', parseUnits('100', 6)],
account,
});
console.log('Simulation result:', result); // true (return value)
// Execute if simulation succeeds
const hash = await walletClient.writeContract(request);Handle Simulation Errors
import { ContractFunctionRevertedError } from 'viem';
try {
const { request } = await publicClient.simulateContract({
address: '0x...',
abi,
functionName: 'transfer',
args: ['0x...', amount],
account,
});
const hash = await walletClient.writeContract(request);
} catch (error) {
if (error instanceof ContractFunctionRevertedError) {
console.error('Contract reverted:', error.reason);
}
throw error;
}---
Waiting for Confirmation
waitForTransactionReceipt
const hash = await walletClient.writeContract({
address: '0x...',
abi,
functionName: 'transfer',
args: ['0x...', amount],
});
// Wait for 1 confirmation (default)
const receipt = await publicClient.waitForTransactionReceipt({ hash });
console.log('Status:', receipt.status); // 'success' | 'reverted'
console.log('Block:', receipt.blockNumber);
console.log('Gas used:', receipt.gasUsed);With Options
const receipt = await publicClient.waitForTransactionReceipt({
hash,
confirmations: 3, // Wait for 3 block confirmations
timeout: 120_000, // 2 minute timeout
pollingInterval: 1_000, // Check every second
});Check Transaction Success
const receipt = await publicClient.waitForTransactionReceipt({ hash });
if (receipt.status === 'reverted') {
throw new Error('Transaction reverted');
}---
Gas Estimation
Estimate Gas for Transaction
const gas = await publicClient.estimateGas({
account,
to: '0x...',
value: parseEther('1'),
});
// Use with buffer
const hash = await walletClient.sendTransaction({
to: '0x...',
value: parseEther('1'),
gas: (gas * 110n) / 100n, // 10% buffer
});Estimate Gas for Contract Call
const gas = await publicClient.estimateContractGas({
address: '0x...',
abi,
functionName: 'transfer',
args: ['0x...', amount],
account,
});Get Current Gas Prices
// EIP-1559 fees (recommended)
const { maxFeePerGas, maxPriorityFeePerGas } = await publicClient.estimateFeesPerGas();
// Legacy gas price
const gasPrice = await publicClient.getGasPrice();Custom Gas Strategy
// Get fee data and add buffer
const feeData = await publicClient.estimateFeesPerGas();
const hash = await walletClient.sendTransaction({
to: '0x...',
value: parseEther('1'),
maxFeePerGas: (feeData.maxFeePerGas! * 120n) / 100n, // 20% buffer
maxPriorityFeePerGas: (feeData.maxPriorityFeePerGas! * 120n) / 100n,
});---
Nonce Management
Auto Nonce (Default)
viem automatically manages nonces:
// These will be sent with sequential nonces
const hash1 = await client.sendTransaction({ to: '0x...', value: parseEther('1') });
const hash2 = await client.sendTransaction({ to: '0x...', value: parseEther('1') });Manual Nonce
// Get current nonce
const nonce = await publicClient.getTransactionCount({ address: account.address });
// Send with specific nonce
const hash = await walletClient.sendTransaction({
to: '0x...',
value: parseEther('1'),
nonce,
});Batch Transactions with Manual Nonces
const baseNonce = await publicClient.getTransactionCount({
address: account.address,
});
// Send multiple transactions in parallel
const hashes = await Promise.all([
walletClient.sendTransaction({
to: '0xAddr1',
value: parseEther('1'),
nonce: baseNonce,
}),
walletClient.sendTransaction({
to: '0xAddr2',
value: parseEther('1'),
nonce: baseNonce + 1,
}),
walletClient.sendTransaction({
to: '0xAddr3',
value: parseEther('1'),
nonce: baseNonce + 2,
}),
]);---
Transaction Replacement
Speed Up Transaction
Replace a pending transaction with higher gas:
// Original transaction
const originalNonce = await publicClient.getTransactionCount({
address: account.address,
blockTag: 'latest', // Confirmed nonce
});
const pendingNonce = await publicClient.getTransactionCount({
address: account.address,
blockTag: 'pending', // Includes pending txs
});
// If there's a pending transaction
if (pendingNonce > originalNonce) {
// Speed up by resending with same nonce, higher gas
const hash = await walletClient.sendTransaction({
to: '0x...',
value: parseEther('1'),
nonce: originalNonce,
maxFeePerGas: parseGwei('100'), // Higher gas
maxPriorityFeePerGas: parseGwei('5'),
});
}Cancel Transaction
Send 0 ETH to yourself with the same nonce:
const nonce = await publicClient.getTransactionCount({
address: account.address,
blockTag: 'latest',
});
// Cancel by sending 0 ETH to self with higher gas
const hash = await walletClient.sendTransaction({
to: account.address,
value: 0n,
nonce,
maxFeePerGas: parseGwei('100'),
maxPriorityFeePerGas: parseGwei('5'),
});---
Raw Transaction Signing
Sign Without Sending
import { createWalletClient, http, parseEther, serializeTransaction } from 'viem';
// Prepare transaction
const request = await walletClient.prepareTransactionRequest({
to: '0x...',
value: parseEther('1'),
});
// Sign the transaction
const signedTx = await walletClient.signTransaction(request);
// Later: send the signed transaction
const hash = await walletClient.sendRawTransaction({
serializedTransaction: signedTx,
});---
EIP-4844 Blob Transactions
For L2 data availability:
import { stringToHex, toBlobs } from 'viem';
const blobs = toBlobs({ data: stringToHex('my data') });
const hash = await walletClient.sendTransaction({
blobs,
kzg, // KZG instance
maxFeePerBlobGas: parseGwei('10'),
to: '0x...',
});---
Error Handling
Common Errors
import {
InsufficientFundsError,
ContractFunctionExecutionError,
TransactionExecutionError,
UserRejectedRequestError,
} from 'viem';
try {
const hash = await walletClient.writeContract({
address: '0x...',
abi,
functionName: 'transfer',
args: ['0x...', amount],
});
} catch (error) {
if (error instanceof InsufficientFundsError) {
console.error('Not enough ETH for gas');
}
if (error instanceof ContractFunctionExecutionError) {
console.error('Contract error:', error.shortMessage);
}
if (error instanceof UserRejectedRequestError) {
console.error('User rejected the transaction');
}
if (error instanceof TransactionExecutionError) {
console.error('Transaction failed:', error.shortMessage);
}
}Retry Pattern
async function sendWithRetry(
client: WalletClient,
tx: Parameters<typeof client.sendTransaction>[0],
maxRetries = 3
) {
for (let i = 0; i < maxRetries; i++) {
try {
return await client.sendTransaction(tx);
} catch (error) {
if (i === maxRetries - 1) throw error;
// Increase gas on retry
tx = {
...tx,
maxFeePerGas: tx.maxFeePerGas ? (tx.maxFeePerGas * 120n) / 100n : undefined,
};
await new Promise((r) => setTimeout(r, 1000 * (i + 1)));
}
}
}Related skills
How it compares
Choose viem-integration when standardizing TypeScript agents on viem for accounts and signing rather than general Web3 architecture guidance.
FAQ
What does viem-integration cover for Ethereum development?
viem-integration is a Claude Code skill that documents viem account creation, private key and HD wallet usage, message signing, and WalletClient configuration in TypeScript for agents and scripts.
Which viem imports does the skill reference?
viem-integration references privateKeyToAccount from viem/accounts, createWalletClient and http from viem, and chain definitions such as mainnet from viem/chains for transaction-ready clients.
Is Viem Integration safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.