Перейти к основному содержимому
Блокчейн53ГЛАВА 53dApp целиком: чтение, транзакция, событияstaticCallsendwait(2)узелблок 7 · статус 1

dApp целиком: чтение, транзакция, события

Прочитано0%
Квизы0/4
Тренажёрыне пройден0/1
Уровень 1 · Новичок

Зачем это нужно​

Всё по отдельности уже есть: контракт развёрнут, интерфейс собран, кошелёк подключается, подписи проверяются. Осталось собрать это в работающее приложение и понять, что показывать пользователю на каждом шаге.

Транзакция — не вызов функции. Она проходит пять состояний, на каждом может отвалиться, и человек должен понимать, где именно всё встало. Это и есть разница между «работает» и «этим можно пользоваться».

Прогоны этой главы идут против настоящего узла из главы про свою сеть.

Чтение: подпись не нужна​

── чтение: подпись не нужна ──
owner() : 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
rate() : 500
totalDeposits() : 0.0 ETH
это обычный запрос к узлу: ни газа, ни окна кошелька

── попытка записи без подписывающего ──
contract runner does not support sending transactions
ДВА РЕЖИМА ОДНОГО КОНТРАКТАnew Contract(адрес, ABI, provider)читает: owner, rate, totalDepositsобычный запрос к узлуни газа, ни окна кошельказапись → contract runner does not supportnew Contract(адрес, ABI, signer)читает так жеи умеет отправлять транзакциикаждая — окно подтверждения у человекаи плата за газинтерфейс показывает данные сразу, до подключения кошелька: читать может кто угоднокнопка «подключить» нужна ровно тогда, когда пользователь собрался что-то изменить
Один и тот же контракт в двух режимах: с провайдером только читает, с подписывающим ещё и пишет
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
ПУТЬ ОДНОЙ КНОПКИ: ЧТО ПОКАЗЫВАТЬ НА КАЖДОМ ШАГЕпроверка вызоваstaticCall — выполнится лиNothingToWithdraw ловится здесьоценка стоимостиestimateGas + цена газа68 332 газа ≈ 0.00003 ETHподпись в кошелькечеловек подтверждаетможет отказаться: код 4001отправленохеш есть, блока ещё нетблок: nullподтвержденопопала в блокблок 7, статус 1, газ 67 481оценка газа оказалась чуть больше факта: 68 332 против 67 481 — запас закладывается нарочно
Пять состояний одной кнопки — и на каждом человеку надо показать своё
Нажми на часть выражения

Пять состояний кнопки, и каждое стоит показать: «проверяем» → «подтвердите в кошельке» → «отправлено, ждём блок» → «готово» либо «отклонено». Интерфейс, который просто зависает на несколько секунд, выглядит сломанным.

Отвечено вопросов: 0 из 2
Что даёт вызов «как будто» перед отправкой?
Оценка газа 68 332, истрачено 67 481. Это ошибка оценки?
Отвечено верно: 0 из 2

Три способа узнать о событии​

── что поймала подписка ──
0x70997970… 2.0 ETH
(то же событие, но пришло само, без запроса)

── история событий с начала цепочки ──
блок 7 | 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 | 2.0 ETH
ТРИ СПОСОБА УЗНАТЬ, ЧТО СОБЫТИЕ СЛУЧИЛОСЬиз чека транзакциисвоё событие, сразу после подтверждениясобытий в чеке: 1подпискойчужие события тоже; приходят самиопрос узла каждые 4000 мсзапросом историивсё, что было раньшеблок 7 | 0x70997970… | 2.0 ETHсписок операций собирают из истории при загрузке и дополняют подпиской по ходу работыподписка по обычному соединению — это опрос узла, а не push: узел спрашивают раз в несколько секунд
Из чека — своё; подпиской — чужие; запросом — всё, что было раньше

Из чека транзакции. Своё событие, сразу после подтверждения. Годится, чтобы показать «зачислено 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Нужно добавить сеть — предложить кнопку

Три правила:

  • Словарь в одном месте. Не разбрасывать переводы по компонентам.
  • Неизвестная ошибка тоже должна что-то говорить. «Не удалось: …» с текстом отказа плюс кнопка «повторить» лучше, чем молчание.
  • Ссылку на обозреватель показывать всегда. Даже при откате: по хешу видно, что именно произошло.
