Запись через wagmi и состояния транзакции

Запись через wagmi: useWriteContract плюс ожидание подтверждения, и корректные состояния pending/success/error в UI.

useWriteContract — хук wagmi для отправки транзакций; в паре с useWaitForTransactionReceipt он покрывает весь путь от подписи до подтверждения.

Запись сложнее чтения: тут две асинхронные стадии (подписать и дождаться блока) и больше состояний. wagmi разбивает это на два хука, что удобно мапится на UI.

Отправка транзакции

import { useWriteContract, useWaitForTransactionReceipt } from "wagmi";

function Send() {
  const { writeContract, data: hash, isPending, error } = useWriteContract();

  const onClick = () => {
    writeContract({
      address: tokenAddress,
      abi,
      functionName: "transfer",
      args: [recipient, amount],
    });
  };

  // вторая стадия: ждём включения в блок
  const { isLoading: confirming, isSuccess } =
    useWaitForTransactionReceipt({ hash });

  return (
    <div>
      <button onClick={onClick} disabled={isPending}>
        {isPending ? "Подтвердите в кошельке…" : "Отправить"}
      </button>
      {confirming && <p>Ожидаем подтверждения…</p>}
      {isSuccess && <p>Готово!</p>}
      {error && <p>Ошибка или отклонено</p>}
    </div>
  );
}

Карта состояний UI

СостояниеЧто показать пользователю
isPending«Подтвердите в кошельке» — ждём подпись
есть hash, confirming«Транзакция отправлена, ждём блок» (+ссылка на explorer)
isSuccess«Готово» — обновить данные
errorразличить отказ (4001) и реальную ошибку

Главное правило: блокируйте кнопку на время isPending/confirming, иначе пользователь нажмёт дважды и отправит две транзакции.

Обновление данных после успеха

После isSuccess старые прочитанные данные устарели (баланс изменился). В wagmi их обновляют, инвалидируя кэш TanStack Query — тогда useReadContract перечитает свежее значение. Часто это делают в обработчике успеха транзакции.

Как работает под капотом

writeContract через viem Wallet Client просит кошелёк подписать и рассылает транзакцию, отдавая hash. useWaitForTransactionReceipt опрашивает ноду (или слушает через WebSocket), пока транзакция не попадёт в блок, и тогда выставляет isSuccess. Разделение на два хука повторяет два await из «голого» ethers (tx и tx.wait()), но в реактивной форме, удобной для рендера.

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

  • Не блокировать кнопку. Двойной клик = двойная транзакция и двойной газ.
  • Не дожидаться receipt перед обновлением UI. Покажете «успех», когда транзакция ещё в мемпуле.
  • Не инвалидировать кэш чтения. Баланс на экране останется старым после успешной записи.

Итоги

  • useWriteContract отправляет, useWaitForTransactionReceipt ждёт блок.
  • Мапьте pending/confirming/success/error на понятный UI и блокируйте кнопку.
  • После успеха инвалидируйте кэш чтений, чтобы показать свежие данные.
Проверьте себя
1. Какой хук ждёт включения транзакции в блок?
AuseWriteContract
BuseWaitForTransactionReceipt
CuseAccount
DuseReadContract
2. Почему важно блокировать кнопку на время isPending/confirming?
AДля красоты
BИначе двойной клик отправит две транзакции и спишет газ дважды
CЭто требование viem
DЧтобы скрыть hash
3. Что нужно сделать после успешной записи, чтобы баланс на экране обновился?
AПерезагрузить всю страницу всегда
BИнвалидировать кэш чтения (TanStack Query)
CСменить сеть
DПереподключить кошелёк