Архитектура dApp: фронт, кошелёк, нода, контракт
Полная карта: кто с кем разговаривает в dApp от клика пользователя до изменения состояния контракта.
Архитектура dApp — это цепочка «фронт ↔ кошелёк ↔ нода/провайдер ↔ смарт-контракт», где каждое звено отвечает за свою часть: интерфейс, ключи, связь с сетью и логику.
В прошлом разделе мы говорили о трёх слоях. Теперь соберём их в единую схему и проследим, как идёт каждый тип запроса. Без этой карты легко запутаться: почему чтение работает без кошелька, а запись — нет, и зачем вообще нужен и провайдер, и кошелёк одновременно.
Четыре участника
- Фронтенд — ваш код. Рисует UI, держит состояние, вызывает библиотеку (ethers.js/viem).
- Кошелёк — хранит ключи, подписывает, управляет выбранной сетью и аккаунтом. Внедряет в страницу объект
window.ethereum. - Провайдер / нода — точка входа в блокчейн. Через неё идут JSON-RPC-запросы. Это может быть нода самого кошелька или сторонний RPC (Infura, Alchemy).
- Смарт-контракт — код и состояние в блокчейне, исполняемые EVM.
Общая схема
+-----------+ +------------+ +-----------+ +-------------+
| Фронтенд | <----> | Кошелёк | <----> | Нода | <----> | Контракт |
| (React) | | (MetaMask) | | (RPC) | | (EVM) |
+-----------+ +------------+ +-----------+ +-------------+
UI/логика ключи/подпись связь с сетью состояниеДва маршрута: чтение и запись
Чтение (call) не меняет состояние, ничего не стоит и не требует подписи. Фронт может ходить прямо к ноде, минуя кошелёк:
Фронт --> Провайдер (нода) --> контракт.view() --> значение --> ФронтЗапись (transaction) меняет состояние, стоит газ и обязана быть подписана. Здесь без кошелька никак:
Фронт --> Кошелёк (подпись) --> Нода (broadcast) --> блок
контракт меняет состояние --> событие --> Фронт обновляет UIОтсюда ключевой вывод: чтение можно делать без подключённого кошелька (через публичную ноду), а запись — нет. Многие dApp показывают данные сразу, а кошелёк просят подключить только когда пользователь хочет что-то сделать.
Как работает под капотом
Когда фронт вызывает функцию контракта, ethers.js по ABI кодирует имя функции и аргументы в шестнадцатеричную строку (calldata). Для чтения эта строка уходит методом eth_call — нода исполняет вызов локально и сразу возвращает результат, ничего не записывая. Для записи строка кладётся в транзакцию, которую подписывает кошелёк методом eth_sendTransaction; нода рассылает её по сети, и только после попадания в блок состояние меняется.
Частые ошибки
- Требовать кошелёк для простого чтения. Лишний барьер: данные можно показать через публичный RPC до подключения.
- Путать ноду и кошелёк. Нода — связь с сетью; кошелёк — ключи и подпись. Это разные роли, хоть MetaMask и совмещает их.
- Ждать мгновенного обновления UI после записи. Состояние меняется только после включения транзакции в блок.
Итоги
- В dApp четыре участника: фронт, кошелёк, нода, контракт — у каждого своя зона ответственности.
- Чтение идёт через ноду без подписи и бесплатно; запись требует подписи кошелька и стоит газ.
- Показывать данные можно до подключения кошелька — это лучше для UX.