Реальный пример: баланс и перевод токена
Финальный разбор: страница, которая показывает баланс токена и отправляет его — со всеми правильными состояниями и проверками.
Цель — собрать всё из курса в одну осмысленную страницу dApp: подключение, чтение баланса ERC-20, отправка с учётом decimals, статусы транзакции и проверки.
Соберём типичный экран DeFi-фронта: «мой баланс токена + форма перевода». Разберём по слоям, какие куски курса где работают. Код на wagmi/React — нерабочий в песочнице (нужны кошелёк и сеть), но показывает целостную картину.
Слой 1: подключение и определение пользователя
import { useAccount } from "wagmi";
import { ConnectButton } from "@rainbow-me/rainbowkit";
function App() {
const { address, isConnected } = useAccount();
return (
<div>
<ConnectButton />
{isConnected ? <TokenPanel user={address!} /> : <p>Подключите кошелёк</p>}
</div>
);
}Слой 2: чтение баланса с decimals
import { useReadContract } from "wagmi";
function useTokenBalance(user: `0x${string}`) {
const { data: raw } = useReadContract({
address: TOKEN, abi, functionName: "balanceOf", args: [user],
});
const { data: decimals } = useReadContract({
address: TOKEN, abi, functionName: "decimals",
});
return { raw, decimals };
}Форматирование сырого баланса — чистая логика, её можно проверить прямо здесь:
function formatUnits(raw, decimals) {
const base = 10n ** BigInt(decimals);
const whole = raw / base;
const frac = (raw % base).toString().padStart(Number(decimals), "0").replace(/0+$/, "");
return frac ? `${whole}.${frac}` : whole.toString();
}
// пример: баланс 1234560 у токена с decimals=6
console.log(formatUnits(1234560n, 6)); // 1.23456Вывод:
1.23456
Слой 3: отправка с проверками и статусами
import { useWriteContract, useWaitForTransactionReceipt, useChainId } from "wagmi";
import { parseUnits } from "viem";
function SendForm({ decimals }: { decimals: number }) {
const chainId = useChainId();
const { writeContract, data: hash, isPending, error } = useWriteContract();
const { isLoading: confirming, isSuccess } =
useWaitForTransactionReceipt({ hash });
const onSend = (to: `0x${string}`, human: string) => {
if (chainId !== EXPECTED_CHAIN) return alert("Переключите сеть");
const amount = parseUnits(human, decimals); // строка -> bigint, без float
writeContract({ address: TOKEN, abi, functionName: "transfer", args: [to, amount] });
};
return (
<button disabled={isPending || confirming} onClick={() => onSend(to, value)}>
{isPending ? "Подтвердите…" : confirming ? "Ждём блок…" : "Отправить"}
</button>
// + isSuccess: показать "Готово" и обновить баланс
// + error: различить reject (4001) и реальную ошибку
);
}Что где из курса сработало
| Кусок страницы | Тема курса |
| ConnectButton, useAccount | кошелёк, wagmi, RainbowKit |
| balanceOf + decimals + formatUnits | чтение контракта, числа, ERC-20 |
| parseUnits, bigint | wei/decimals, антиграбля float |
| проверка chainId | смена сети, мультичейн |
| isPending/confirming/success/error | состояния транзакции, обработка reject |
Как работает под капотом (полный путь клика)
Пользователь жмёт «Отправить». Фронт проверяет сеть, переводит человекочитаемую сумму в wei через parseUnits, wagmi через viem кодирует вызов transfer по ABI и просит кошелёк подписать. Пользователь подтверждает — транзакция уходит в мемпул (isPending закончился, есть hash). useWaitForTransactionReceipt ждёт включения в блок (confirming), затем выставляет isSuccess. Фронт инвалидирует кэш баланса — useReadContract перечитывает новое значение, и UI обновляется. Весь путь — это сумма всех уроков курса в одном клике.
Частые ошибки в такой странице
- Брать сумму из инпута как number и звать parseUnits(Number(...)). Потеря точности; передавайте строку.
- Не блокировать кнопку. Двойная отправка перевода.
- Не обновлять баланс после успеха. Пользователь видит старое число и думает, что перевод не прошёл.
Итоги
- Реальный экран dApp = подключение + чтение с decimals + запись с проверками и статусами.
- Каждая «мелочь» из курса (float, reject, сеть, decimals) здесь обязательна.
- Полный путь клика проходит через все слои: сеть → подпись → блок → обновление UI.