Чтение смарт-контракта без подключения кошелька
Оглавление курса «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 относится к локальной инфраструктуре; отключать защиту браузера ради чтения не требуется.