Реализация взаимодействия с блокчейном через wagmi/viem на сайте
wagmi + viem — современный стандарт для Web3-фронтенда на React. wagmi предоставляет хуки (useAccount, useReadContract, useWriteContract), viem — низкоуровневый типизированный клиент. Связка заменяет ethers.js + react-query: меньше boilerplate, встроенный кэш, автоматический refetch по блокам.
Чем отличается от ethers.js
ethers.js — класс-ориентированная библиотека с мутабельным состоянием. new Contract(...) создаёт объект, который держит провайдер внутри себя. В React это проблема: при смене сети или аккаунта объект нужно пересоздавать.
viem строится на функциях и иммутабельных клиентах. wagmi управляет жизненным циклом клиентов автоматически — при смене аккаунта хуки автоматически обновляют данные.
// 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;
// Симуляция до подписи — выявляем ошибки без gas
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
# Автогенерация типизированных хуков
npx wagmi generate
После генерации появляются хуки вида useReadErc20BalanceOf(...) с полной типизацией аргументов — ошибка передачи неверного типа поймается на этапе компиляции.
Сроки: настройка wagmi/viem в проекте, подключение кошелька, чтение и запись в 1–2 контракта — 1–2 дня. Полный слой взаимодействия с типогенерацией, серверными клиентами и event-подпиской — 2–3 дня.







