dApp целиком: чтение, транзакция, события
- Зачем это нужно
- Чтение: подпись не нужна
- Проверка до отправки
- Сколько это будет стоить
- Отправка и подтверждения
- Три способа узнать о событии
- Собираем всё вместе
- Как объяснять отказы
- Типичные ошибки
- Слова главы
- Тренажёр: набрать вслепую
- На чемпионате
- Куда углубиться
- Экзамен по блоку «Кошелёк и dApp»
- Куда дальше: проверенные ресурсы
Зачем это нужно
Всё по отдельности уже есть: контракт развёрнут, интерфейс собран, кошелёк подключается, подписи проверяются. Осталось собрать это в работающее приложение и понять, что показывать пользователю на каждом шаге.
Транзакция — не вызов функции. Она проходит пять состояний, на каждом может отвалиться, и человек должен понимать, где именно всё встало. Это и есть разница между «работает» и «этим можно пользоваться».
Прогоны этой главы идут против настоящего узла из главы про свою сеть.
Чтение: подпись не нужна
── чтение: подпись не нужна ──
owner() : 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
rate() : 500
totalDeposits() : 0.0 ETH
это обычный запрос к узлу: ни газа, ни окна кошелька
── попытка записи без подписывающего ──
contract runner does not support sending transactions
const readOnly = new Contract(address, abi, provider); // читать
const writable = new Contract(address, abi, signer); // читать и писать
Отсюда правило, которое сильно улучшает первое впечатление: показывайте данные до подключения кошелька. Баланс хранилища, ставка, последние операции — всё это доступно любому. Пустой экран с одной кнопкой «подключить» выглядит гораздо хуже, чем работающий интерфейс, где кнопка нужна только для действия.
Проверка до отправки
── проверка ДО отправки: что будет, если выполнить ──
withdraw() не пройдёт: NothingToWithdraw
узнали, не потратив ни единицы газа и не открывая кошелёк
try {
await vault.withdraw.staticCall();
} catch (e) {
const err = vault.interface.parseError(e.data ?? "0x");
setError(explain(err?.name)); // NothingToWithdraw → «На счету пусто»
}
Это самый недооценённый приём во всём интерфейсе к контракту. Он превращает «транзакция отклонена, потрачено 0.0004 ETH» в «На счету пусто» — до того, как человек что-либо подтвердил.
Проверка не даёт гарантии: между ней и отправкой состояние сети может измениться. Она убирает предсказуемые отказы — те, что видны уже сейчас. Обрабатывать отказ после отправки всё равно надо.
Сколько это будет стоить
── сколько это будет стоить ──
оценка газа : 68332
цена газа : 451289466 wei
примерно : 0.000030837511790712 ETH
Показывать стоимость до подтверждения — правило хорошего тона: кошелёк покажет её и сам, но уже в своём окне и своими словами.
Обратите внимание на цифру дальше по прогону: фактически истрачено 67 481 при оценке 68 332. Оценка нарочно с запасом: она выполняется на текущем состоянии, а к моменту попадания в блок состояние может измениться, и вызов подорожает.
Отправка и подтверждения
── отправляем ──
хеш есть сразу : 0x6912a43d3b776c6cb6270367fe74725bf10b4188e2442c31c920a70e67843a22
блок пока : null ← ещё не в блоке
попала в блок : 7
статус : 1 (1 = исполнена)
газа истрачено : 67481 (оценка была 68332)
событий в чеке : 1
Deposited(0x70997970C51812dc3A010C7d01b50e0d17dc79C8, 2000000000000000000)
── ждём два подтверждения ──
блок транзакции : 7
высота сейчас : 8
Пять состояний кнопки, и каждое стоит показать: «проверяем» → «подтвердите в кошельке» → «отправлено, ждём блок» → «готово» либо «отклонено». Интерфейс, который просто зависает на несколько секунд, выглядит сломанным.
Три способа узнать о событии
── что поймала подписка ──
0x70997970… 2.0 ETH
(то же событие, но пришло само, без запроса)
── история событий с начала цепочки ──
блок 7 | 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 | 2.0 ETH
Из чека транзакции. Своё событие, сразу после подтверждения. Годится, чтобы показать «зачислено 2 ETH» ровно тому, кто нажимал.
Подпиской. Приходят сами, в том числе чужие. Так список операций пополняется, пока человек смотрит на экран.
Запросом истории. Всё, что было до открытия страницы. Список собирают из истории при загрузке и дополняют подпиской по ходу.
const past = await vault.queryFilter(vault.filters.Deposited(), 0, "latest");
await vault.on("Deposited", (who, amount) => addOp({ who, amount }));
Подписка по обычному соединению — это опрос, а не мгновенное уведомление: библиотека спрашивает узел раз в несколько секунд. В прогоне интервал был 4000 мс. Мгновенные уведомления бывают по постоянному соединению, но такое соединение надо отдельно открывать на узле.
Собираем всё вместе
async function deposit(amountText) {
const parsed = parseAmount(amountText);
if (!parsed.ok) return setError(parsed.error);
setStatus("checking");
try {
await vault.deposit.staticCall({ value: parsed.wei });
} catch (e) {
return setError(explain(vault.interface.parseError(e.data ?? "0x")?.name));
}
setStatus("signing");
let tx;
try {
tx = await vault.deposit({ value: parsed.wei });
} catch (e) {
if (e.code === 4001 || e.code === "ACTION_REJECTED") return setStatus("idle");
return setError("Не удалось отправить: " + (e.shortMessage ?? e.message));
}
setStatus("pending");
setTxHash(tx.hash);
const rc = await tx.wait();
setStatus(rc.status === 1 ? "done" : "reverted");
}
Шесть блоков, и каждый закрывает свою породу отказов: неверный ввод, предсказуемый откат, отказ человека, неудачная отправка, ожидание блока, откат в блоке.
Как объяснять отказы
Имена ошибок контракта — для программиста. Пользователю нужен словарь:
| ошибка контракта | что показать |
|---|---|
NothingToWithdraw | На счету пусто |
NotOwner | Это действие доступно только владельцу |
Expired | Срок разрешения истёк |
insufficient funds | Не хватает эфира на комиссию |
| код 4001 | ничего: человек сам отказался |
| код 4902 | Нужно добавить сеть — предложить кнопку |
Три правила:
- Словарь в одном месте. Не разбрасывать переводы по компонентам.
- Неизвестная ошибка тоже должна что-то говорить. «Не удалось: …» с текстом отказа плюс кнопка «повторить» лучше, чем молчание.
- Ссылку на обозреватель показывать всегда. Даже при откате: по хешу видно, что именно произошло.
Типичные ошибки
Кнопка не выключается на время операции. Три нажатия — три транзакции.
Нет проверки до отправки. Пользователь платит за отказ, который был виден заранее.
Отказ человека показывают как ошибку. Код 4001 — не ошибка.
Ждут только попадания в блок. Для настоящей сети нужны подтверждения.
Не проверяют статус чека. Транзакция в блоке, но откатилась — а интерфейс показывает «готово».
Список операций собирают только подпиской. После перезагрузки страницы он пуст.
Показывают внутренние имена ошибок. NothingToWithdraw ничего не говорит пользователю.
Обновляют баланс только по своей транзакции. Чужой вклад в хранилище на экране не появится.
Слова главы
- Контракт с провайдером — только чтение; с подписывающим — ещё и запись.
- Вызов «как будто» — выполнить у узла без отправки; показывает предсказуемый откат.
- Разбор отката — четыре байта ошибки превращаются в имя по ABI.
- Оценка газа — с запасом; факт обычно меньше.
- Хеш до блока — есть сразу, блок ещё нет.
- Чек — блок, статус, истраченный газ, события.
- Подтверждения — сколько блоков поверх того, в котором транзакция.
- Три источника событий — чек, подписка, история.
Тренажёр: набрать вслепую
Цель: скорость ≥ 95 зн/мин, точность ≥ 88%
await vault.deposit.staticCall({ value: wei });
На чемпионате
Пять состояний кнопки — это дёшево и очень заметно. Никаких библиотек, пять строк условий.
Проверка до отправки — отличительный признак. Показать, что приложение не даёт пользователю потратить газ впустую, — сильный ход на защите.
Ссылка на обозреватель по хешу. Даже в приватной сети: если поднят обозреватель, это выглядит законченно.
Список операций из истории плюс подписка. Перезагрузили страницу — список на месте. Пришла чужая операция — появилась сама.
Словарь ошибок в одном файле. Это и требование к интерфейсу, и просто удобно.
И порядок работы: сначала добейтесь, чтобы транзакция проходила и данные обновлялись. Состояния, объяснения и обозреватель — потом, но обязательно: именно они отличают работу от демонстрации.
Куда углубиться
- Постоянное соединение с узлом — мгновенные уведомления вместо опроса.
- Замена зависшей транзакции — та же очередь, большая цена газа.
- Оптимистичное обновление — показать результат до подтверждения и откатить при отказе.
- Пакетные вызовы чтения — один запрос вместо двадцати.
- Индексаторы — почему историю событий в настоящих приложениях берут не у узла.
Сводная проверка по всей главе: 8 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.
Экзамен по блоку «Кошелёк и dApp»
Три главы: подключение, подписи, полный путь транзакции.
Сводная проверка по 3 главам блока: 15 вопросов, 10:00 на всё, по одному вопросу, без возврата. Пересдавать можно сколько угодно раз.
Готовность: экзамены глав сданы 0 из 3. Блок можно сдавать и так, но шанс выше после всех глав.
Куда дальше: проверенные ресурсы
- ethers: контракты — чтение, запись, вызов «как будто», события.
- ethers: ожидание транзакции — чек и подтверждения.
- ethers: события контракта — подписка и запрос истории.
- Ethereum: жизненный цикл транзакции — что происходит между отправкой и блоком.
- MetaMask: обработка ошибок — коды и что они означают.
Комментарии
Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.