Архитектура 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.
Проверьте себя
1. Для какой операции кошелёк обязателен?
AЧтение view-функции
BЗапись (отправка транзакции)
CПолучение баланса через RPC
DЗагрузка ABI
2. Что делает нода (провайдер) в архитектуре dApp?
AХранит приватные ключи
BРисует интерфейс
CСлужит точкой входа в сеть для JSON-RPC-запросов
DПодписывает транзакции
3. Почему данные часто можно показать ещё до подключения кошелька?
AКошелёк не умеет читать
BЧтение идёт через ноду и не требует подписи
CКонтракт сам шлёт push
DДанные кэшируются в React