---
title: "Серверные компоненты и агенты для web3-приложений"
description: "После прочтения этого руководства вы сможете писать серверы на TypeScript, которые прослушивают события в блокчейне и реагируют на них собственными транзакциями. Это позволит вам писать централизованные приложения (поскольку сервер является точкой отказа), которые могут взаимодействовать с сущностями web3. Те же методы можно использовать для написания агента, который реагирует на ончейн-события без участия человека."
author: "Ори Померанц"
lang: ru
tags: ["агент", "сервер", "оффчейн", "dapps"]
skill: beginner
breadcrumb: "Серверные компоненты"
published: 2024-07-15
---

## Введение {#introduction}

В большинстве случаев децентрализованное приложение (dapp) использует сервер для распространения программного обеспечения, но все фактическое взаимодействие происходит между клиентом (обычно веб-браузером) и блокчейном.

![Normal interaction between web server, client, and blockchain](./fig-1.svg)

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

![The interaction with the addition of a server](./fig-2.svg)

Существует несколько возможных задач, которые мог бы выполнять такой сервер.

- Хранитель секретного состояния. В играх часто бывает полезно, чтобы не вся информация, известная игре, была доступна игрокам. Однако _в блокчейне нет секретов_, любую информацию, находящуюся в блокчейне, легко может узнать любой желающий. Поэтому, если часть состояния игры должна храниться в секрете, она должна храниться в другом месте (и, возможно, эффекты этого состояния должны проверяться с использованием [доказательств с нулевым разглашением](/zero-knowledge-proofs)).

- Централизованный оракул. Если ставки достаточно низки, внешний сервер, который считывает некоторую информацию в сети, а затем публикует ее в цепь, может быть достаточно хорош для использования в качестве [оракула](/developers/docs/oracles/).

