Реалізація взаємодії з блокчейном через wagmi/viem на веб-сайті
wagmi + viem — сучасний стандарт для Web3-фронтенду на React. wagmi надає hook'и (useAccount, useReadContract, useWriteContract), viem — низькорівневий типізований клієнт. Ця комбінація замінює ethers.js + react-query: менше boilerplate, вбудований кеш, автоматичний refetch по блокам.
Як це відрізняється від ethers.js
ethers.js — бібліотека, орієнтована на класи, з мінливим станом. new Contract(...) створює об'єкт, який тримає провайдер всередині себе. У React це проблема: коли мережа або рахунок змінюються, об'єкт потрібно пересоздавати.
viem побудований на функціях та незмінних клієнтах. wagmi управляє життєвим циклом клієнтів автоматично — коли рахунок змінюється, hook'и автоматично оновлюють дані.
// підхід ethers.js
const provider = new BrowserProvider(window.ethereum);
const contract = new Contract(address, abi, provider);
const balance = await contract.balanceOf(walletAddress);
// підхід viem/wagmi
const balance = await readContract(publicClient, {
address,
abi,
functionName: 'balanceOf',
args: [walletAddress],
});
Конфігурація
// lib/wagmi.ts
import { createConfig, http } from 'wagmi';
import { mainnet, arbitrum, base } from 'wagmi/chains';
import { injected, walletConnect } from 'wagmi/connectors';
export const config = createConfig({
chains: [mainnet, arbitrum, base],
connectors: [
injected(),
walletConnect({ projectId: process.env.NEXT_PUBLIC_WC_PROJECT_ID! }),
],
transports: {
[mainnet.id]: http(process.env.ETH_RPC_URL!),
[arbitrum.id]: http(process.env.ARBITRUM_RPC_URL!),
[base.id]: http(process.env.BASE_RPC_URL!),
},
});
// app/providers.tsx
'use client';
import { WagmiProvider } from 'wagmi';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { config } from '@/lib/wagmi';
const queryClient = new QueryClient();
export function Providers({ children }: { children: React.ReactNode }) {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</WagmiProvider>
);
}
Читання даних з контракту
// hooks/useTokenData.ts
import { useReadContracts } from 'wagmi';
import { erc20Abi, formatUnits } from 'viem';
export function useTokenData(tokenAddress: `0x${string}`, userAddress?: `0x${string}`) {
const { data, isLoading } = useReadContracts({
contracts: [
{ address: tokenAddress, abi: erc20Abi, functionName: 'name' },
{ address: tokenAddress, abi: erc20Abi, functionName: 'symbol' },
{ address: tokenAddress, abi: erc20Abi, functionName: 'decimals' },
{ address: tokenAddress, abi: erc20Abi, functionName: 'totalSupply' },
...(userAddress ? [{
address: tokenAddress,
abi: erc20Abi,
functionName: 'balanceOf' as const,
args: [userAddress] as [`0x${string}`],
}] : []),
],
query: {
refetchInterval: 30_000,
staleTime: 10_000,
},
});
const decimals = (data?.[2].result as number) ?? 18;
return {
isLoading,
name: data?.[0].result as string | undefined,
symbol: data?.[1].result as string | undefined,
decimals,
totalSupply: data?.[3].result
? formatUnits(data[3].result as bigint, decimals)
: undefined,
userBalance: userAddress && data?.[4]?.result
? formatUnits(data[4].result as bigint, decimals)
: undefined,
};
}
Запис до контракту
// hooks/useTokenTransfer.ts
import { useWriteContract, useWaitForTransactionReceipt, useSimulateContract } from 'wagmi';
import { erc20Abi, parseUnits } from 'viem';
import { useState } from 'react';
export function useTokenTransfer(tokenAddress: `0x${string}`, decimals: number) {
const [recipient, setRecipient] = useState<`0x${string}` | undefined>();
const [amount, setAmount] = useState('');
const amountWei = amount ? parseUnits(amount, decimals) : 0n;
// Симуляція до підпису — виявляємо помилки без газу
const { error: simError } = useSimulateContract({
address: tokenAddress,
abi: erc20Abi,
functionName: 'transfer',
args: [recipient!, amountWei],
query: { enabled: !!recipient && amountWei > 0n },
});
const { writeContract, data: txHash, isPending } = useWriteContract();
const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ hash: txHash });
const transfer = () => {
if (!recipient || amountWei === 0n) return;
writeContract({
address: tokenAddress,
abi: erc20Abi,
functionName: 'transfer',
args: [recipient, amountWei],
});
};
return {
recipient, setRecipient,
amount, setAmount,
simError,
transfer,
txHash,
isPending,
isConfirming,
isSuccess,
};
}
Прямий viem-клієнт для серверного використання
wagmi працює лише в React-контексті. Для API routes, server components, cron — прямий viem:
// lib/publicClient.ts
import { createPublicClient, http, createWalletClient } from 'viem';
import { mainnet } from 'viem/chains';
import { privateKeyToAccount } from 'viem/accounts';
export const publicClient = createPublicClient({
chain: mainnet,
transport: http(process.env.ETH_RPC_URL!),
});
// Серверний wallet-клієнт (для автоматичних транзакцій, relayer)
export const serverWallet = createWalletClient({
account: privateKeyToAccount(process.env.RELAYER_PRIVATE_KEY as `0x${string}`),
chain: mainnet,
transport: http(process.env.ETH_RPC_URL!),
});
Підписка на події через viem
// Polling для HTTP транспорту
import { watchContractEvent } from 'viem/actions';
const unwatch = watchContractEvent(publicClient, {
address: contractAddress,
abi: contractAbi,
eventName: 'Transfer',
onLogs: (logs) => {
for (const log of logs) {
console.log(log.args);
}
},
poll: true,
pollingInterval: 4_000,
});
// Для WebSocket транспорту — push, не polling
import { webSocket } from 'viem';
const wsClient = createPublicClient({
chain: mainnet,
transport: webSocket(process.env.ETH_WS_URL!),
});
Генерація типів з ABI
# Встановлення wagmi CLI
npm i -D @wagmi/cli
# Автогенерація типізованих hook'ів
npx wagmi generate
Після генерації з'являються hook'и вигляду useReadErc20BalanceOf(...) з повною типізацією аргументів — помилка передачі неправильного типу буде виявлена на етапі компіляції.
Часові рамки: налаштування wagmi/viem у проекті, підключення гаманця, читання та запис до 1–2 контрактів — 1–2 дні. Повний шар взаємодії з типогенерацією, серверними клієнтами та event-підпискою — 2–3 дні.







