Web3-фронтенд: проверка сети и адреса контракта
Оглавление курса «Web3-интерфейс: реестр в браузере» · Урок 3 из 30.
Вызов courses(17) имеет смысл только для конкретного экземпляра реестра в конкретной сети. Два контракта могут поддерживать одинаковые методы и хранить совершенно разные выпуски. В этом уроке выделим конфигурацию развёртывания в самостоятельный объект и введём проверки, которые выполняются до отображения карточки.
Предполагается модуль публичного клиента из предыдущего урока. Результат — приложение не путает адрес издателя с адресом реестра и не принимает новый локальный экземпляр за продолжение старого. Код подготовлен для чтения и будущей локальной работы; в данной серии примеры не исполнялись. Учебный профиль остаётся viem 2.37.9 и сетью 31337.
Адрес без сети недостаточен
Адрес EVM — двадцать байтов, которые интерфейс обычно показывает шестнадцатеричной строкой. Эти байты не содержат встроенного названия сети. Совпадение строки в двух сетях не доказывает совпадение контракта, исходников или состояния. Поэтому ссылки на explorer, кеши и журналы операций должны нести идентификатор сети вместе с адресом.
Локальная сеть добавляет ещё одну тонкость. После удаления её данных новая цепочка может снова получить chainId = 31337. Это удобное учебное значение, а не всемирный уникальный идентификатор каждого запуска. Если повторное развёртывание происходит в похожих условиях, возможен тот же адрес. Историю старого запуска нельзя автоматически соединять с новой только по паре «31337 плюс адрес».
Выделим поле environmentId. Оно относится к приложению, а не к стандарту Ethereum. При новой лаборатории меняется запись конфигурации и поколение данных. Для более строгой привязки можно сохранять хеш известного блока развёртывания, его номер и ожидаемый байткод. Такие значения берутся из собственного процесса развёртывания; произвольные выдуманные хеши не обеспечивают никакой проверки.
Конфигурация экземпляра
Файл deployment.js принимает данные конфигурации через функцию. Адрес здесь вводится при подключении подготовленного локального экземпляра, поэтому в листинге нет правдоподобного, но неизвестного сетевого адреса.
import { getAddress, isAddress } from 'viem';
export function normalizeDeployment(raw) {
if (raw.chainId !== 31337) throw new Error('Ожидается локальная сеть');
if (!isAddress(raw.catalogAddress)) throw new Error('Некорректный адрес');
if (!raw.environmentId || typeof raw.environmentId !== 'string') {
throw new Error('Нужен идентификатор лаборатории');
}
return Object.freeze({
chainId: raw.chainId,
catalogAddress: getAddress(raw.catalogAddress),
environmentId: raw.environmentId,
abiVersion: 'CourseCatalog.base.v1',
});
}
Функция проверяет форму и фиксирует выбранный профиль. Она не доказывает наличие контракта по адресу. getAddress нормализует представление адреса, но не обращается в сеть и не подтверждает полномочия владельца. Эти различия полезно удерживать в голове: формат, наличие кода и происхождение кода являются отдельными вопросами.
Object.freeze защищает объект от случайного изменения в обычном коде приложения. Посетитель всё равно контролирует свой браузер, поэтому это не механизм безопасности контракта. Польза здесь инженерная: модуль чтения и модуль подписи получают одну неизменяемую конфигурацию и реже расходятся из-за незаметного присваивания.
Проверка кода и издателя
Добавим сетевую часть проверки. Полный модуль использует ранее определённые publicClient и catalogAbi. Ожидаемый издатель поступает из подготовленного журнала развёртывания, а не из формы посетителя.
import { getAddress } from 'viem';
import { publicClient } from './client.js';
import { catalogAbi } from './catalog.js';
export async function inspectDeployment(deployment, expectedPublisher) {
if (await publicClient.getChainId() !== deployment.chainId) {
throw new Error('Сеть конфигурации и RPC различаются');
}
const code = await publicClient.getCode({ address: deployment.catalogAddress });
if (!code || code === '0x') throw new Error('По адресу нет кода');
const publisher = await publicClient.readContract({
address: deployment.catalogAddress,
abi: catalogAbi,
functionName: 'publisher',
});
if (getAddress(publisher) !== getAddress(expectedPublisher)) {
throw new Error('Неожиданный издатель');
}
return { deployment, publisher };
}
Наличие непустого байткода сообщает, что адрес содержит код. Совпадение getter издателя сообщает, что ответ соответствует ожидаемому адресу. Эти проверки уменьшают число обычных ошибок, но не заменяют сопоставление байткода с исходниками. Злоумышленный контракт мог бы вернуть такой же publisher и выполнять другую запись. Для процесса выпуска нужны исходники, параметры компиляции и запись экземпляра, а интерфейс использует их результат.
Если реестр станет прокси, проверка кода адреса прокси также не описывает текущую реализацию целиком. Появятся полномочия обновления и отдельная история реализации. Базовый CourseCatalog в этой серии не является прокси, поэтому не добавляем неподтверждённые предположения о слотах и администраторах.
Поведение страницы при смене экземпляра
Допустим, после перезапуска лаборатории адрес реестра изменился. Старый кеш курса 17 должен перестать быть текущим. Ключ чтения включает environmentId, chainId, адрес и номер курса. История отправленных операций также сохраняет эти поля. Иначе страница может показать старую успешную публикацию рядом с новым пустым контрактом и создать впечатление потери данных.
Не следует исправлять эту ситуацию автоматическим перебором всех возможных адресов пользователя. Явная запись развёртывания делает происхождение карточки понятным и ограничивает число запросов. Если конфигурация отсутствует, интерфейс сообщает о необходимости выбрать экземпляр; он не придумывает адрес из подключённого аккаунта.
Разберите вручную четыре входа: корректный экземпляр, адрес без кода, контракт с другим издателем и правильный адрес в другой сети. Для каждого запишите, на какой проверке останавливается последовательность. Ожидаемый итог — разные причины отказа и отсутствие попытки записи в неопределённый экземпляр. Операции адресов и чтения кода описаны в getAddress и getCode.
Сопоставление записи с интерфейсом
Сохраните рядом с конфигурацией название исходного контракта, версию компилятора, целевую EVM и дату подготовки лаборатории. Эти сведения помогают восстановить пример, но не должны автоматически превращаться в баннер «контракт проверен». Статус проверки имеет смысл только вместе с методом, исполнителем и конкретным результатом. В нашем локальном курсе запуск и проверка байткода ещё не выполнены, поэтому интерфейс сообщает только о назначении конфигурации.
Для внешних сетей запись развёртывания следует получать из контролируемого источника приложения, а пользовательское переключение сети ограничивать известными вариантами. Произвольный endpoint из URL страницы опасен не только из-за ложных ответов: браузер начинает обращаться к указанному ресурсу и может раскрывать историю запросов. Учебная конфигурация принимает адрес экземпляра, но не предоставляет посетителю универсальный сетевой прокси.
Полезно также различать обновление экземпляра и изменение выпуска в прежнем экземпляре. В первом случае меняются ключи кеша и история лаборатории. Во втором остаётся прежний адрес, но publish записывает другой отпечаток курса 17. Если при каждом выпуске менять environmentId, интерфейс потеряет связность истории; если не менять его при новой цепочке, смешает независимые данные. Правило выбора поколения нужно записать рядом с конфигурацией.
Несколько адресов одной лаборатории
В журнале могут находиться старый реестр, новый реестр и адрес вспомогательного контракта. Интерфейс должен различать назначение каждой записи, даже если все созданы одним издателем. Проверка publisher не выбирает из них правильный автоматически. Название роли экземпляра и ссылка на исходник сохраняются вместе с адресом, чтобы сопровождающий человек мог понять происхождение конфигурации без перебора сети.
Для обновления приложения полезно подготовить новую конфигурацию отдельно и переключить её целиком. Последовательное присваивание сети, адреса и ABI создаёт промежуточное состояние, где часть полей относится к старому выпуску приложения, а часть к новому. В нашем модуле нормализуется один объект и затем становится текущим; активные запросы прежнего поколения признаются устаревшими. Такой способ не делает браузер доверенным, но исключает обычную инженерную гонку.