Запись в контракт: транзакция, газ, подтверждение

Запись в контракт: транзакция, газ, ожидание подтверждения и обработка отказа пользователя.

Транзакция (write) — подписанный запрос на изменение состояния контракта; стоит газ, исполняется не мгновенно и необратим после включения в блок.

Запись — самая ответственная часть dApp: здесь тратятся деньги и меняются данные. Поток принципиально асинхронный: отправили — ждём — подтвердилось. Каждый этап нужно отразить в UI.

Жизненный цикл транзакции записи

const contract = new Contract(addr, abi, signer); // нужен Signer!

// 1) Отправка: ethers просит кошелёк подписать, пользователь подтверждает
const tx = await contract.setValue(42);
// сюда мы попадаем, когда транзакция ОТПРАВЛЕНА, но ещё НЕ в блоке
console.log("hash:", tx.hash);

// 2) Ожидание включения в блок
const receipt = await tx.wait();
// сюда — когда транзакция ПОДТВЕРЖДЕНА
console.log("в блоке:", receipt.blockNumber);

Два await — две разные точки. Первый — пользователь подписал и транзакция ушла. Второй (tx.wait()) — транзакция попала в блок. Между ними проходят секунды; именно здесь UI показывает «pending».

Обработка отказа пользователя

Пользователь может нажать «Reject» в кошельке. Это нормальный сценарий, не баг — обработайте его по коду ошибки ACTION_REJECTED (или числовой код 4001):

try {
  const tx = await contract.setValue(42);
  await tx.wait();
  // успех
} catch (e) {
  if (e.code === "ACTION_REJECTED" || e.code === 4001) {
    // пользователь отклонил — это не ошибка приложения
  } else {
    // настоящая ошибка: нехватка газа, revert контракта и т.п.
  }
}

Газ и оценка

Газ — плата за вычисления в сети, в нативной валюте сети (ETH, MATIC). ethers сам оценит газ (estimateGas) перед отправкой; кошелёк покажет пользователю примерную стоимость. Если контракт «отревертит» (например, не хватает прав), оценка газа упадёт ещё до подписи — это удобно: ошибка ловится заранее, без траты денег.

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

За contract.setValue(42) стоит цепочка: ethers кодирует вызов по ABI, через Signer просит кошелёк подписать транзакцию, кошелёк показывает окно и при подтверждении возвращает подписанную транзакцию, ethers рассылает её (eth_sendRawTransaction) и отдаёт вам объект с hash. tx.wait() опрашивает ноду, пока транзакция не окажется в блоке, и возвращает receipt с логами событий. Состояние контракта меняется только в момент включения в блок — до этого читать новые данные бесполезно.

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

  • Не вызывать tx.wait(). Тогда вы считаете запись завершённой, хотя она ещё в воздухе; UI обновится раньше времени.
  • Считать reject ошибкой. Отказ пользователя — штатный путь; не пугайте его красным «Ошибка!».
  • Звать write на контракте с Provider. Нужен Signer — иначе подписать некому.

Итоги

  • Запись требует Signer и проходит два этапа: отправка и подтверждение (tx.wait()).
  • Отказ пользователя (4001/ACTION_REJECTED) — нормальный сценарий, обрабатывайте отдельно.
  • Состояние меняется только при включении транзакции в блок.
Проверьте себя
1. Что означает попадание в первый await при contract.setValue(42)?
AТранзакция уже в блоке
BТранзакция подписана и отправлена, но ещё не подтверждена
CКонтракт прочитан
DПользователь отклонил
2. Как правильно трактовать ошибку с кодом 4001 / ACTION_REJECTED?
AСбой сети
BНехватка газа
CПользователь отклонил транзакцию — штатный сценарий
DНеверный ABI
3. Зачем нужен вызов tx.wait()?
AЧтобы подписать транзакцию
BЧтобы дождаться включения транзакции в блок
CЧтобы оценить газ
DЧтобы сменить сеть