> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tenderly.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Как обрабатывать on-chain события

> Используйте Web3 Actions в качестве serverless-бэкенда для реагирования на on-chain события. Следуйте примеру с крестиками-ноликами для отслеживания изменений в игре.

Этот туториал научит вас **создавать serverless-бэкенд для вашего смарт-контракта с помощью Tenderly Web3 Actions**. Web3 Actions позволяют запускать пользовательский код в ответ на on-chain или off-chain события, инициированные вашим смарт-контрактом.

Чтобы проиллюстрировать, как работают Web3 Actions, мы построим простую игру «крестики-нолики» и развернём её в тестовой сети. Смарт-контракт поддерживает состояние игры, а Web3 Actions отслеживают изменения в игре.

**Всякий раз, когда из смарт-контракта генерируется конкретное событие, Tenderly будет выполнять ваш пользовательский код в виде NodeJS-проекта**. Результаты игры и игровое поле будут выводиться в консоль каждый раз, когда игрок делает ход или игра окончена.

<Info>
  Ознакомьтесь с этим [Github-репозиторием](https://github.com/Tenderly/examples-web3-actions/), чтобы найти
  исходный код этого проекта. Свободно клонируйте репозиторий и экспериментируйте с ним, или кодите вместе с нами.
</Info>

## Понимание Web3 Actions

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

Tenderly помогает упорядочить этот процесс с помощью Web3 Actions. Вы можете написать свои Web3 Actions в одном JavaScript-файле или развернуть как NodeJS-проект.

**Web3 Actions также позволяют выполнять любые действия, которые обычно делают с NodeJS**. Этот туториал покажет, как использовать Tenderly CLI для развёртывания Web3 Actions и обеспечения их запуска при выполнении определённых условий.

<Note>
  Узнайте больше о Web3 Actions и о том, как использовать их в своих проектах, в [**Основах
  Web3 Actions**](/monitoring/web3-actions/tutorials/on-chain-events).
</Note>

### Обзор проекта

Цель этого проекта — показать вам, как развернуть смарт-контракт и написать несколько JavaScript-функций, которые Tenderly будет вызывать при возникновении события. Вы узнаете, как использовать Tenderly CLI для развёртывания ваших Web3 Actions на инфраструктуре Tenderly.

Вот пошаговый обзор того, как это сделать:

1. Разверните смарт-контракт в тестовую сеть
2. Используйте Tenderly CLI для настройки JavaScript- и конфигурационных файлов, необходимых для запуска Web3 Actions
3. Разработайте функции, реагирующие на события игры
4. Разверните эти функции на инфраструктуре Tenderly с помощью CLI

<Warning>
  Для этого туториала вам понадобится доступ к **Tenderly Dashboard**. Если у вас нет аккаунта,
  [**зарегистрируйтесь здесь бесплатно**](https://dashboard.tenderly.co/register?utm_source=homepage) (карта не требуется).
</Warning>

## 0: Разверните смарт-контракт

<Note>
  Если вы кодите вместе с нами, вы можете пропустить этот шаг и использовать контракт, который мы [**развернули и верифицировали в
  Tenderly**](https://dashboard.tenderly.co/contract/sepolia/0x1338d7232786b365b0a6bbd06552f885371f5f06).
</Note>

Мы используем заранее написанный смарт-контракт. Ознакомьтесь с исходным кодом [**смарт-контракта на GitHub**](https://github.com/Tenderly/examples-web3-actions/blob/main/tic-tac-toe/TicTacToe.sol). Этот смарт-контракт спроектирован так, чтобы генерировать события всякий раз, когда происходит изменение состояния игры.

Наша простая игра в крестики-нолики может иметь четыре возможных состояния:

* Начало игры
* Игрок присоединяется к игре
* Игрок делает ход
* Конец игры

### Предварительные условия

**Создайте два аккаунта** — разверните смарт-контракт в любую [сеть, поддерживаемую Tenderly](/platform/supported-networks). Наиболее удобный способ это сделать — с помощью [Remix](https://remix.ethereum.org/) и плагина кошелька [MetaMask](https://metamask.io/). Вам нужны два аккаунта с положительным балансом Ether. Эти аккаунты будут представлять двух игроков. Для целей этого туториала мы используем Sepolia. Если вы планируете следовать этому туториалу, вы можете использовать [кран Sepolia](https://faucet.chainstack.com/sepolia-testnet-faucet/) для пополнения аккаунтов эфиром.

**Скомпилируйте контракт** — используйте IDE Remix для компиляции смарт-контракта. Создайте новый файл контракта и добавьте код, найденный [**здесь**](https://github.com/Tenderly/examples-web3-actions/blob/main/tic-tac-toe/TicTacToe.sol).

<Frame caption="Компиляция контракта в IDE Remix">
  <img src="https://mintcdn.com/tenderly/XsEZlaGXYskrtN68/images/image%20(69).webp?fit=max&auto=format&n=XsEZlaGXYskrtN68&q=85&s=6f20b3090790b771b014692cdcf7be9a" alt="Compiling the contract in the Remix IDE" width="2878" height="1642" data-path="images/image (69).webp" />
</Frame>

**Разверните в тестовую сеть** — в плагине MetaMask убедитесь, что выбрана Sepolia. Затем перейдите в раздел **«Deploy And Run Transactions»** в Remix и выберите **`Injected Web3`**. Нажмите **«Deploy»**, убедившись, что аккаунт соответствует аккаунту в MetaMask.

<Frame caption="Развёртывание контракта в тестнет">
  <img src="https://mintcdn.com/tenderly/XsEZlaGXYskrtN68/images/image%20(73).webp?fit=max&auto=format&n=XsEZlaGXYskrtN68&q=85&s=a8cfef3f43f563adbb3dfdd62177b50f" alt="Deploying the contract to a testnet" width="1070" height="1372" data-path="images/image (73).webp" />
</Frame>

<Frame caption="Копирование адреса контракта">
  <img src="https://mintcdn.com/tenderly/XsEZlaGXYskrtN68/images/image%20(92).webp?fit=max&auto=format&n=XsEZlaGXYskrtN68&q=85&s=4a2339153a44fa17921bce22e01e703b" alt="Copying the contract address" width="912" height="1070" data-path="images/image (92).webp" />
</Frame>

Чтобы получить больше информации о развёрнутом контракте, обратитесь к разделу **«Deployed Contracts»**. Нажмите значок копирования, чтобы скопировать полный адрес контракта (`0x133...`), и сохраните его где-нибудь, так как он нам понадобится на следующих шагах.

Теперь перейдите в **Tenderly Dashboard, чтобы верифицировать контракт**. Это позволит нам взаимодействовать с контрактом позже в туториале.

## 1: Настройка Web3 Actions через Tenderly CLI

Чтобы продолжить туториал, на вашей машине должен быть установлен **Tenderly CLI**. Следуйте [**этому руководству**](https://github.com/Tenderly/tenderly-cli#installation), чтобы узнать, как его настроить и аутентифицировать доступ.

С установленным CLI создайте новый каталог и перейдите в него (`cd`). Инициализируйте свои Web3 Actions, выполнив команду `tenderly actions init`:

```bash theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
$> cd tdly-actions
$> tenderly actions init
```

Вам будет предложено выбрать один из ваших существующих проектов Tenderly. Ваши Web3 Actions будут развёрнуты в этот проект и должны появиться в вашей структуре каталогов так:

```bash title="example" showLineNumbers theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
$> ls actions
  example.ts     # where we write Web3 actions code
  tsconfig.json
  package.json
```

По умолчанию используется TypeScript, но вы можете переключиться на обычный JavaScript. Перед выполнением команды init задайте JavaScript как предпочитаемый язык так: `tenderly actions init --language javascript`.

<Note>
  `package.json` содержит npm-зависимости, которые будут доступны при развёртывании вашего Web3 Action.
  Файл `tsconfig.json` содержит конфигурацию TypeScript,
  относящуюся только к файлам `.ts` в каталоге{' '}
  `actions`.
</Note>

### 1.1. Добавьте ABI контракта Tic-Tac-Toe

Прежде чем углубляться в код, скопируйте ABI, сгенерированный компилятором, в каталог actions. В нашем примере это каталог `tdly-actions`.

В Remix перейдите к `files/artifacts/TicTacToe.json`, скопируйте содержимое файла и вставьте его в файл `TicTacToe.json` вашего проекта.

<Warning>
  Все файлы, которые использует и на которые ссылается код вашего Web3 Action, должны находиться в{' '}
  каталоге `actions` (например,{' '}
  `TicTacToe.json`).
</Warning>

### 1.2. Настройте TypeScript для импорта JSON-файла как модуля

По умолчанию TypeScript не позволяет импортировать JSON-файлы как модули. Нужно настроить TypeScript так, чтобы можно было импортировать файл `TicTacToe.json` как модуль и обращаться к нему как к объекту.

Перейдите в файл `tsconfig.json` и добавьте следующие две настройки в раздел `compilerOptions`:

```diff title="example" showLineNumbers theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
{
    ...
    "compilerOptions": {
	...
+        "resolveJsonModule": true,
+        "esModuleInterop": true
    },
   ...
}
```

## 2: Напишите функцию для обработки события новой игры

Напишите функцию, которая будет выполняться при начале новой игры. Переименуем файл TypeScript в что-то более описательное:

```bash title="example" showLineNumbers theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
mv example.ts ticTacToeActions.ts
```

Чтобы использовать возможности TypeScript, сначала нужно определить тип, представляющий состояние игры. По сути, мы будем хранить реплику состояния игры в Storage вашего Web3 Action. Каждое поле содержит ID игрока, сходившего туда. Мы будем сопоставлять адрес игрока его ходу (первый, второй и т. д.) в объекте `players`.

### 2.1. Добавьте Action

```typescript title="example.ts" showLineNumbers theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
// ticTacToeActions.ts

export type Game = {
  players: { [address: string]: number };
  board: number[][];
};
```

На этом этапе мы готовы определить сам Web3 Action так:

```typescript title="example.ts" showLineNumbers theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
export const createNewGame = (): Game => {
  return {
    players: {},
    board: [
      [0, 0, 0],
      [0, 0, 0],
      [0, 0, 0],
    ],
  };
};

// ticTacToeActions.ts continued
export const newGameAction: ActionFn = async (context: Context, event: Event) => {
  let txEvent = event as TransactionEvent;

  let iface = new ethers.utils.Interface(TicTacToe.abi);

  const result = iface.decodeEventLog('GameCreated', txEvent.logs[0].data, txEvent.logs[0].topics);

  const { gameId, playerNumber, player } = result;

  console.log('Game Created Event:', {
    gameId: gameId.toString(),
    playerNumber,
    player,
  });

  const game: Game = createNewGame();
  await context.storage.putJson(gameId.toString(), game);
};
```

Этот Web3 Action будет обрабатывать событие `GameCreated`, определённое в смарт-контракте.

Взглянув на смарт-контракт, мы видим, что из функции `newGame` генерируется только одно событие, поэтому нас интересует первая запись лога. Мы можем получить `result` с помощью ethers.js, декодировав `txEvent.logs[0].data` события GameCreated на основе `TicTacToe.abi`.

Здесь мы можем получить ID, ассоциированный с этой конкретной игрой, после того как она была создана: `result.gameId`. Мы хотим отслеживать данные для этой конкретной игры: игроков и сделанные ими ходы, используя новый экземпляр `Game`. Мы сохраняем объект, представляющий новую игру, в Storage такой командой: `context.storage.putJson(gameId, game)`.

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

Вы также можете написать автоматизированные тесты, чтобы проверить поведение Web3 Action. Некоторые тесты доступны в репозитории, но это выходит за рамки этого туториала.

### 2.2. Задайте вызов New Game Action

Откройте tenderly.yaml. В разделе `specs` определите спецификации для `newGame` (произвольное имя) для вызова функции `newGameAction`. Это можно сделать так: `newGameAction:newGameAction`, сначала определяем имя файла, содержащего функцию, а затем имя функции.

Далее укажите `trigger`, за которым Tenderly должен следить для вызова action. Это триггер `transaction`, который сработает при майнинге блока. Мы делаем это для `network 3`, когда событие `NewGame` генерируется контрактом по указанному адресу.

Замените `TTT_CONTRACT_ADDRESS` фактическим адресом вашего смарт-контракта. Если вы хотите развернуть контракт в сеть, отличную от Sepolia, укажите ID сети как значение network.

<Note>
  Замените `YOUR_USERNAME` и{' '}
  `YOUR_PROJECT_SLUG` на ваше имя пользователя Tenderly и slug вашего
  проекта. Их можно скопировать из URL Dashboard:{' '}
  `https://dashboard.tenderly.co/{YOUR_USERNAME}/{YOUR_PROJECT_SLUG}/transactions`
</Note>

```yaml title="example.yaml" showLineNumbers theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
account_id: ''
actions:
  YOUR_USERNAME/YOUR_PROJECT_SLUG:
    runtime: v1
    sources: actions
    specs:
      newGame:
        description: Respond to newGame event
        function: ticTacToeActions:newGameAction
        trigger:
          type: transaction
          transaction:
            status:
              - mined
            filters:
              - network: 3
                eventEmitted:
                  contract:
                    address: TTT_CONTRACT_ADDRESS
                  name: GameCreated
project_slug: ''
```

### 2.3. Верифицируйте ваш смарт-контракт Tic-Tac-Toe в Tenderly

Прежде чем разворачивать контракт, нужно верифицировать его в Tenderly Dashboard.

При желании вы можете [загрузить контракт через браузер](/contract-verification/overview). После загрузки выберите контракт TicTacToe, сеть, в которую вы его развернули, и адрес контракта.

Нажмите Add Contract и заполните опции компилятора, как показано на картинке ниже:

<Frame caption="Верификация и добавление контракта в Tenderly ">
  <img src="https://mintcdn.com/tenderly/XsEZlaGXYskrtN68/images/image%20(74).webp?fit=max&auto=format&n=XsEZlaGXYskrtN68&q=85&s=01b598bfc2a6fbdf63ddb1533f10d40c" alt="Verifying and adding the contract to Tenderly " width="2154" height="1142" data-path="images/image (74).webp" />
</Frame>

<Frame caption="Добавление информации о компиляторе контракта ">
  <img src="https://mintcdn.com/tenderly/XsEZlaGXYskrtN68/images/image%20(87).webp?fit=max&auto=format&n=XsEZlaGXYskrtN68&q=85&s=e99c1eb47857101c573bddf8131cf40d" alt="Adding the contract compiler information " width="1668" height="660" data-path="images/image (87).webp" />
</Frame>

### 2.4. Разверните Web3 Action в Tenderly

Чтобы развернуть ваш Web3 Action, выполните команду `deploy` через Tenderly CLI:

```bash title="example" showLineNumbers theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
tenderly actions deploy
```

Вывод должен выглядеть так:

<Frame caption="Вывод выполнения Web3 Action">
  <img src="https://mintcdn.com/tenderly/XsEZlaGXYskrtN68/images/image%20(79).webp?fit=max&auto=format&n=XsEZlaGXYskrtN68&q=85&s=265837ec27e0378acfbc192074661c47" alt="The Web3 Action execution output" width="1111" height="202" data-path="images/image (79).webp" />
</Frame>

Чтобы увидеть детали развёртывания вашего Web3 Action, зайдите в Tenderly Dashboard. Если развёртывание прошло успешно, вы увидите что-то вроде этого:

<Frame caption="Открытие информации о развёртывании Web3 Action ">
  <img src="https://mintcdn.com/tenderly/XsEZlaGXYskrtN68/images/image%20(80).webp?fit=max&auto=format&n=XsEZlaGXYskrtN68&q=85&s=c35821d0a7d044029d8e1047cfa89ff6" alt="Opening the Web3 Action deployment information " width="2878" height="1394" data-path="images/image (80).webp" />
</Frame>

### 2.5. Попробуйте начать новую игру 🎉

Чтобы убедиться, что всё работает правильно, вернитесь в Remix и создайте новую игру или несколько игр.

Откройте контракт TicTacToe и нажмите `newGame`. MetaMask должен запросить у вас подтверждение выполнения Web3 Action.

После того как транзакция отправлена в цепочку, Remix должен вывести результат, похожий на этот:

<Frame caption="Создание новой игры в Remix">
  <img src="https://mintcdn.com/tenderly/XsEZlaGXYskrtN68/images/image%20(96).webp?fit=max&auto=format&n=XsEZlaGXYskrtN68&q=85&s=a0aad1196ca95ba6470b8b5f0edcee12" alt="Creating a new game in Remix" width="2880" height="1648" data-path="images/image (96).webp" />
</Frame>

Далее вернитесь в Tenderly Dashboard и откройте раздел Transactions, чтобы увидеть свою транзакцию. В разделе Actions вы заметите, что колонка **«Latest Execution»** изменилась.

Чтобы просмотреть историю выполнения, откройте свой Web3 Action и нажмите вкладку Execution History:

<Frame caption="Открытие вкладки Execution history для Web3 Action ">
  <img src="https://mintcdn.com/tenderly/XsEZlaGXYskrtN68/images/image%20(81).webp?fit=max&auto=format&n=XsEZlaGXYskrtN68&q=85&s=7b2dedf972e0e4a6f9bc11fe27a33f85" alt="Opening the Execution history tab for your Web3 Action " width="2868" height="1444" data-path="images/image (81).webp" />
</Frame>

История выполнения предоставит вам следующие данные:

<Frame caption="Данные истории выполнения">
  <img src="https://mintcdn.com/tenderly/XsEZlaGXYskrtN68/images/image%20(89).webp?fit=max&auto=format&n=XsEZlaGXYskrtN68&q=85&s=ec2d6994130b322e35d858124d104898" alt="The execution history data" width="2169" height="1106" data-path="images/image (89).webp" />
</Frame>

В верхней панели вы найдёте информацию о полезной нагрузке, приходящей из цепочки. Нижняя панель содержит детали о залогированном номере игры. В нашем случае это `0xb`, что означает, что это 11-я игра, начатая для этого контракта.

### 2.6. Проверьте Storage вашего Web3 Action

Нажмите кнопку **«Go To Storage»**, чтобы увидеть содержимое Storage. Каждая инициированная игра получит свой слот в этой карте ключ-значение.

Когда вы откроете только что созданную игру с **ID 11**, вы увидите нули во всех полях, что означает, что ни один игрок ещё не играл.

<Frame caption="Открытие Storage вашего Web3 Action ">
  <img src="https://mintcdn.com/tenderly/XsEZlaGXYskrtN68/images/image%20(90).webp?fit=max&auto=format&n=XsEZlaGXYskrtN68&q=85&s=88f11391c94122ab621cac47f2bc8bdd" alt="Opening the Storage of your Web3 Action " width="1522" height="558" data-path="images/image (90).webp" />
</Frame>

## 3: Добавьте Web3 Action для обработки присоединения новых игроков к игре

Присоединение игрока к игре должно обрабатываться регистрацией адреса игрока и его хода (1 или 2).

Помимо шаблонного кода, который помогает получить данные события, файл `playerJoinAction.ts` также содержит код, позволяющий:

* Читать текущее состояние игры из storage по ID игры (`storage.getJson`).
* Получать адрес игрока и сохранять его ход: `game.players[player] = playerNumber`
* Сохранять обновлённый объект игры в storage Web3 Action с помощью `storage.putJson`.

```typescript title="example.ts" showLineNumbers theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
// playerJoinAction.ts

export const playerJoinedAction: ActionFn = async (context: Context, event: Event) => {
  let txEvent = event as TransactionEvent;
  let iface = new ethers.utils.Interface(TicTacToe.abi);
  const result = iface.decodeEventLog(
    'PlayerJoinedGame',
    txEvent.logs[0].data,
    txEvent.logs[0].topics,
  );

  const gameId = result.gameId.toString();
  const playerAddress = result.player.toLowerCase() as string;
  const playerNumber = result.playerNumber;

  console.log('Player joined event:', {
    gameId,
    playerAddress,
    playerNumber,
  });

  const game: Game = (await context.storage.getJson(gameId)) as Game;
  game.players[playerAddress] = playerNumber;

  await context.storage.putJson(result.gameId.toString(), game);
};
```

### 3.1. Задайте вызов PlayerJoinedGame Action

Нам также нужно расширить `specs` в файле `tenderly.yaml`, чтобы включить спецификации, необходимые для вызова Web3 Action.

```yaml title="example.yaml" showLineNumbers theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
playerJoined:
  description: Respond to player joining game
  function: ticTacToeActions:playerJoinedAction
  trigger:
    type: transaction
    transaction:
      status:
        - mined
      filters:
        - network: 3
          eventEmitted:
            contract:
              address: TTT_CONTRACT_ADDRESS
            name: PlayerJoinedGame
```

### 3.2. Разверните action в Tenderly

Разверните ваш Web3 Action, выполнив команду `deploy`. Эта команда также переразвернёт ранее развёрнутые Web3 Actions.

```javascript title="example.js" showLineNumbers theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
tenderly action deploy
```

### 3.3. Попробуйте присоединиться к новой игре

Присоединитесь к новой игре, перейдя в Remix и добавив номер игры из логов в поле ввода `newGame`. Чтобы отправить транзакцию, нажмите кнопку `joinGame`:

<Frame caption="Присоединение к новой игре в Remix ">
  <img src="https://mintcdn.com/tenderly/XsEZlaGXYskrtN68/images/image%20(85).webp?fit=max&auto=format&n=XsEZlaGXYskrtN68&q=85&s=b775e77378e6a2706fe73eed98a3d79d" alt="Joining a new game in Remix " width="852" height="796" data-path="images/image (85).webp" />
</Frame>

После того как транзакция заминирована, в логах выполнения будет отображаться `playerNumber: 1`.

<Frame caption="Лог выполнения">
  <img src="https://mintcdn.com/tenderly/XsEZlaGXYskrtN68/images/image%20(93).webp?fit=max&auto=format&n=XsEZlaGXYskrtN68&q=85&s=c528321ee8ce731435000c561537e60a" alt="The execution log" width="1116" height="345" data-path="images/image (93).webp" />
</Frame>

Чтобы играть в игру, переключитесь на второй аккаунт в MetaMask и снова нажмите `joinGame`. После завершения транзакции вы увидите похожий вывод в Execution History.

### 3.4. Проверьте Storage вашего Web3 Action

<Frame caption="Открытие выполнения вашего Web3 Action">
  <img src="https://mintcdn.com/tenderly/XsEZlaGXYskrtN68/images/image%20(94).webp?fit=max&auto=format&n=XsEZlaGXYskrtN68&q=85&s=0e7db1401134a1407e2857804a9ec87e" alt="Opening the execution of your Web3 Action" width="1763" height="728" data-path="images/image (94).webp" />
</Frame>

Откройте любое из этих выполнений и нажмите кнопку **«Go To Storage»**.

Игра, к которой присоединились наши игроки, имеет ID 11, и это состояние до того, как был сделан какой-либо ход. Доска Tic-Tac-Toe содержит только нули, и карта из Ethereum-адреса в ход игрока присутствует.

<Frame caption="Открытие значения ключа Storage ">
  <img src="https://mintcdn.com/tenderly/XsEZlaGXYskrtN68/images/image%20(91).webp?fit=max&auto=format&n=XsEZlaGXYskrtN68&q=85&s=d98b4d0220c38499bde2925eabcc1a6c" alt="Opening the Storage key value " width="1400" height="678" data-path="images/image (91).webp" />
</Frame>

### 3.5. Добавьте Action для обработки хода игрока

Используя Ethers, мы получаем ID игры и загружаем экземпляр игры из storage. Далее обновляем поле в строке `result.boardRow` и колонке `result.boardCol` вводом игрока `game.players[player]`.

Функция `processNewGameState` будет выводить доску в консоль, но также вы можете отправить твит при ходе, вызвать новую транзакцию в цепочке или использовать её любым другим способом.

```typescript title="example.ts" showLineNumbers theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
// ticTacToeActions.ts
//...
export const playerMadeMoveAction: ActionFn = async (context: Context, event: Event) => {
  let txEvent = event as TransactionEvent;
  let iface = new ethers.utils.Interface(TicTacToe.abi);
  const result = iface.decodeEventLog(
    'PlayerMadeMove',
    txEvent.logs[0].data,
    txEvent.logs[0].topics,
  );

  const gameId = result.gameId.toString();
  const game = (await context.storage.getJson(gameId)) as Game;
  const player = result.player.toLowerCase() as string;
  const { boardRow, boardCol } = result;

  console.log("Player's move event log:", {
    gameId,
    player,
    boardRow: boardRow.toString(),
    boardCol: boardCol.toString(),
  });

  console.log(
    `Move: gameId ${gameId}, game ${JSON.stringify(
      game,
    )}, boradRow ${boardRow}, boardCol ${boardCol}, player ${player}`,
  );

  game.board[boardRow][boardCol] = game.players[player];
  console.log('MV', JSON.stringify(game));
  await context.storage.putJson(gameId, game);

  processNewGameState(game);
};

const processNewGameState = (game: Game) => {
  let board = '\n';
  game.board.forEach(row => {
    row.forEach(field => {
      if (field == 1) {
        board += '❎ ';
        return;
      }

      if (field == 2) {
        board += '🅾️ ';
        return;
      }

      board += '💜 ';
    });

    board += '\n';
  });

  console.log(board);
};
```

Соответствующая спецификация для этого action:

```yaml title="example.yaml" showLineNumbers theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
playerMadeMove:
  description: Respond to player making a move event
  function: ticTacToeActions:playerMadeMoveAction
  trigger:
    type: transaction
    transaction:
      status:
        - mined
      filters:
        - network: 3
          eventEmitted:
            contract:
              address: TTT_CONTRACT_ADDRESS
            name: PlayerMadeMove
```

### 3.6. Сделайте ход, чтобы сыграть в игру

Чтобы сделать ход, используйте MetaMask для переключения на первого игрока, присоединившегося к игре. Далее введите полученный номер игры, а также строку и колонку на доске (0, 1).

<Frame caption="Ход в Remix ">
  <img src="https://mintcdn.com/tenderly/XsEZlaGXYskrtN68/images/image%20(88).webp?fit=max&auto=format&n=XsEZlaGXYskrtN68&q=85&s=d87ef8ceb06a0bbd9474af3eb12c2169" alt="Making a move in Remix " width="914" height="934" data-path="images/image (88).webp" />
</Frame>

Когда action выполнится, вывод должен выглядеть так:

<Frame caption="Вывод выполнения Web3 Action">
  <img src="https://mintcdn.com/tenderly/XsEZlaGXYskrtN68/images/image%20(95).webp?fit=max&auto=format&n=XsEZlaGXYskrtN68&q=85&s=781967b49bfe714430a3558cbdb95f5d" alt="The Web3 Action execution output" width="698" height="236" data-path="images/image (95).webp" />
</Frame>

## Шаг 4. Добавьте Web3 Action для обработки окончания игры

Ниже приведён код для события `GameOver`. Событие `GameOver` срабатывает, когда игрок делает ход, выигрывающий игру, или когда доска заполнена. Это событие срабатывает после события `PlayerMadeMove`. В списке `txEvent.logs` событие `GameOver` — это второй элемент.

Простой способ получить событие `GameOver` — обратиться к нему через `txEvent.logs[1]`. Однако мы реализуем более надёжное решение, не зависящее от порядка и количества сгенерированных событий.

Сначала нужно получить `gameOverTopic` через ethers с помощью `iface.getEventTopics`. Это даст вам соответствующее шестнадцатеричное значение. Далее нужно найти запись лога в `txEvent.logs`, список topic'ов которой содержит `gameOverTopic`. Это лог нашего события `GameOver`, который мы можем декодировать с помощью Ethers.

```javascript title="example.js" showLineNumbers theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
export const gameOverAction: ActionFn = async (context: Context, event: Event) => {
	let txEvent = event as TransactionEvent;
	let iface = new ethers.utils.Interface(TicTacToe.abi);

	const gameOverTopic = iface.getEventTopic("GameOver");
	const gameOverLog = txEvent.logs.find(log => log.topics.find(topic => topic == gameOverTopic) !== undefined);

	if (gameOverLog == undefined) {
		// impossible
		throw Error("GameOver log not found in event's logs");
	}

	const result = iface.decodeEventLog("GameOver", gameOverLog.data, gameOverLog.topics);

	console.log(result);

	const gameId = result.gameId.toString();
	const winner = result.winner as number;

	const winnerMessage = getWinnerMessage(winner);
	if (winnerMessage !== false) {
		console.info(`🎉 Winner of the game ${gameId} is ${winnerMessage}`);
	} else {
		console.error("🤔 weird winner code");
	}
}
```

### 4.1. Задайте вызов GameOver Action

Наконец, нам нужно добавить спецификацию для вызова action `GameOver`, расширив `specs` в `tenderly.yaml`:

```yaml title="example.yaml" showLineNumbers theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
gameOver:
  description: Respond to game over
  function: ticTacToeActions:gameOverAction
  trigger:
    type: transaction
    transaction:
      status:
        - mined
      filters:
        - network: 3
          eventEmitted:
            contract:
              address: TTT_CONTRACT_ADDRESS
            name: GameOver
```

### 4.2. Играйте в игру

Продолжайте играть, пока один из игроков не выиграет или доска не будет полностью заполнена. В конце игры Execution History должен выглядеть так:

<Frame caption="История выполнения в конце игры">
  <img src="https://mintcdn.com/tenderly/XsEZlaGXYskrtN68/images/image%20(78).webp?fit=max&auto=format&n=XsEZlaGXYskrtN68&q=85&s=eb6b64f3d40bbc512205c312c51d4c04" alt="The execution history at the end of the game" width="2542" height="826" data-path="images/image (78).webp" />
</Frame>
