Серверные компоненты и агенты для приложений Web3
Введение
В большинстве случаев децентрализованное приложение использует сервер для распространения программного обеспечения, но все фактическое взаимодействие происходит между клиентом (обычно веб-браузером) и блокчейном.
Однако есть случаи, когда приложению было бы полезно иметь независимо работающий серверный компонент. Такой сервер сможет реагировать на события и запросы, поступающие из других источников, например API, создавая транзакции.
Такой сервер может выполнять несколько возможных задач.
-
Хранитель секретного состояния. В играх часто бывает полезно, чтобы не вся известная игре информация была доступна игрокам. Однако в блокчейне нет секретов, и любую информацию, находящуюся в блокчейне, легко может узнать кто угодно. Поэтому, если часть состояния игры должна храниться в секрете, ее необходимо хранить в другом месте (и, возможно, проверять эффекты этого состояния с помощью доказательств с 0-знанием).
-
Централизованный оракул. Если ставки достаточно низки, внешнего сервера, который считывает некоторую информацию в сети, а затем публикует ее в цепи, может быть достаточно для использования в качестве оракула.
-
Агент. В блокчейне ничего не происходит без транзакции, которая это активирует. Сервер может действовать от имени пользователя для выполнения таких действий, как арбитраж, когда предоставляется такая возможность.
Пример программы
Пример сервера можно посмотреть на GitHub (opens in a new tab). Этот сервер прослушивает события, поступающие от этого контракта (opens in a new tab), модифицированной версии Greeter от Hardhat. Когда приветствие изменяется, он меняет его обратно.
Чтобы запустить ее:
-
Клонируйте репозиторий.
1git clone https://github.com/qbzzt/20240715-server-component.git2cd 20240715-server-component -
Установить нужные пакеты. Если у вас еще нет Node, сначала установите его (opens in a new tab).
1npm install -
Отредактируйте
.env, чтобы указать приватный ключ аккаунта, на котором есть ETH в тестовой сети Holesky. Если у вас нет ETH в сети Holesky, вы можете воспользоваться этим краном (opens in a new tab).1PRIVATE_KEY=0x <сюда введите приватный ключ> -
Запустите сервер.
1npm start -
Перейдите в обозреватель блоков (opens in a new tab) и, используя адрес, отличный от того, к которому относится приватный ключ, измените приветствие. Вы увидите, что приветствие автоматически изменится обратно.
Как это работает?
Самый простой способ понять, как написать серверный компонент, — это построчно разобрать пример.
src/app.ts
Основная часть программы содержится в src/app.ts (opens in a new tab).
Создание необходимых объектов
1import {2 createPublicClient,3 createWalletClient,4 getContract,5 http,6 Address,7} from "viem"Это сущности Viem (opens in a new tab), которые нам нужны: функции и тип Address (opens in a new tab). Этот сервер написан на TypeScript (opens in a new tab) — расширении JavaScript, которое делает его строго типизированным (opens in a new tab).
1import { privateKeyToAccount } from "viem/accounts"Эта функция (opens in a new tab) позволяет нам генерировать информацию о кошельке, включая адрес, соответствующий приватному ключу.
1import { holesky } from "viem/chains"Чтобы использовать блокчейн в Viem, необходимо импортировать его определение. В данном случае мы хотим подключиться к тестовому блокчейну Holesky (opens in a new tab).
1// Так мы добавляем определения из .env в process.env.2import * as dotenv from "dotenv"3dotenv.config()Таким образом мы считываем .env в окружение. Это нужно нам для приватного ключа (см. далее).
1const greeterAddress : Address = "0xB8f6460Dc30c44401Be26B0d6eD250873d8a50A6"2const greeterABI = [3 {4 "inputs": [5 {6 "internalType": "string",7 "name": "_greeting",8 "type": "string"9 }10 ],11 "stateMutability": "nonpayable",12 "type": "constructor"13 },14 .15 .16 .17 {18 "inputs": [19 {20 "internalType": "string",21 "name": "_greeting",22 "type": "string"23 }24 ],25 "name": "setGreeting",26 "outputs": [],27 "stateMutability": "nonpayable",28 "type": "function"29 }30] as constПоказать всеДля использования контракта нам нужен его адрес и . Здесь мы предоставляем и то, и другое.
В JavaScript (и, следовательно, в TypeScript) нельзя присвоить новое значение константе, но можно изменить объект, который в ней хранится. Используя суффикс as const, мы сообщаем TypeScript, что сам список является константой и не может быть изменен.
1const publicClient = createPublicClient({2 chain: holesky,3 transport: http(),4})Создайте публичный клиент (opens in a new tab) Viem. Публичные клиенты не имеют привязанного приватного ключа и поэтому не могут отправлять транзакции. Они могут вызывать функции view (opens in a new tab), считывать балансы аккаунтов и т. д.
1const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`)Переменные окружения доступны в process.env (opens in a new tab). Однако TypeScript строго типизирован. Переменная окружения может быть любой строкой или пустой, поэтому тип для переменной окружения — string | undefined. Однако ключ в Viem определяется как 0x${string} (0x, за которым следует строка). Здесь мы сообщаем TypeScript, что переменная окружения PRIVATE_KEY будет этого типа. Если это не так, мы получим ошибку времени выполнения.
Функция privateKeyToAccount (opens in a new tab) затем использует этот приватный ключ для создания полного объекта аккаунта.
1const walletClient = createWalletClient({2 account,3 chain: holesky,4 transport: http(),5})Далее мы используем объект аккаунта для создания клиента кошелька (opens in a new tab). Этот клиент имеет приватный ключ и адрес, поэтому его можно использовать для отправки транзакций.
1const greeter = getContract({2 address: greeterAddress,3 abi: greeterABI,4 client: { public: publicClient, wallet: walletClient },5})Теперь, когда у нас есть все необходимое, мы можем наконец создать экземпляр контракта (opens in a new tab). Мы будем использовать этот экземпляр контракта для связи с ончейн-контрактом.
Чтение из блокчейна
1console.log(`Current greeting:`, await greeter.read.greet())Функции контракта, предназначенные только для чтения (view (opens in a new tab) и pure (opens in a new tab)), доступны в read. В данном случае мы используем ее для доступа к функции greet (opens in a new tab), которая возвращает приветствие.
JavaScript является однопоточным, поэтому, когда мы запускаем длительный процесс, нам нужно указать, что мы делаем это асинхронно (opens in a new tab). Вызов блокчейна, даже для операции только для чтения, требует обмена данными между компьютером и узлом блокчейна. Именно по этой причине мы указываем, что код должен await (ожидать) результата.
Если вам интересно, как это работает, вы можете прочитать об этом здесь (opens in a new tab), но на практике все, что вам нужно знать, это то, что вы должны await (ожидать) результатов, если вы начинаете операцию, которая занимает много времени, и что любая функция, которая это делает, должна быть объявлена как async.
Создание транзакций
1const setGreeting = async (greeting: string): Promise<any> => {Это функция, которую вы вызываете для создания транзакции, изменяющей приветствие. Поскольку это длительная операция, функция объявлена как async. Из-за внутренней реализации любая async-функция должна возвращать объект Promise. В данном случае Promise<any> означает, что мы не указываем, что именно будет возвращено в Promise.
1const txHash = await greeter.write.setGreeting([greeting])Поле write экземпляра контракта содержит все функции, которые записывают данные в состояние блокчейна (те, которые требуют отправки транзакции), например setGreeting (opens in a new tab). Параметры, если они есть, предоставляются в виде списка, а функция возвращает хэш транзакции.
1 console.log(`Working on a fix, see https://eth-holesky.blockscout.com/tx/${txHash}`)23 return txHash4}Сообщите хэш транзакции (как часть URL-адреса для его просмотра в обозревателе блоков) и верните его.
Реагирование на события
1greeter.watchEvent.SetGreeting({Функция watchEvent (opens in a new tab) позволяет указать функцию, которая будет выполняться при возникновении события. Если вас интересует только один тип события (в данном случае SetGreeting), вы можете использовать этот синтаксис, чтобы ограничиться этим типом события.
1 onLogs: logs => {Функция onLogs вызывается при наличии записей в журнале. В Ethereum «log» (запись в журнале) и «event» (событие), как правило, взаимозаменяемы.
1console.log(2 `Address ${logs[0].args.sender} changed the greeting to ${logs[0].args.greeting}`3)Событий может быть несколько, но для простоты нас интересует только первое. logs[0].args — это аргументы события, в данном случае sender и greeting.
1 if (logs[0].args.sender != account.address)2 setGreeting(`${account.address} insists on it being Hello!`)3 }4})Если отправитель — не этот сервер, используйте setGreeting, чтобы изменить приветствие.
package.json
Этот файл (opens in a new tab) управляет конфигурацией Node.js (opens in a new tab). В этой статье объясняются только важные определения.
1{2 "main": "dist/index.js",Это определение указывает, какой файл JavaScript нужно запустить.
1 "scripts": {2 "start": "tsc && node dist/app.js",3 },Скрипты — это различные действия приложения. В данном случае у нас есть только start, который компилирует, а затем запускает сервер. Команда tsc является частью пакета typescript и компилирует TypeScript в JavaScript. Если вы хотите запустить его вручную, он находится в node_modules/.bin. Вторая команда запускает сервер.
1 "type": "module",Существует несколько типов приложений Node.js. Тип module позволяет нам использовать await в коде верхнего уровня, что важно при выполнении медленных (и, следовательно, асинхронных) операций.
1 "devDependencies": {2 "@types/node": "^20.14.2",3 "typescript": "^5.4.5"4 },Это пакеты, которые требуются только для разработки. Здесь нам нужен typescript, и поскольку мы используем его с Node.js, мы также получаем типы для переменных и объектов node, таких как process. Обозначение ^<version> (opens in a new tab) означает эту версию или более высокую версию, не содержащую критических изменений. Подробнее о значении номеров версий см. здесь (opens in a new tab).
1 "dependencies": {2 "dotenv": "^16.4.5",3 "viem": "2.14.1"4 }5}Это пакеты, которые требуются во время выполнения, при запуске dist/app.js.
Заключение
Созданный нами здесь централизованный сервер выполняет свою работу — действует как агент для пользователя. Любой другой, кто хочет, чтобы децентрализованное приложение продолжало функционировать, и готов потратить газ, может запустить новый экземпляр сервера со своим собственным адресом.
Однако это работает только тогда, когда действия централизованного сервера можно легко проверить. Если централизованный сервер хранит какую-либо информацию о секретном состоянии или выполняет сложные вычисления, он является централизованной сущностью, которой необходимо доверять для использования приложения, а это именно то, чего пытаются избежать блокчейны. В одной из следующих статей я планирую показать, как использовать доказательства с 0-знанием, чтобы обойти эту проблему.
Больше моих работ смотрите здесь (opens in a new tab).
Последнее обновление страницы: 25 февраля 2026 г.