Overview
MetaVault AI uses Wagmi v2 and Viem v2 for all Web3 interactions. This provides type-safe, React-friendly hooks for wallet connections, contract reads/writes, and event listening.Wagmi Configuration
Setup (src/config/wagmi.ts:1)
import { http, createConfig, createStorage, cookieStorage } from 'wagmi'
import { sepolia } from 'wagmi/chains'
import { injected, walletConnect } from 'wagmi/connectors'
const walletConnectProjectId = process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID || ''
const sepoliaRpcUrl = process.env.NEXT_PUBLIC_RPC_URL
export const config = createConfig({
// Supported chains
chains: [sepolia],
// Wallet connectors
connectors: [
// Browser extension wallets (MetaMask, Rabby, etc.)
injected({
shimDisconnect: true,
target() {
return {
id: 'injected',
name: 'Browser Wallet',
provider: typeof window !== 'undefined' ? window.ethereum : undefined,
}
},
}),
// WalletConnect v2 (client-side only)
...(typeof window !== 'undefined' ? [
walletConnect({
projectId: walletConnectProjectId,
metadata: {
name: 'MetaVault AI',
description: 'Intelligent DeFi Portfolio Management',
url: window.location.origin,
icons: [`${window.location.origin}/logo.png`],
},
showQrModal: true,
qrModalOptions: {
themeMode: 'dark',
themeVariables: {
'--wcm-z-index': '9999',
},
},
})
] : []),
],
// RPC transports
transports: {
[sepolia.id]: http(sepoliaRpcUrl)
},
// Cookie storage for SSR compatibility
storage: createStorage({
storage: cookieStorage,
}),
// Enable SSR
ssr: true,
// Batch multiple calls into single RPC request
batch: {
multicall: true,
},
})
- SSR Support: Cookie storage instead of localStorage
- Multiple Connectors: Injected and WalletConnect
- Multicall Batching: Reduces RPC calls
- Client-Side WalletConnect: Prevents SSR issues
Contract Abstractions
Contract Addresses (src/lib/contracts.ts:1)
import { Address } from "viem";
export const CONTRACTS = {
VAULT: (process.env.NEXT_PUBLIC_VAULT_ADDRESS || "0x0000000000000000000000000000000000000000") as Address,
ROUTER: (process.env.NEXT_PUBLIC_ROUTER_ADDRESS || "0x0000000000000000000000000000000000000000") as Address,
STRATEGY_AAVE: (process.env.NEXT_PUBLIC_STRATEGY_AAVE_ADDRESS || "0x0000000000000000000000000000000000000000") as Address,
STRATEGY_LEVERAGE: (process.env.NEXT_PUBLIC_STRATEGY_LEVERAGE_ADDRESS || "0x0000000000000000000000000000000000000000") as Address,
LINK: (process.env.NEXT_PUBLIC_LINK_ADDRESS || "0x0000000000000000000000000000000000000000") as Address,
};
export function isValidAddress(address: Address): boolean {
return address !== "0x0000000000000000000000000000000000000000" && address.startsWith("0x");
}
Contract ABIs
Vault ABI (src/lib/contracts.ts:19)
export const VAULT_ABI = [
{
inputs: [{ internalType: "uint256", name: "amount", type: "uint256" }],
name: "deposit",
outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
stateMutability: "nonpayable",
type: "function",
},
{
inputs: [{ internalType: "uint256", name: "shares", type: "uint256" }],
name: "withdraw",
outputs: [{ internalType: "uint256", name: "assetsOut", type: "uint256" }],
stateMutability: "nonpayable",
type: "function",
},
{
inputs: [],
name: "totalAssets",
outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "totalManagedAssets",
outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
stateMutability: "view",
type: "function",
},
{
inputs: [{ internalType: "address", name: "account", type: "address" }],
name: "balanceOf",
outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
stateMutability: "view",
type: "function",
},
{
inputs: [{ internalType: "uint256", name: "shares", type: "uint256" }],
name: "convertToAssets",
outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
stateMutability: "view",
type: "function",
},
{
inputs: [{ internalType: "address", name: "user", type: "address" }],
name: "userGrowthPercent",
outputs: [{ internalType: "int256", name: "", type: "int256" }],
stateMutability: "view",
type: "function",
},
] as const;
Router ABI (src/lib/contracts.ts:85)
export const ROUTER_ABI = [
{
inputs: [],
name: "rebalance",
outputs: [],
stateMutability: "nonpayable",
type: "function",
},
{
inputs: [],
name: "getPortfolioState",
outputs: [
{ internalType: "address[]", name: "strats", type: "address[]" },
{ internalType: "uint256[]", name: "balances", type: "uint256[]" },
{ internalType: "uint256[]", name: "targets", type: "uint256[]" },
],
stateMutability: "view",
type: "function",
},
] as const;
ERC20 ABI (src/lib/contracts.ts:131)
export const ERC20_ABI = [
{
inputs: [{ internalType: "address", name: "account", type: "address" }],
name: "balanceOf",
outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
stateMutability: "view",
type: "function",
},
{
inputs: [
{ internalType: "address", name: "spender", type: "address" },
{ internalType: "uint256", name: "amount", type: "uint256" },
],
name: "approve",
outputs: [{ internalType: "bool", name: "", type: "bool" }],
stateMutability: "nonpayable",
type: "function",
},
{
inputs: [
{ internalType: "address", name: "owner", type: "address" },
{ internalType: "address", name: "spender", type: "address" },
],
name: "allowance",
outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
stateMutability: "view",
type: "function",
},
] as const;
Wagmi Hooks Patterns
1. Wallet Connection
useAccount
import { useAccount } from 'wagmi'
function Component() {
const { address, isConnected, connector } = useAccount()
if (!isConnected) {
return <div>Please connect wallet</div>
}
return <div>Connected: {address}</div>
}
useConnect
import { useConnect } from 'wagmi'
function WalletButton() {
const { connect, connectors, isPending, error } = useConnect()
return (
<>
{connectors.map((connector) => (
<button
key={connector.id}
onClick={() => connect({ connector })}
disabled={isPending}
>
{connector.name}
</button>
))}
{error && <div>{error.message}</div>}
</>
)
}
useDisconnect
import { useDisconnect } from 'wagmi'
function DisconnectButton() {
const { disconnect } = useDisconnect()
return <button onClick={() => disconnect()}>Disconnect</button>
}
2. Reading Contract Data
useReadContract
import { useReadContract } from 'wagmi'
import { CONTRACTS, VAULT_ABI } from '@/lib/contracts'
function VaultBalance() {
const { data: totalAssets, isLoading, error } = useReadContract({
address: CONTRACTS.VAULT,
abi: VAULT_ABI,
functionName: 'totalAssets',
query: {
enabled: true,
refetchInterval: 5000, // Refetch every 5 seconds
},
})
if (isLoading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return <div>Total Assets: {totalAssets?.toString()}</div>
}
With Dynamic Arguments
const { address } = useAccount()
const { data: userShares } = useReadContract({
address: CONTRACTS.VAULT,
abi: VAULT_ABI,
functionName: 'balanceOf',
args: address ? [address] : undefined,
query: {
enabled: !!address, // Only run when address is available
},
})
Dependent Reads
const { data: userShares } = useReadContract({
address: CONTRACTS.VAULT,
abi: VAULT_ABI,
functionName: 'balanceOf',
args: [address],
})
const { data: userAssets } = useReadContract({
address: CONTRACTS.VAULT,
abi: VAULT_ABI,
functionName: 'convertToAssets',
args: userShares ? [userShares] : undefined,
query: {
enabled: !!userShares, // Only run after userShares is available
},
})
3. Writing to Contracts
useWriteContract
import { useWriteContract, useWaitForTransactionReceipt } from 'wagmi'
import { CONTRACTS, VAULT_ABI } from '@/lib/contracts'
import { parseTokenAmount } from '@/lib/utils'
function DepositButton() {
const { writeContract, data: hash, isPending, error } = useWriteContract()
const handleDeposit = () => {
const amount = parseTokenAmount('100') // 100 tokens
writeContract({
address: CONTRACTS.VAULT,
abi: VAULT_ABI,
functionName: 'deposit',
args: [amount],
})
}
return (
<button onClick={handleDeposit} disabled={isPending}>
{isPending ? 'Depositing...' : 'Deposit'}
</button>
)
}
Transaction Confirmation
const { writeContract, data: hash } = useWriteContract()
const { isLoading, isSuccess, isError } = useWaitForTransactionReceipt({
hash,
})
useEffect(() => {
if (isSuccess) {
console.log('Transaction confirmed!')
refetchData()
}
}, [isSuccess])
return (
<div>
{isLoading && <div>Confirming transaction...</div>}
{isSuccess && <div>Transaction successful!</div>}
{isError && <div>Transaction failed</div>}
</div>
)
Sequential Transactions (Approve + Deposit)
const [shouldDepositAfterApproval, setShouldDepositAfterApproval] = useState(false)
const { writeContract: approve, data: approveHash } = useWriteContract()
const { writeContract: deposit, data: depositHash } = useWriteContract()
const { isSuccess: isApprovalSuccess } = useWaitForTransactionReceipt({
hash: approveHash,
})
// Auto-deposit after approval
useEffect(() => {
if (isApprovalSuccess && shouldDepositAfterApproval) {
deposit({
address: CONTRACTS.VAULT,
abi: VAULT_ABI,
functionName: 'deposit',
args: [amount],
})
setShouldDepositAfterApproval(false)
}
}, [isApprovalSuccess, shouldDepositAfterApproval])
const handleDeposit = () => {
if (needsApproval) {
setShouldDepositAfterApproval(true)
approve({
address: CONTRACTS.LINK,
abi: ERC20_ABI,
functionName: 'approve',
args: [CONTRACTS.VAULT, amount],
})
} else {
deposit({ ... })
}
}
4. Event Listening
useWatchContractEvent
import { useWatchContractEvent } from 'wagmi'
import { CONTRACTS, VAULT_ABI } from '@/lib/contracts'
function VaultMonitor() {
const { refetch } = useReadContract({ ... })
useWatchContractEvent({
address: CONTRACTS.VAULT,
abi: VAULT_ABI,
eventName: 'Deposit',
onLogs: (logs) => {
console.log('Deposit detected:', logs)
refetch() // Refresh data
},
})
return <div>Monitoring deposits...</div>
}
Multiple Event Listeners
const refetchAll = () => {
refetchTotalAssets()
refetchUserShares()
}
useWatchContractEvent({
address: CONTRACTS.VAULT,
abi: VAULT_ABI,
eventName: 'Deposit',
onLogs: () => refetchAll(),
})
useWatchContractEvent({
address: CONTRACTS.VAULT,
abi: VAULT_ABI,
eventName: 'Withdraw',
onLogs: () => refetchAll(),
})
5. Chain Management
useChainId
import { useChainId } from 'wagmi'
import { sepolia } from 'wagmi/chains'
function NetworkIndicator() {
const chainId = useChainId()
const isCorrectNetwork = chainId === sepolia.id
if (!isCorrectNetwork) {
return <div>Please switch to Sepolia</div>
}
return <div>Connected to Sepolia</div>
}
useSwitchChain
import { useSwitchChain } from 'wagmi'
import { sepolia } from 'wagmi/chains'
function NetworkSwitcher() {
const { switchChain } = useSwitchChain()
return (
<button onClick={() => switchChain({ chainId: sepolia.id })}>
Switch to Sepolia
</button>
)
}
Auto Network Switch
const { isConnected } = useAccount()
const chainId = useChainId()
const { switchChain } = useSwitchChain()
useEffect(() => {
if (isConnected && chainId !== sepolia.id) {
const timer = setTimeout(() => {
switchChain?.({ chainId: sepolia.id })
}, 1000)
return () => clearTimeout(timer)
}
}, [isConnected, chainId])
Viem Utilities
Direct Client Usage
For operations not covered by Wagmi hooks:usePublicClient
import { usePublicClient } from 'wagmi'
function EventFetcher() {
const publicClient = usePublicClient()
const fetchEvents = async () => {
const logs = await publicClient.getContractEvents({
address: CONTRACTS.VAULT,
abi: ERC20_ABI,
eventName: 'Transfer',
fromBlock: 'earliest',
})
console.log('Transfer events:', logs)
}
return <button onClick={fetchEvents}>Fetch Events</button>
}
useWalletClient
import { useWalletClient } from 'wagmi'
function TransactionSender() {
const { data: walletClient } = useWalletClient()
const sendTransaction = async () => {
if (!walletClient) return
const hash = await walletClient.sendTransaction({
to: '0x...',
value: parseEther('0.01'),
})
console.log('Transaction sent:', hash)
}
return <button onClick={sendTransaction}>Send TX</button>
}
Utility Functions
Token Amount Conversion (src/lib/utils.ts:1)
import { formatUnits, parseUnits } from "viem";
export function formatTokenAmount(amount: bigint, decimals: number = 18): string {
return formatUnits(amount, decimals);
}
export function parseTokenAmount(amount: string, decimals: number = 18): bigint {
return parseUnits(amount, decimals);
}
import { formatTokenAmount, parseTokenAmount } from '@/lib/utils'
// BigInt to display string
const balance: bigint = 1500000000000000000n
const formatted = formatTokenAmount(balance, 18) // "1.5"
// User input to BigInt
const userInput = "100.5"
const amount = parseTokenAmount(userInput, 18) // 100500000000000000000n
Address Formatting
export function formatAddress(address: string): string {
if (!address) return "";
return `${address.slice(0, 6)}...${address.slice(-4)}`;
}
const address = "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb"
formatAddress(address) // "0x742d...bEb"
Number Formatting
export function formatNumber(num: number | string, decimals: number = 2): string {
const n = typeof num === "string" ? parseFloat(num) : num;
return n.toLocaleString(undefined, {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
});
}
Advanced Patterns
Polling vs Events
Polling (useReadContract with refetchInterval):const { data } = useReadContract({
...,
query: { refetchInterval: 5000 } // Poll every 5 seconds
})
useWatchContractEvent({
...,
onLogs: () => refetch() // Only refetch when event occurs
})
Optimistic Updates
const [optimisticBalance, setOptimisticBalance] = useState<bigint | null>(null)
const { data: actualBalance, refetch } = useReadContract({ ... })
const { writeContract } = useWriteContract()
const handleDeposit = (amount: bigint) => {
// Immediate UI update
setOptimisticBalance((prev) => (prev || 0n) + amount)
writeContract({ ... })
}
const { isSuccess } = useWaitForTransactionReceipt({ hash })
useEffect(() => {
if (isSuccess) {
refetch() // Get real balance from chain
setOptimisticBalance(null)
}
}, [isSuccess])
const displayBalance = optimisticBalance || actualBalance
Error Handling
const { data, error, isError } = useReadContract({ ... })
if (isError) {
if (error.name === 'ContractFunctionExecutionError') {
return <div>Contract call failed: {error.message}</div>
}
if (error.name === 'ChainMismatchError') {
return <div>Wrong network</div>
}
return <div>Unknown error</div>
}
Best Practices
1. Enable Queries Conditionally
const { data } = useReadContract({
...,
query: {
enabled: isConnected && !!address && hasValidContracts,
},
})
2. Refetch After Mutations
const { refetch } = useReadContract({ ... })
const { writeContract, data: hash } = useWriteContract()
const { isSuccess } = useWaitForTransactionReceipt({ hash })
useEffect(() => {
if (isSuccess) refetch()
}, [isSuccess])
3. Use as const for ABIs
export const VAULT_ABI = [
...
] as const // Enables full type inference
4. Batch RPC Calls
Enable multicall in config:const config = createConfig({
batch: { multicall: true },
})
5. SSR-Safe Rendering
const [isMounted, setIsMounted] = useState(false)
useEffect(() => setIsMounted(true), [])
if (!isMounted) return <Skeleton />
Common Issues
Issue: “window is not defined” in SSR
Solution: Use client-side only code:...(typeof window !== 'undefined' ? [walletConnect({ ... })] : [])
Issue: Hydration mismatch
Solution: Wait for mount:const [isMounted, setIsMounted] = useState(false)
useEffect(() => setIsMounted(true), [])
if (!isMounted) return null
Issue: Transaction reverts with no error
Solution: Check gas and approvals:const { data: allowance } = useReadContract({
functionName: 'allowance',
args: [owner, spender],
})
if (allowance < amount) {
// Approve first
}
Next Steps
Frontend Overview
Return to architecture overview
React Components
Explore component patterns