китаев.tech

viem + wagmi: где заканчивается viem и начинается wagmi

Разведёшь viem и wagmi: один и тот же balanceOf через publicClient.readContract и через useReadContract.

Два слоя одного стека

viem — TypeScript SDK для Ethereum: клиенты, readContract, writeContract, multicall, утилиты. Работает в Node, скриптах, бэкенде, React — где угодно.

wagmi — React-хуки и провайдеры поверх viem: кэш, рефетч, статус загрузки, коннект кошелька. Внутри хуков те же действия viem.

Не «viem или wagmi». В React-dApp обычно оба: wagmi для UI-слоя, viem — типы, ABI, chains и иногда прямые вызовы вне React.


Одна задача: balanceOf USDC

viem (скрипт / любой runtime)

typescript
import { createPublicClient, http, formatUnits } from "viem";
import { mainnet } from "viem/chains";

const publicClient = createPublicClient({
  chain: mainnet,
  transport: http("https://ethereum.publicnode.com"),
});

const usdcAddress = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
const holder = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045";

const erc20Abi = [
  {
    type: "function",
    name: "balanceOf",
    stateMutability: "view",
    inputs: [{ name: "account", type: "address" }],
    outputs: [{ type: "uint256" }],
  },
  {
    type: "function",
    name: "decimals",
    stateMutability: "view",
    inputs: [],
    outputs: [{ type: "uint8" }],
  },
] as const;

const [rawBalance, decimals] = await Promise.all([
  publicClient.readContract({
    address: usdcAddress,
    abi: erc20Abi,
    functionName: "balanceOf",
    args: [holder],
  }),
  publicClient.readContract({
    address: usdcAddress,
    abi: erc20Abi,
    functionName: "decimals",
  }),
]);

console.log(formatUnits(rawBalance, decimals));

wagmi (React-компонент)

Те же address / abi / functionName / args. Добавляются кэш и состояние запроса.

bash
npm install wagmi viem @tanstack/react-query
typescript
// config.ts
import { createConfig, http } from "wagmi";
import { mainnet } from "wagmi/chains";

export const config = createConfig({
  chains: [mainnet],
  transports: {
    [mainnet.id]: http("https://ethereum.publicnode.com"),
  },
});
tsx
// main.tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { WagmiProvider } from "wagmi";
import { config } from "./config";
import { UsdcBalance } from "./UsdcBalance";

const queryClient = new QueryClient();

export function App() {
  return (
    <WagmiProvider config={config}>
      <QueryClientProvider client={queryClient}>
        <UsdcBalance />
      </QueryClientProvider>
    </WagmiProvider>
  );
}
tsx
// UsdcBalance.tsx
import { useReadContract } from "wagmi";
import { formatUnits } from "viem";

const usdcAddress = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
const holder = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045";

const erc20Abi = [
  {
    type: "function",
    name: "balanceOf",
    stateMutability: "view",
    inputs: [{ name: "account", type: "address" }],
    outputs: [{ type: "uint256" }],
  },
  {
    type: "function",
    name: "decimals",
    stateMutability: "view",
    inputs: [],
    outputs: [{ type: "uint8" }],
  },
] as const;

export function UsdcBalance() {
  const balance = useReadContract({
    address: usdcAddress,
    abi: erc20Abi,
    functionName: "balanceOf",
    args: [holder],
  });

  const decimals = useReadContract({
    address: usdcAddress,
    abi: erc20Abi,
    functionName: "decimals",
  });

  if (balance.isLoading || decimals.isLoading) {
    return <p>Загрузка…</p>;
  }

  if (balance.error || decimals.error || balance.data == null || decimals.data == null) {
    return <p>Не удалось прочитать баланс</p>;
  }

  return <p>{formatUnits(balance.data, decimals.data)} USDC</p>;
}

useReadContract под капотом вызывает тот же readContract viem. Wagmi добавляет: isLoading / isError / data, рефетч, дедуп запросов через TanStack Query.


Как это работает

СлойОтвечает за
viemRPC, ABI, кодирование calldata, клиенты, типы
wagmiReact-хуки, коннект кошелька, кэш, ре-рендер UI
TanStack Queryкэш и статусы запросов, которые использует wagmi

Типичные соответствия:

viemwagmi
publicClient.readContractuseReadContract
publicClient.multicalluseReadContracts
walletClient.writeContractuseWriteContract
createWalletClient + requestAddressesuseConnect / useAccount
waitForTransactionReceiptuseWaitForTransactionReceipt

createConfig + http() — это конфиг транспортов/сетей для wagmi. По сути те же chain + transport, что ты задавал у createPublicClient.

Импорты mainnet можно брать из wagmi/chains или viem/chains — это одни и те же chain-объекты экосистемы wevm.


Если ты знаешь ethers.js

Старый React-стекСовременный
ethers + свой useEffect + ручной стейтwagmi-хуки
BrowserProvider в компонентеuseConnect / connectors
вызов contract.balanceOf в useEffectuseReadContract
нет встроенного кэшаTanStack Query через wagmi

RainbowKit и похожие UI-kit'ы для кошельков тоже сидят на wagmi → viem.


Частые ошибки

«Нужно выбрать: учу viem или wagmi» → для React-вакансий нужны оба. Без viem не поймёшь, что делают хуки; без wagmi будешь изобретать кэш и коннект кошелька.

Вызов createPublicClient в теле каждого компонента → для UI бери хуки wagmi. Прямой viem-клиент — в скриптах, API-роутах, тестах или редких местах вне React-дерева.

Забыли QueryClientProvider → wagmi v2 без TanStack Query не заведёт хуки чтения. Оба провайдера обязательны.


Что дальше

Раздел viem на этом закрыт. Имеет смысл вернуться к практике:

Материалы китаev.tech публикуются в образовательных целях и не являются инвестиционной рекомендацией. Примеры кода и описания протоколов — для обучения; использование в продакшне на ваш собственный риск. Дисклеймер и политика конфиденциальности.