Запись в контракт: транзакция, газ, подтверждение
Запись в контракт: транзакция, газ, ожидание подтверждения и обработка отказа пользователя.
Транзакция (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) — нормальный сценарий, обрабатывайте отдельно.
- Состояние меняется только при включении транзакции в блок.