- Агент. В блокчейне ничего не происходит без транзакции, которая бы это активировала. Сервер может действовать от имени пользователя для выполнения таких действий, как [арбитраж](/developers/docs/mev/#mev-examples-dex-arbitrage), когда представляется такая возможность.

## Пример программы {#sample-program}

Вы можете посмотреть пример сервера [на GitHub](https://github.com/qbzzt/20240715-server-component). Этот сервер прослушивает события, исходящие от [этого контракта](https://eth-holesky.blockscout.com/address/0xB8f6460Dc30c44401Be26B0d6eD250873d8a50A6?tab=contract_code), модифицированной версии Greeter от Hardhat. Когда приветствие изменяется, он меняет его обратно.

Чтобы запустить его:

1. Клонируйте репозиторий.

   ```sh copy
   git clone https://github.com/qbzzt/20240715-server-component.git
   cd 20240715-server-component
   ```

2. Установите необходимые пакеты. Если у вас его еще нет, [сначала установите Node](https://nodejs.org/en/download/package-manager).

   ```sh copy
   npm install
   ```

3. Отредактируйте `.env`, чтобы указать приватный ключ аккаунта, на котором есть ETH в тестовой сети Холески. Если у вас нет ETH в Холески, вы можете [использовать этот кран](https://holesky-faucet.pk910.de/).

   ```sh filename=".env" copy
   PRIVATE_KEY=0x <private key goes here>
   ```

4. Запустите сервер.

   ```sh copy
   npm start
   ```

5. Перейдите в [обозреватель блоков](https://eth-holesky.blockscout.com/address/0xB8f6460Dc30c44401Be26B0d6eD250873d8a50A6?tab=write_contract) и, используя адрес, отличный от того, которому принадлежит приватный ключ, измените приветствие. Убедитесь, что приветствие автоматически меняется обратно.

### Как это работает? {#how-it-works}

Самый простой способ понять, как написать серверный компонент — разобрать пример построчно.

#### `src/app.ts` {#src-app-ts}

Подавляющая часть программы содержится в [`src/app.ts`](https://github.com/qbzzt/20240715-server-component/blob/main/src/app.ts).

##### Создание необходимых объектов

```typescript
import {
  createPublicClient,
  createWalletClient,
  getContract,
  http,
  Address,
} from "viem"
```

Это необходимые нам сущности [Viem](https://viem.sh/), функции и [тип `Address`](https://viem.sh/docs/glossary/types#address). Этот сервер написан на [TypeScript](https://www.typescriptlang.org/), который является расширением JavaScript, делающим его [строго типизированным](https://en.wikipedia.org/wiki/Strong_and_weak_typing).

```typescript
import { privateKeyToAccount } from "viem/accounts"
```

[Эта функция](https://viem.sh/docs/accounts/privateKey) позволяет нам генерировать информацию о кошельке, включая адрес, соответствующий приватному ключу.

```typescript
import { holesky } from "viem/chains"
```

Чтобы использовать блокчейн в Viem, вам нужно импортировать его определение. В данном случае мы хотим подключиться к тестовому блокчейну [Холески](https://github.com/eth-clients/holesky).

```typescript
// Вот так мы добавляем определения из .env в process.env.
import * as dotenv from "dotenv"
dotenv.config()
```

Так мы считываем `.env` в окружение. Это нужно нам для приватного ключа (см. далее).

```typescript
const greeterAddress : Address = "0xB8f6460Dc30c44401Be26B0d6eD250873d8a50A6"
const greeterABI = [
    {
        "inputs": [
            {
                "internalType": "string",
                "name": "_greeting",
                "type": "string"
            }
        ],
        "stateMutability": "nonpayable",
        "type": "constructor"
    },
         .
         .
         .
    {
        "inputs": [
            {
                "internalType": "string",
                "name": "_greeting",
                "type": "string"
            }
        ],
        "name": "setGreeting",
        "outputs": [],
        "stateMutability": "nonpayable",
        "type": "function"
    }
] as const
```

Для использования контракта нам нужен его адрес и [ABI](/glossary/#abi) для него. Мы предоставляем и то, и другое здесь.

В JavaScript (и, следовательно, в TypeScript) вы не можете присвоить новое значение константе, но вы _можете_ изменить объект, который в ней хранится. Используя суффикс `as const`, мы сообщаем TypeScript, что сам список является константой и не может быть изменен.

```typescript
const publicClient = createPublicClient({
  chain: holesky,
  transport: http(),
})
```

Создайте [публичный клиент](https://viem.sh/docs/clients/public.html) Viem. Публичные клиенты не имеют привязанного приватного ключа и поэтому не могут отправлять транзакции. Они могут вызывать [функции `view`](https://www.tutorialspoint.com/solidity/solidity_view_functions.htm), считывать балансы аккаунтов и т. д.

```typescript
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`)
```

Переменные окружения доступны в [`process.env`](https://www.totaltypescript.com/how-to-strongly-type-process-env). Однако TypeScript строго типизирован. Переменная окружения может быть любой строкой или пустой, поэтому тип для переменной окружения — `string | undefined`. Однако ключ определяется в Viem как `0x${string}` (`0x`, за которым следует строка). Здесь мы сообщаем TypeScript, что переменная окружения `PRIVATE_KEY` будет иметь этот тип. Если это не так, мы получим ошибку во время выполнения.

Затем функция [`privateKeyToAccount`](https://viem.sh/docs/accounts/privateKey) использует этот приватный ключ для создания полного объекта аккаунта.

```typescript
const walletClient = createWalletClient({
  account,
  chain: holesky,
  transport: http(),
})
```

Далее мы используем объект аккаунта для создания [клиента кошелька](https://viem.sh/docs/clients/wallet). У этого клиента есть приватный ключ и адрес, поэтому его можно использовать для отправки транзакций.

```typescript
const greeter = getContract({
  address: greeterAddress,
  abi: greeterABI,
  client: { public: publicClient, wallet: walletClient },
})
```

Теперь, когда у нас есть все необходимое, мы наконец можем создать [экземпляр контракта](https://viem.sh/docs/contract/getContract). Мы будем использовать этот экземпляр контракта для связи с ончейн-контрактом.

##### Чтение из блокчейна

```typescript
console.log(`Current greeting:`, await greeter.read.greet())
```

Функции контракта, доступные только для чтения ([`view`](https://www.tutorialspoint.com/solidity/solidity_view_functions.htm) и [`pure`](https://www.tutorialspoint.com/solidity/solidity_pure_functions.htm)), доступны в `read`. В данном случае мы используем его для доступа к функции [`greet`](https://eth-holesky.blockscout.com/address/0xB8f6460Dc30c44401Be26B0d6eD250873d8a50A6?tab=read_contract#cfae3217), которая возвращает приветствие.

JavaScript является однопоточным, поэтому, когда мы запускаем длительный процесс, нам нужно [указать, что мы делаем это асинхронно](https://eloquentjavascript.net/11_async.html#h-XvLsfAhtsE). Вызов блокчейна, даже для операции только для чтения, требует двустороннего обмена данными между компьютером и узлом блокчейна. По этой причине мы указываем здесь, что коду нужно использовать `await` для ожидания результата.

Если вам интересно, как это работает, вы можете [прочитать об этом здесь](https://www.w3schools.com/js/js_promise.asp), но на практике все, что вам нужно знать, это то, что вы используете `await` для результатов, если запускаете операцию, которая занимает много времени, и что любая функция, которая это делает, должна быть объявлена как `async`.

##### Отправка транзакций

```typescript
const setGreeting = async (greeting: string): Promise<any> => {
```

Это функция, которую вы вызываете для отправки транзакции, изменяющей приветствие. Поскольку это длительная операция, функция объявлена как `async`. Из-за внутренней реализации любая функция `async` должна возвращать объект `Promise`. В данном случае `Promise<any>` означает, что мы не указываем, что именно будет возвращено в `Promise`.

```typescript
const txHash = await greeter.write.setGreeting([greeting])
```

Поле `write` экземпляра контракта содержит все функции, которые записывают в состояние блокчейна (те, которые требуют отправки транзакции), такие как [`setGreeting`](https://eth-holesky.blockscout.com/address/0xB8f6460Dc30c44401Be26B0d6eD250873d8a50A6?tab=write_contract#a4136862). Параметры, если они есть, предоставляются в виде списка, и функция возвращает хеш транзакции.

```typescript
    console.log(`Working on a fix, see https://eth-holesky.blockscout.com/tx/${txHash}`)

    return txHash
}
```

Сообщите хеш транзакции (как часть URL-адреса обозревателя блоков для его просмотра) и верните его.

##### Реагирование на события

```typescript
greeter.watchEvent.SetGreeting({
```

[Функция `watchEvent`](https://viem.sh/docs/actions/public/watchEvent) позволяет указать, что функция должна запускаться при генерации события. Если вас интересует только один тип события (в данном случае `SetGreeting`), вы можете использовать этот синтаксис, чтобы ограничиться этим типом события.

```typescript
    onLogs: logs => {
```

Функция `onLogs` вызывается при наличии записей логов. В Эфириуме «лог» и «событие» обычно взаимозаменяемы.

```typescript
console.log(
  `Address ${logs[0].args.sender} changed the greeting to ${logs[0].args.greeting}`
)
```

Событий может быть несколько, но для простоты нас интересует только первое. `logs[0].args` — это аргументы события, в данном случае `sender` и `greeting`.

```typescript
        if (logs[0].args.sender != account.address)
            setGreeting(`${account.address} insists on it being Hello!`)
    }
})
```

Если отправителем _не_ является этот сервер, используйте `setGreeting` для изменения приветствия.

#### `package.json` {#package-json}

[Этот файл](https://github.com/qbzzt/20240715-server-component/blob/main/package.json) управляет конфигурацией [Node.js](https://nodejs.org/en). В этой статье объясняются только важные определения.

```json
{
  "main": "dist/index.js",
```

Это определение указывает, какой файл JavaScript следует запустить.

```json
  "scripts": {
    "start": "tsc && node dist/app.js",
  },
```

Скрипты — это различные действия приложения. В данном случае у нас есть только `start`, который компилирует, а затем запускает сервер. Команда `tsc` является частью пакета `typescript` и компилирует TypeScript в JavaScript. Если вы хотите запустить ее вручную, она находится в `node_modules/.bin`. Вторая команда запускает сервер.

```json
  "type": "module",
```

Существует несколько типов приложений узлов JavaScript. Тип `module` позволяет нам использовать `await` в коде верхнего уровня, что важно при выполнении медленных (и, следовательно, асинхронных) операций.

```json
  "devDependencies": {
    "@types/node": "^20.14.2",
    "typescript": "^5.4.5"
  },
```

Это пакеты, которые требуются только для разработки. Здесь нам нужен `typescript`, и поскольку мы используем его с Node.js, мы также получаем типы для переменных и объектов узла, таких как `process`. [Обозначение `^<version>`](https://github.com/npm/node-semver?tab=readme-ov-file#caret-ranges-123-025-004) означает эту версию или более высокую версию, которая не содержит критических изменений. Смотрите [здесь](https://semver.org) для получения дополнительной информации о значении номеров версий.

```json
  "dependencies": {
    "dotenv": "^16.4.5",
    "viem": "2.14.1"
  }
}
```

Это пакеты, которые требуются во время выполнения, при запуске `dist/app.js`.

## Заключение {#conclusion}

Созданный нами здесь централизованный сервер выполняет свою работу, которая заключается в том, чтобы действовать в качестве агента для пользователя. Любой другой, кто хочет, чтобы dapp продолжало функционировать, и готов тратить газ, может запустить новый экземпляр сервера со своим собственным адресом.

Однако это работает только тогда, когда действия централизованного сервера можно легко проверить. Если централизованный сервер обладает какой-либо секретной информацией о состоянии или выполняет сложные вычисления, он становится централизованной сущностью, которой вам нужно доверять для использования приложения, а это именно то, чего блокчейны пытаются избежать. В будущей статье я планирую показать, как использовать [доказательства с нулевым разглашением](/zero-knowledge-proofs), чтобы обойти эту проблему.

[Смотрите здесь больше моих работ](https://cryptodocguy.pro/).