Перейти к содержанию

Чтение смарт-контракта без подключения кошелька

Оглавление курса «Web3-интерфейс: реестр в браузере» · Урок 2 из 30.

Публичный клиент нужен, чтобы страница курса 17 читала состояние без доступа к кошельку посетителя. В предыдущем уроке мы разделили наблюдение и подпись; теперь реализуем первый путь. Результат — модуль, который явно выбирает локальную сеть, обращается к её RPC и возвращает согласованную пару значений из courses(17).

Учебный профиль серии закреплён: Node 22.14.0 для инструментов разработки, viem 2.37.9 для клиентского API, локальная EVM-сеть с идентификатором 31337 и контракт, собранный для Solidity 0.8.30/Cancun. Эти версии описывают наш пример, а не последние версии рынка. Установку зависимостей, запуск узла и чтение реального экземпляра здесь не выполняем; приведённые ответы — ожидаемые состояния будущей лаборатории.

Явное описание сети

Создадим client.js. Конфигурация сообщает библиотеке имя сети, обозначение её базовой денежной единицы и endpoint чтения. Указание chain полезно для типизации и для следующих операций, но само по себе не доказывает, что по HTTP-адресу отвечает именно нужный узел.

import { createPublicClient, defineChain, http } from 'viem';

export const local = defineChain({
  id: 31337,
  name: 'Library local',
  nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
  rpcUrls: { default: { http: ['http://127.0.0.1:8545'] } },
});

export const publicClient = createPublicClient({
  chain: local,
  transport: http('http://127.0.0.1:8545', { timeout: 8_000 }),
});

export async function assertReadNetwork() {
  const actual = await publicClient.getChainId();
  if (actual !== local.id) throw new Error('RPC другой сети');
}

127.0.0.1 относится к компьютеру, на котором работает браузер. Если такую страницу опубликовать для посетителей, она не начнёт читать ваш удалённый узел: посетитель будет обращаться к собственному loopback. Это адрес локального упражнения. Production-конфигурация использует отдельный endpoint и выбранную политику доступа, а также проверяет совместимость HTTPS-страницы и транспорта.

Тайм-аут ограничивает время ожидания ответа одного транспорта. Он не превращает недоступный узел в пустой каталог и не гарантирует завершение всей операции страницы за восемь секунд: приложение может выполнять несколько запросов. Политику повторов и общее время ожидания следует продумать отдельно. Особенно опасно повторять запросы записи под тем же правилом, что безвредное чтение.

Небольшой ABI для чтения

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

import { parseAbi } from 'viem';
import { publicClient, assertReadNetwork } from './client.js';

export const catalogAbi = parseAbi([
  'function publisher() view returns (address)',
  'function courses(uint256) view returns (bytes32 contentHash, bool active)',
]);

export async function readCourse(address, courseId = 17n) {
  await assertReadNetwork();
  const [contentHash, active] = await publicClient.readContract({
    address,
    abi: catalogAbi,
    functionName: 'courses',
    args: [courseId],
  });
  return { courseId, contentHash, active };
}

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

Getter публичного mapping возвращает два поля структуры. Если курс ещё не публиковался, типичный результат — нулевой bytes32 и false. Это не та же ситуация, что ошибка транспорта: в первом случае контракт ответил на корректный вызов, во втором ответа о состоянии вообще нет. Интерфейс должен различать «запись отсутствует» и «не удалось прочитать запись».

Превращение ответа в карточку

Полученный объект не следует немедленно называть выпуском только по номеру 17. Сначала определяем, ненулевой ли отпечаток. При ненулевом отпечатке и active = true показывается действующий выпуск. При том же отпечатке и false — сохранённая, но деактивированная запись. Нулевой отпечаток с false соответствует начальному состоянию именно нашего базового контракта.

Деактивация не удаляет contentHash. Поэтому пользователь может сравнить исторический файл с последним записанным отпечатком, даже если интерфейс больше не предлагает действующий материал. Такой вывод следует из реализации deactivate, а не из общего свойства всех контрактных реестров. Если позднее изменится контракт, классификацию состояния тоже нужно пересмотреть.

Запрос не требует отправителя с издательскими правами. Он использует публичное исполнение чтения и не меняет storage. Узел всё равно тратит вычислительные ресурсы на обработку, поэтому публичные RPC-сервисы имеют ограничения. «Не требует газа от читателя» не означает «можно послать неограниченное число запросов». Страница будет переиспользовать результаты и обновлять их по осмысленному событию.

Случаи для самостоятельного разбора

Рассмотрим три ожидаемых сценария. При доступном узле и известном контракте читатель видит корректную карточку без расширения кошелька. При закрытом локальном узле карточка сообщает о проблеме чтения и предлагает повторить запрос. При endpoint другой сети проверка getChainId прерывает чтение до обращения к реестру. Никакой из этих случаев не требует скрытого подключения аккаунта.

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

Для упражнения сохраните отдельно результаты проверки сети и чтения записи. Сформулируйте разные пользовательские сообщения для несовпадения сети, отсутствия записи и недоступного транспорта. Ожидаемый результат — страница остаётся понятной даже до появления подписи и редактирования. Семантика клиента и чтения приведена в документации Public Client и readContract.

Чтение из разных вкладок

Два посетителя могут получить разные наблюдения, если один запрос выполнен до публикации, а другой — после неё. Это ещё не доказательство неправильной работы клиента. Значение latest описывает состояние на момент обработки конкретного запроса. Когда карточка показывает номер наблюдаемого блока, разницу легче объяснить и воспроизвести в будущей лаборатории.

Если страница одновременно читает publisher, выпуск и номер блока отдельными запросами, между ними может появиться новый блок. Для нашего неизменяемого издателя это не меняет адрес, однако общую проблему согласованности лучше решить заранее. В уроке о снимке передадим один номер блока во все чтения и сохраним хеш наблюдения. Такое согласование имеет собственные ограничения при реорганизации, которые также нельзя спрятать за надписью «актуальные данные».

Сетевой запрос в браузере зависит ещё от доступа к endpoint. Локальный узел может не разрешать запросы с origin страницы. Ответ браузера о блокировке доступа не означает, что getter контракта откатился. Ошибки транспорта и ошибки EVM следует диагностировать отдельно. Настройка доступных origin относится к локальной инфраструктуре; отключать защиту браузера ради чтения не требуется.

Оглавление серии