Отвечено вопросов: 0 из 2
Откуда берут список прошлых операций при открытии страницы?
Статус чека равен нулю. Что это значит?
Отвечено верно: 0 из 2

Типичные ошибки​

Кнопка не выключается на время операции. Три нажатия — три транзакции.

Нет проверки до отправки. Пользователь платит за отказ, который был виден заранее.

Отказ человека показывают как ошибку. Код 4001 — не ошибка.

Ждут только попадания в блок. Для настоящей сети нужны подтверждения.

Не проверяют статус чека. Транзакция в блоке, но откатилась — а интерфейс показывает «готово».

Список операций собирают только подпиской. После перезагрузки страницы он пуст.

Показывают внутренние имена ошибок. NothingToWithdraw ничего не говорит пользователю.

Обновляют баланс только по своей транзакции. Чужой вклад в хранилище на экране не появится.

Отвечено вопросов: 0 из 2
Подписка на события по обычному HTTP-соединению — это…
Почему имена ошибок контракта не показывают пользователю как есть?
Отвечено верно: 0 из 2

Слова главы​

  • Контракт с провайдером — только чтение; с подписывающим — ещё и запись.
  • Вызов «как будто» — выполнить у узла без отправки; показывает предсказуемый откат.
  • Разбор отката — четыре байта ошибки превращаются в имя по ABI.
  • Оценка газа — с запасом; факт обычно меньше.
  • Хеш до блока — есть сразу, блок ещё нет.
  • Чек — блок, статус, истраченный газ, события.
  • Подтверждения — сколько блоков поверх того, в котором транзакция.
  • Три источника событий — чек, подписка, история.

Тренажёр: набрать вслепую​

Цель: скорость ≥ 95 зн/мин, точность ≥ 88%

await vault.deposit.staticCall({ value: wei });

На чемпионате​

Пять состояний кнопки — это дёшево и очень заметно. Никаких библиотек, пять строк условий.

Проверка до отправки — отличительный признак. Показать, что приложение не даёт пользователю потратить газ впустую, — сильный ход на защите.

Ссылка на обозреватель по хешу. Даже в приватной сети: если поднят обозреватель, это выглядит законченно.

Список операций из истории плюс подписка. Перезагрузили страницу — список на месте. Пришла чужая операция — появилась сама.

Словарь ошибок в одном файле. Это и требование к интерфейсу, и просто удобно.

И порядок работы: сначала добейтесь, чтобы транзакция проходила и данные обновлялись. Состояния, объяснения и обозреватель — потом, но обязательно: именно они отличают работу от демонстрации.

Куда углубиться​

  • Постоянное соединение с узлом — мгновенные уведомления вместо опроса.
  • Замена зависшей транзакции — та же очередь, большая цена газа.
  • Оптимистичное обновление — показать результат до подтверждения и откатить при отказе.
  • Пакетные вызовы чтения — один запрос вместо двадцати.
  • Индексаторы — почему историю событий в настоящих приложениях берут не у узла.

Сводная проверка по всей главе: 8 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.

Экзамен по блоку «Кошелёк и dApp»​

Три главы: подключение, подписи, полный путь транзакции.

Экзамен по блоку: Кошелёк и dApp

Сводная проверка по 3 главам блока: 15 вопросов, 10:00 на всё, по одному вопросу, без возврата. Пересдавать можно сколько угодно раз.

Готовность: экзамены глав сданы 0 из 3. Блок можно сдавать и так, но шанс выше после всех глав.

Куда дальше: проверенные ресурсы​

Видео по теме

ВидеоSolidity и смарт-контракты Ethereum, урок #7 | События, модификаторы, require/revert и тестыIlya Krukowski
ВидеоSolidity и смарт-контракты Ethereum, урок #43 | Ethers.js v6 - апдейты, BigInt, перегрузка, импортыIlya Krukowski
ВидеоРабота со смарт-контрактами Ethereum с помощью Ethers.jsIlya Krukowski
ВидеоЧто такое dApps? Обзор и примеры с анимациейWhiteboard Crypto на русском
ВидеоHow to Speed Up or Cancel a Pending Transaction in MetaMaskData Slayer

Комментарии

Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.