Разработка Web3-фронтенда и dApp-интерфейсов
Пользователь нажимает «Connect Wallet» — MetaMask открывается, подтверждает — и ничего не происходит. Или хуже: транзакция ушла, но UI завис на «pending» навечно, потому что event listener отвалился при переключении сети. Web3-фронтенд это не просто React + API вызовы. Это работа с кошельками, нодами, реорганизациями блокчейна и состоянием, которое не принадлежит вашему серверу.
Современный стек: wagmi v2 + viem
Wagmi v2 — React hooks для взаимодействия с EVM-чейнами. viem — низкоуровневый TypeScript клиент, заменивший ethers.js в большинстве новых проектов. Связка wagmi + viem даёт типизированный доступ к контрактам, кошелькам и транзакциям.
import { useReadContract, useWriteContract, useWaitForTransactionReceipt } from 'wagmi'
const { data: balance } = useReadContract({
address: contractAddress,
abi: erc20Abi,
functionName: 'balanceOf',
args: [userAddress],
})
const { writeContract, data: txHash } = useWriteContract()
const { isLoading: isConfirming } = useWaitForTransactionReceipt({ hash: txHash })
Типизация через viem — ABI передаётся как const assertion, и TypeScript знает типы аргументов и возвращаемых значений на уровне компиляции. Ошибки контракта ловятся до runtime.
Wallet connection: RainbowKit и WalletConnect
RainbowKit — UI библиотека поверх wagmi для wallet modal. Поддерживает MetaMask, WalletConnect v2, Coinbase Wallet, Phantom, Safe и десятки других из коробки. ConnectKit — альтернатива с другим дизайном. Оба решения правильно обрабатывают wallet detection, deep links для мобильных, и EIP-6963 (multi-injected wallet discovery).
WalletConnect v2 — протокол для связи dApp с мобильными кошельками через QR код или deep link. Требует ProjectID из cloud.walletconnect.com. При интеграции важно: WalletConnect v1 deprecated, миграция на v2 обязательна.
Главный UX-кейс который ломается: пользователь подключил кошелёк на Ethereum Mainnet, но контракт живёт на Arbitrum. Нужно:
- Детектировать неправильную сеть
- Предложить переключение через
wallet_switchEthereumChain - Если сеть не добавлена —
wallet_addEthereumChain - Дождаться подтверждения переключения перед отправкой транзакции
wagmi обрабатывает это через useSwitchChain(), но UX flow нужно проектировать явно — автоматическое переключение без объяснения пугает пользователей.
Мультичейн: один dApp, несколько сетей
Wagmi поддерживает мультичейн конфигурацию. Контракты на разных сетях имеют разные адреса, разные ABI (при апгрейдах), разные block times. Структура конфига:
const config = createConfig({
chains: [mainnet, arbitrum, optimism, polygon, base],
connectors: [injected(), walletConnect({ projectId }), coinbaseWallet()],
transports: {
[mainnet.id]: http(alchemyUrl),
[arbitrum.id]: http(arbitrumRpcUrl),
},
})
Адреса контрактов в типизированной map по chainId — не хардкодить отдельно для каждой сети.
Обработка транзакций: состояния и ошибки
Транзакция проходит несколько состояний: idle → pending (wallet) → submitted → confirming → confirmed. Каждый переход может прерваться с ошибкой.
Типичные ошибки и их обработка:
-
UserRejectedRequestError— пользователь отклонил в кошельке. Не показывать как «ошибку сервера», просто сбросить состояние. -
InsufficientFundsError— не хватает ETH/нативного токена на gas. Показать конкретную сумму. -
ContractFunctionRevertedError— контракт отреверчен. viem парсит custom errors из ABI и возвращает типизированную ошибку с args. - Транзакция dropped/replaced — пользователь ускорил транзакцию с тем же nonce.
useWaitForTransactionReceiptобрабатывает это черезonReplacedcallback.
Gas estimation failures нужно перехватывать до отправки транзакции. estimateGas() в viem выбросит ошибку с revert reason если транзакция заведомо зафейлится — показывать это пользователю лучше, чем дать ему потратить gas на failed транзакцию.
Чтение данных: multicall и кеширование
Один RPC запрос на каждый balanceOf при загрузке страницы с 20 токенами — 20 запросов. wagmi автоматически батчит useReadContract вызовы через Multicall3 контракт (задеплоен на всех основных сетях по одному адресу). Это снижает нагрузку на RPC и ускоряет загрузку.
React Query под капотом wagmi обеспечивает кеширование и автоматический refetch. Настройка staleTime и refetchInterval важна для баланса между актуальностью данных и нагрузкой на RPC.
Для сложных запросов — исторические данные, агрегация событий — The Graph subgraph или Ponder. GraphQL запрос к subgraph вместо сканирования тысяч блоков через RPC.
ENS и identity
normalize из viem для ENS — резолвинг .eth адресов и reverse lookup (адрес → ENS имя). Показывать vitalik.eth вместо 0xd8dA... где возможно. Avatar resolution — ENS avatar через getEnsAvatar().
Подписи и аутентификация: Sign-In with Ethereum
EIP-4361 (SIWE) — стандарт аутентификации через подпись кошелька без транзакции. Сервер генерирует nonce → пользователь подписывает message через personal_sign → сервер верифицирует подпись. Замена username/password для Web3 приложений. siwe npm пакет на клиенте и сервере.
Подписи для off-chain операций (EIP-712 typed data) — структурированные данные, которые MetaMask отображает human-readable вместо hex blob. Использовать для approve, order signatures в DEX, permit (ERC-2612).
Производительность и оптимизация
Бандл wagmi + viem + RainbowKit весит ~200–400kb gzipped. Для NextJS — dynamic imports с ssr: false для всех wallet-зависимых компонентов. Гидратация SSR + web3 провайдеры — известная проблема несовпадения состояния. Паттерн: рендерить connected state только на клиенте.
Стек и инструменты
Основные: React 18 + TypeScript, wagmi v2, viem, RainbowKit или ConnectKit Альтернативы: ethers.js v6 (legacy проекты), web3.js (не рекомендуется для новых) Запросы данных: React Query, The Graph (subgraph + Apollo), SWR Тестирование: Vitest + Testing Library, anvil (локальная нода) для интеграционных тестов Сети для разработки: anvil (Foundry), Hardhat Network с mainnet fork
Сроки
- Базовый dApp (чтение данных + одна транзакция): 2–3 недели
- Полноценный DeFi-интерфейс (swap, stake, dashboard): 6–10 недель
- NFT marketplace UI: 4–8 недель
- Кастомный wallet с мультичейн: 8–14 недель







