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

Чтение реестра из браузера

Оглавление серии «Реестр» · Урок 6 из 10.

Локальный экземпляр определяется сетью и адресом. Теперь подключим чтение из браузера с помощью viem 2.37.9. Ожидаемый результат — по введённому хешу страница получит запись, покажет адрес издателя, время регистрации и признак отзыва. Кошелёк пока не нужен: чтение выполняется через Public Client и выбранный RPC. Это разделение полезно для сервиса проверки, где посетитель может рассмотреть документ, не связывая страницу со своей учётной записью кошелька.

Добавим библиотеку в package.json веб-части. Vite 7.1.7 и Node.js 22.14.0 сохраняют профиль предыдущих уроков. Зависимости устанавливает читатель самостоятельно; при написании это не выполнялось. Важно, что браузерная библиотека не читает Solidity-файл прямо из каталога src. Для вызова нужны адрес и ABI — описание доступных функций, их аргументов, результатов и ошибок. Исходник помогает понять поведение, но не заменяет этот договор передачи данных.

ABI описывает вызов

В abi.js используем parseAbi для собственного небольшого набора функций и событий. Getter возвращает tuple с именованными полями issuer, issuedAt и revoked. Имена помогают библиотеке дать понятное представление результата. ABI также включает пользовательские ошибки, чтобы последующие операции могли объяснять отказ. Этот набор соответствует контракту после четвёртого шага. Если изменить сигнатуру или размещённый исходник, определения и адрес нужно согласовать, а не продолжать вызовы по старой схеме.

import {parseAbi} from 'viem';
export const abi=parseAbi([
 'function owner() view returns (address)',
 'function issuers(address) view returns (bool)',
 'function get(bytes32) view returns ((address issuer, uint64 issuedAt, bool revoked))',
 'function issue(bytes32)', 'function revoke(bytes32)', 'function setIssuer(address,bool)',
 'event Issued(bytes32 indexed digest,address indexed issuer,uint64 issuedAt)',
 'event Revoked(bytes32 indexed digest,address indexed actor)',
 'event IssuerChanged(address indexed issuer,bool allowed)',
 'error NotAuthorized()', 'error InvalidDigest()', 'error AlreadyIssued()', 'error NotFound()', 'error AlreadyRevoked()'
]);

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

Public Client и выбранная цепочка

defineChain описывает учебную сеть 31337, её название и RPC. createPublicClient создаёт клиент с HTTP-транспортом к 127.0.0.1:8545. Данные конфигурации клиента сами по себе не заставляют удалённый узел работать в этой сети. Поэтому функция deployment отдельно вызывает getChainId и сравнивает ответ. Такой guard помогает заметить, что на том же порту оказался другой сервер или была запущена лаборатория с иной настройкой идентификатора.

Полный client.js ниже также получает deployment.json без кеширования, отвергает нулевой или неверный адрес и проверяет наличие кода. Все эти действия предшествуют getter. Нулевой адрес в архиве является намеренным заполнением для читателя. Если его не заменить, приложение должно остановиться с понятной причиной. Оно не должно обращаться к случайному общественному сервису или молча выбирать адрес из старого примера ради красивого результата.

import {createPublicClient,defineChain,http,isAddress} from 'viem';
import {abi} from './abi.js';
export const local=defineChain({id:31337,name:'Учебный Anvil',nativeCurrency:{name:'Учебный ETH',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')});
export async function deployment() {
 const response=await fetch('/deployment.json',{cache:'no-store'});if(!response.ok)throw Error('Не найден deployment.json');
 const config=await response.json();
 if(config.chainId!==31337 || !isAddress(config.address) || config.address==='0x0000000000000000000000000000000000000000')throw Error('Запишите локальный адрес контракта');
 if(await publicClient.getChainId()!==31337)throw Error('RPC не в учебной сети');
 const code=await publicClient.getCode({address:config.address});if(!code || code==='0x')throw Error('По этому адресу нет контракта');
 return config;
}
export function digestValue(input) {
 const digest=input.trim();if(!/^0x[0-9a-fA-F]{64}$/.test(digest) || /^0x0{64}$/.test(digest))throw Error('Нужен ненулевой bytes32');return digest;
}
export async function certificate(config,digest) {return publicClient.readContract({address:config.address,abi,functionName:'get',args:[digest]});}

Для digestValue проверяем текстовую форму ненулевого bytes32. Это ранняя проверка интерфейса. Она не заменяет правила issue внутри контракта, но помогает не отправлять заведомо неправильный ввод. Наличие 64 шестнадцатеричных символов не доказывает, что строка рассчитана из нужного файла: человек мог ввести произвольное значение. Проверка принадлежности файла появится в последнем уроке, где хеш будет вычисляться непосредственно из выбранного документа.

Чтение является отдельной операцией

readContract кодирует вызов get и выполняет чтение состояния через RPC. Он не вызывает окно подписи и не создаёт новой записи в реестре. Поэтому посетитель может читать независимо от выбранного кошелька. RPC всё равно является сетевой зависимостью: отключённый Anvil, неверный порт или повреждённая конфигурация приведут к ошибке. Обычный HTML и локальное вычисление файла могут работать при этом отдельно. Статус должен описывать конкретную неудавшуюся операцию, а не исчезать целиком вместе с карточкой.

issuedAt библиотека возвращает как BigInt. JSON не сериализует BigInt напрямую, поэтому интерфейс превращает это поле в строку перед JSON.stringify. Не стоит бездумно преобразовывать любые значения цепочки в Number: его безопасный целочисленный диапазон ограничен. Для нашей временной отметки привычное представление может быть удобно, но общее правило данных блокчейна должно сохранять точность. Строка в JSON делает границу передачи явной и сохраняет исходное целое значение.

Сначала наличие, затем статус

Как выяснили во втором уроке, отсутствующий ключ возвращает нулевой адрес и revoked == false. Интерфейс сначала сравнивает издателя с zeroAddress. Для такого результата показывает «Запись отсутствует». Только найденная запись может быть активной или отозванной. Не нужно считать любой false в поле отзыва признаком действительности. Это важный порядок предметных проверок, который останется одинаковым при ручном хеше, загруженном файле и просмотре истории событий.

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

Параметры среды и происхождение страницы

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

Если после перезапуска узла по адресу нет кода, guard должен сообщить об этом до декодирования результата. Если код есть, но getter несовместим с ABI, ошибка возникнет позднее. Эти случаи требуют разных действий: сначала восстановление нужного экземпляра, затем согласование интерфейса. Наличие одной универсальной фразы «ошибка сети» скрывало бы эту причинную связь. В лаборатории показываем сообщение библиотеки или собственного guard; продуктовый интерфейс сможет позже оформить причины более дружелюбно.

Ожидаемое чтение

Самостоятельно заполните deployment.json адресом своей лаборатории и введите ненулевой неизвестный хеш. Ожидается отсутствующая запись. После выдачи этого значения владельцем ожидаются ненулевой издатель и время блока. Нулевой конфиг должен быть отвергнут. Такие результаты в процессе написания не получались и не подставляются заранее. API описан в Public Client viem, readContract и parseAbi. Следующий урок добавит кошелёк как отдельный источник полномочий для изменения состояния, сохранив чтение независимым от его подключения.

Исходники этого шага

Архив серии содержит полную папку lesson-06. Возьмите именно её; переносить файлы из соседних папок не требуется. Версии и самостоятельная подготовка описаны в README. Пример, установка зависимостей и его выполнение при написании не запускались; результаты в тексте ожидаются по разбору исходников. Материал сохранён локально как Markdown-черновик.