# Skill: wagmi
## Scope
- React/Next.js wallet integration with Wagmi v3 for EVM chains
- Contract interactions using viem v2 for address validation and transaction building
- Transaction state management and error handling
- Custom hooks wrapping wagmi for contract-specific interactions
Does NOT cover:
- Solana frontend development
- Backend RPC interactions
- Smart contract development
## Assumptions
- Wagmi v3.3.2+
- viem v2.44.4
- React 18+ or Next.js 14+
- TypeScript v5+ with strict mode
- TanStack Query v5+ (peer dependency of wagmi)
- WalletConnect v2+ (optional, for WalletConnect connector)
## Principles
- Use Wagmi v3.x hooks for wallet state (`useAccount`, `useWriteContract`, `useReadContract`, `useWaitForTransactionReceipt`, `useSimulateContract`)
- Use viem v2 for address validation (`getAddress`) and transaction utilities (`parseEther`, `parseGwei`)
- Create custom hooks wrapping wagmi for contract-specific interactions
- Handle connection states explicitly: disconnected, connecting, connected, reconnecting
- Validate addresses with `getAddress()` from viem before use (never cast directly as `Address`)
- Use generated contract ABIs and types from OpenAPI specs
- Use TanStack Query (via wagmi) for caching and refetching contract data
- Simulate contracts before writing to validate and estimate gas
- Use conditional queries with `enabled` flags to prevent unnecessary fetches
- Handle SSR properly with cookie storage for persistent wallet state
## Constraints
### MUST
- Use Wagmi v3.x (not v1 or v2) - v1/v2 patterns are incompatible
- Validate addresses with `getAddress()` from viem - never cast strings directly
- Handle SSR properly in Next.js (use `dynamic` with `ssr: false` for wallet components)
### SHOULD
- Create custom hooks for contract interactions
- Simulate contracts before writing (`useSimulateContract`)
- Use conditional queries with `enabled` flags
- Use cookie storage for SSR persistence
- Handle all connection states explicitly
### AVOID
- Wrapping generated hooks from OpenAPI clients unless necessary for abstraction
- Exposing private keys or sensitive wallet data in components
- Skipping address validation
## Interactions
- Uses generated contract ABIs/types from OpenAPI specs
- Complements [fastify](@cursor/skills/fastify-v5/SKILL.md) for API development
## Patterns
### Configuration Setup Pattern
Configure wagmi with chains, transports, connectors, and storage:
```tsx
import { createConfig, http } from 'wagmi'
import { mainnet, sepolia } from 'wagmi/chains'
import { injected, walletConnect } from 'wagmi/connectors'
import { cookieStorage, createStorage } from 'wagmi'
export const wagmiConfig = createConfig({
chains: [mainnet, sepolia],
connectors: [
injected(),
walletConnect({ projectId: process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID! }),
],
transports: {
[mainnet.id]: http(),
[sepolia.id]: http(),
},
storage: createStorage({
storage: cookieStorage,
}),
ssr: true, // Enable SSR support
multiInjectedProviderDiscovery: false, // Disable for better performance
})
```
**Key Configuration Options:**
- **`chains`**: Array of supported chains (import from `wagmi/chains`)
- **`connectors`**: Wallet connectors (injected, WalletConnect, Coinbase, etc.)
- **`transports`**: RPC providers per chain (use `http()` for public RPCs, or custom providers)
- **`storage`**: Use `cookieStorage` for SSR persistence, `localStorage` for client-only
- **`ssr`**: Enable SSR support for Next.js
- **`autoConnect`**: Automatically reconnect on mount (default: `true`)
**Provider Setup in Next.js:**
```tsx
'use client'
import { WagmiProvider } from 'wagmi'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { type ReactNode, useState } from 'react'
import { wagmiConfig } from '@/lib/wagmi-config'
export function Providers({ children }: { children: ReactNode }) {
const [queryClient] = useState(() => new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minute
},
},
}))
return (
{children}
)
}
```
### Custom Contract Hook Pattern
Create specialized hooks for contract interactions:
```tsx
import { useAccount, useWriteContract } from 'wagmi'
import { getAddress } from 'viem'
import type { Address } from 'viem'
export function useContractMint({ contractAddress }: { contractAddress: Address }) {
const { address: account } = useAccount()
const { writeContract, ...rest } = useWriteContract()
const mint = async (amount: bigint) => {
if (!account) throw new Error('Wallet not connected')
return writeContract({
address: getAddress(contractAddress), // Always validate
abi: ContractAbi,
functionName: 'mint',
args: [amount],
})
}
return { mint, ...rest }
}
```
### Address Validation Pattern
Always validate addresses before use:
```tsx
import { getAddress, type Address } from 'viem'
function validateAndUseAddress(rawAddress: string): Address {
try {
return getAddress(rawAddress) // Validates checksum and format
} catch (error) {
throw new Error('Invalid Ethereum address')
}
}
```
### Connection State Handling
Handle all wallet connection states:
```tsx
import { useAccount } from 'wagmi'
function WalletStatus() {
const { address, isConnected, isConnecting, isDisconnected, isReconnecting } = useAccount()
if (isDisconnected) return
if (isConnecting || isReconnecting) return
Connecting...
if (isConnected && address) return Connected: {address}
return null
}
```
### Explicit Connection Management
Use `useConnect` and `useDisconnect` for explicit connection control:
```tsx
import { useConnect, useDisconnect } from 'wagmi'
import { injected } from 'wagmi/connectors'
function WalletControls() {
const { connect, connectors, isPending } = useConnect()
const { disconnect } = useDisconnect()
const handleConnect = () => {
const connector = connectors.find(c => c.id === 'injected')
if (connector) connect({ connector })
}
return (
<>
{isPending ? 'Connecting...' : 'Connect Wallet'}
disconnect()}>Disconnect
>
)
}
```
### Chain Switching Pattern
Handle chain switching with error handling:
```tsx
import { useSwitchChain, useAccount } from 'wagmi'
import { sepolia } from 'wagmi/chains'
function SwitchChainButton() {
const { chain } = useAccount()
const { switchChain, isPending, error } = useSwitchChain()
const handleSwitch = () => {
switchChain({ chainId: sepolia.id })
}
if (chain?.id === sepolia.id) {
return Already on Sepolia
}
return (
<>
{isPending ? 'Switching...' : 'Switch to Sepolia'}
{error && Error: {error.message}
}
>
)
}
```
### Transaction Lifecycle Pattern
Complete transaction flow: simulate → write → wait → handle success/error:
```tsx
import { useSimulateContract, useWriteContract, useWaitForTransactionReceipt } from 'wagmi'
import { getAddress } from 'viem'
import type { Address } from 'viem'
function MintButton({ contractAddress, amount }: { contractAddress: Address; amount: bigint }) {
const { address } = useAccount()
// Step 1: Simulate transaction (gas estimation, validation)
const { data: simulateData, error: simulateError, isLoading: isSimulating } = useSimulateContract({
address: getAddress(contractAddress),
abi: ContractAbi,
functionName: 'mint',
args: [amount],
query: {
enabled: !!address && amount > 0n, // Only simulate when conditions met
},
})
// Step 2: Write transaction
const { writeContract, data: hash, error: writeError, isPending: isWriting } = useWriteContract()
// Step 3: Wait for transaction receipt
const { isLoading: isConfirming, isSuccess, error: receiptError } = useWaitForTransactionReceipt({
hash,
query: {
enabled: !!hash, // Only wait when hash exists
},
})
const handleMint = () => {
if (!simulateData) return
writeContract(simulateData.request)
}
const isLoading = isSimulating || isWriting || isConfirming
const error = simulateError || writeError || receiptError
return (
<>
{isLoading ? 'Processing...' : 'Mint'}
{error && Error: {error.message}
}
{isSuccess && Mint successful!
}
>
)
}
```
### Query Optimization Pattern
Use `enabled` flags and dependent queries to prevent unnecessary fetches:
```tsx
import { useReadContract, useAccount } from 'wagmi'
import { getAddress } from 'viem'
import type { Address } from 'viem'
function TokenBalance({ tokenAddress }: { tokenAddress: Address }) {
const { address } = useAccount()
// Only fetch when wallet is connected
const { data: balance, isLoading } = useReadContract({
address: getAddress(tokenAddress),
abi: ERC20Abi,
functionName: 'balanceOf',
args: [address!],
query: {
enabled: !!address, // Skip query if no address
staleTime: 30 * 1000, // Consider fresh for 30 seconds
refetchInterval: 60 * 1000, // Refetch every minute
},
})
if (!address) return Connect wallet to view balance
if (isLoading) return Loading...
return Balance: {balance?.toString()}
}
```
**Dependent Query Pattern:**
```tsx
function TokenAllowance({ tokenAddress, spender }: { tokenAddress: Address; spender: Address }) {
const { address } = useAccount()
// First query: get current allowance
const { data: allowance, isLoading: isLoadingAllowance } = useReadContract({
address: getAddress(tokenAddress),
abi: ERC20Abi,
functionName: 'allowance',
args: [address!, getAddress(spender)],
query: {
enabled: !!address,
},
})
// Second query: only fetch if allowance exists and is > 0
const { data: balance } = useReadContract({
address: getAddress(tokenAddress),
abi: ERC20Abi,
functionName: 'balanceOf',
args: [address!],
query: {
enabled: !!address && !!allowance && allowance > 0n,
},
})
return { allowance, balance, isLoadingAllowance }
}
```
### Multi-Step Flow Pattern
Handle sequential transactions (e.g., approve → execute):
```tsx
import { useReadContract, useSimulateContract, useWriteContract, useWaitForTransactionReceipt } from 'wagmi'
import { getAddress, parseUnits } from 'viem'
import type { Address } from 'viem'
function useTokenApproval({ tokenAddress, spender, amount }: { tokenAddress: Address; spender: Address; amount: string }) {
const { address } = useAccount()
const amountWei = parseUnits(amount, 18)
// Step 1: Check current allowance
const { data: allowance } = useReadContract({
address: getAddress(tokenAddress),
abi: ERC20Abi,
functionName: 'allowance',
args: [address!, getAddress(spender)],
query: {
enabled: !!address,
},
})
const needsApproval = !allowance || allowance < amountWei
// Step 2: Simulate approval if needed
const { data: approveSimulate, error: approveSimulateError } = useSimulateContract({
address: getAddress(tokenAddress),
abi: ERC20Abi,
functionName: 'approve',
args: [getAddress(spender), amountWei],
query: {
enabled: needsApproval && !!address,
},
})
// Step 3: Write approval
const { writeContract: writeApprove, data: approveHash, isPending: isApproving } = useWriteContract()
// Step 4: Wait for approval confirmation
const { isLoading: isApprovalConfirming, isSuccess: isApprovalSuccess } = useWaitForTransactionReceipt({
hash: approveHash,
query: {
enabled: !!approveHash,
},
})
// Step 5: Execute main transaction (only after approval succeeds)
const { data: executeSimulate, error: executeSimulateError } = useSimulateContract({
address: getAddress(spender),
abi: SpenderAbi,
functionName: 'execute',
args: [getAddress(tokenAddress), amountWei],
query: {
enabled: isApprovalSuccess && !!address,
},
})
const { writeContract: writeExecute, data: executeHash, isPending: isExecuting } = useWriteContract()
const { isLoading: isExecuteConfirming, isSuccess: isExecuteSuccess } = useWaitForTransactionReceipt({
hash: executeHash,
query: {
enabled: !!executeHash,
},
})
const handleApprove = () => {
if (approveSimulate) writeApprove(approveSimulate.request)
}
const handleExecute = () => {
if (executeSimulate) writeExecute(executeSimulate.request)
}
return {
needsApproval,
handleApprove,
handleExecute,
isApproving: isApproving || isApprovalConfirming,
isExecuting: isExecuting || isExecuteConfirming,
isApprovalSuccess,
isExecuteSuccess,
approveError: approveSimulateError,
executeError: executeSimulateError,
}
}
```
### Event Watching Pattern
Listen to contract events:
```tsx
import { useWatchContractEvent } from 'wagmi'
import { getAddress } from 'viem'
import type { Address } from 'viem'
import { useState } from 'react'
function useTokenTransferEvents({ tokenAddress }: { tokenAddress: Address }) {
const [transfers, setTransfers] = useState>([])
useWatchContractEvent({
address: getAddress(tokenAddress),
abi: ERC20Abi,
eventName: 'Transfer',
onLogs: (logs) => {
const newTransfers = logs.map((log) => ({
from: log.args.from!,
to: log.args.to!,
value: log.args.value!,
}))
setTransfers((prev) => [...prev, ...newTransfers])
},
})
return transfers
}
```
### Custom Viem Actions Pattern
Use client hooks for custom Viem actions:
```tsx
import { usePublicClient, useWalletClient } from 'wagmi'
import { useQuery, useMutation } from '@tanstack/react-query'
import { getLogs, watchAsset } from 'viem/actions'
import { getAddress } from 'viem'
import type { Address } from 'viem'
function useContractLogs({ address, fromBlock }: { address: Address; fromBlock?: bigint }) {
const publicClient = usePublicClient()
return useQuery({
queryKey: ['contractLogs', address, fromBlock, publicClient?.uid],
queryFn: async () => {
if (!publicClient) throw new Error('Public client not available')
return getLogs(publicClient, {
address: getAddress(address),
fromBlock,
})
},
enabled: !!publicClient && !!address,
})
}
function useWatchAsset() {
const walletClient = useWalletClient()
return useMutation({
mutationFn: async ({ address, symbol, decimals }: { address: Address; symbol: string; decimals: number }) => {
if (!walletClient.data) throw new Error('Wallet not connected')
return watchAsset(walletClient.data, {
type: 'ERC20',
options: {
address: getAddress(address),
symbol,
decimals,
},
})
},
})
}
```
### Error Handling Pattern
Comprehensive error handling with user-friendly messages:
```tsx
import { useWriteContract } from 'wagmi'
import { BaseError } from 'viem'
import { captureError } from '@repo/error/nextjs'
import type { WriteContractParameters } from 'wagmi/actions'
function useContractWriteWithErrorHandling() {
const { writeContract, error, isError } = useWriteContract()
const handleWrite = async (request: WriteContractParameters) => {
try {
const hash = await writeContract(request)
return { hash, error: null }
} catch (err) {
const error = err as BaseError
// User-friendly error messages
let userMessage = 'Transaction failed'
if (error.shortMessage?.includes('User rejected')) {
userMessage = 'Transaction cancelled by user'
} else if (error.shortMessage?.includes('insufficient funds')) {
userMessage = 'Insufficient balance for transaction'
} else if (error.shortMessage?.includes('gas')) {
userMessage = 'Gas estimation failed. Please try again.'
}
// Report error via @repo/error/nextjs
captureError({
code: 'TRANSACTION_ERROR',
error,
label: 'Contract Write',
data: { request },
})
return { hash: null, error: { message: userMessage, original: error } }
}
}
return { handleWrite, error, isError }
}
```
### SSR & Next.js Integration Pattern
Proper SSR handling with cookie storage and hydration-safe patterns:
```tsx
// lib/wagmi-config.ts
import { createConfig, cookieStorage, createStorage } from 'wagmi'
import { http } from 'wagmi'
import { mainnet } from 'wagmi/chains'
import { injected } from 'wagmi/connectors'
export const wagmiConfig = createConfig({
chains: [mainnet],
connectors: [injected()],
transports: {
[mainnet.id]: http(),
},
storage: createStorage({
storage: cookieStorage, // Use cookies for SSR persistence
}),
ssr: true,
})
```
```tsx
// components/wallet-button.tsx
'use client'
import dynamic from 'next/dynamic'
// Dynamically import wallet component with SSR disabled
const WalletButtonClient = dynamic(() => import('./wallet-button-client'), {
ssr: false,
loading: () => Loading wallet...
, // Skeleton during hydration
})
export function WalletButton() {
return
}
```
```tsx
// components/wallet-button-client.tsx
'use client'
import { useAccount, useConnect, useDisconnect } from 'wagmi'
import { injected } from 'wagmi/connectors'
import { useEffect, useState } from 'react'
export function WalletButtonClient() {
const [mounted, setMounted] = useState(false)
const { address, isConnected } = useAccount()
const { connect, connectors } = useConnect()
const { disconnect } = useDisconnect()
// Ensure component is mounted before accessing wallet APIs
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) {
return Loading...
// Prevent hydration mismatch
}
if (isConnected && address) {
return (
Connected: {address}
disconnect()}>Disconnect
)
}
return (
{
const connector = connectors.find((c) => c.id === 'injected')
if (connector) connect({ connector })
}}
>
Connect Wallet
)
}
```
## Trade-offs
- **Custom hooks vs direct wagmi hooks**: Custom hooks provide abstraction and type safety but add indirection. Use custom hooks for contract-specific logic, direct hooks for simple wallet state.
- **Address validation**: Always validate with `getAddress()` even if address comes from wagmi - provides runtime safety and checksum correction.
- **SSR handling**: Client-side only rendering (`ssr: false`) prevents hydration errors but may cause layout shift. Use cookie storage and skeleton loading states for better UX.
- **Simulation before write**: `useSimulateContract` adds an extra query but prevents failed transactions and improves UX. Always simulate before writing when possible.
- **Query optimization**: Using `enabled` flags adds complexity but prevents unnecessary network requests and improves performance.
- **Multi-step flows**: Sequential transaction handling (approve → execute) improves UX but requires careful state management and error handling at each step.
- **Event watching**: `useWatchContractEvent` provides real-time updates but can cause performance issues with high-frequency events. Consider debouncing or filtering.
- **Custom Viem actions**: Using `usePublicClient`/`useWalletClient` provides flexibility but requires manual query/mutation setup. Prefer built-in hooks when available.