Перейти к основному содержанию
Этот туториал научит вас создавать serverless-бэкенд для вашего смарт-контракта с помощью Tenderly Web3 Actions. Web3 Actions позволяют запускать пользовательский код в ответ на on-chain или off-chain события, инициированные вашим смарт-контрактом. Чтобы проиллюстрировать, как работают Web3 Actions, мы построим простую игру «крестики-нолики» и развернём её в тестовой сети. Смарт-контракт поддерживает состояние игры, а Web3 Actions отслеживают изменения в игре. Всякий раз, когда из смарт-контракта генерируется конкретное событие, Tenderly будет выполнять ваш пользовательский код в виде NodeJS-проекта. Результаты игры и игровое поле будут выводиться в консоль каждый раз, когда игрок делает ход или игра окончена.
Ознакомьтесь с этим Github-репозиторием, чтобы найти исходный код этого проекта. Свободно клонируйте репозиторий и экспериментируйте с ним, или кодите вместе с нами.

Понимание Web3 Actions

Смарт-контракты позволяют вам выполнять пользовательский код при срабатывании события, вызове функции или, возможно, периодически. Tenderly помогает упорядочить этот процесс с помощью Web3 Actions. Вы можете написать свои Web3 Actions в одном JavaScript-файле или развернуть как NodeJS-проект. Web3 Actions также позволяют выполнять любые действия, которые обычно делают с NodeJS. Этот туториал покажет, как использовать Tenderly CLI для развёртывания Web3 Actions и обеспечения их запуска при выполнении определённых условий.
Узнайте больше о Web3 Actions и о том, как использовать их в своих проектах, в Основах Web3 Actions.

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

Цель этого проекта — показать вам, как развернуть смарт-контракт и написать несколько JavaScript-функций, которые Tenderly будет вызывать при возникновении события. Вы узнаете, как использовать Tenderly CLI для развёртывания ваших Web3 Actions на инфраструктуре Tenderly. Вот пошаговый обзор того, как это сделать:
  1. Разверните смарт-контракт в тестовую сеть
  2. Используйте Tenderly CLI для настройки JavaScript- и конфигурационных файлов, необходимых для запуска Web3 Actions
  3. Разработайте функции, реагирующие на события игры
  4. Разверните эти функции на инфраструктуре Tenderly с помощью CLI
Для этого туториала вам понадобится доступ к Tenderly Dashboard. Если у вас нет аккаунта, зарегистрируйтесь здесь бесплатно (карта не требуется).

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

Если вы кодите вместе с нами, вы можете пропустить этот шаг и использовать контракт, который мы развернули и верифицировали в Tenderly.
Мы используем заранее написанный смарт-контракт. Ознакомьтесь с исходным кодом смарт-контракта на GitHub. Этот смарт-контракт спроектирован так, чтобы генерировать события всякий раз, когда происходит изменение состояния игры. Наша простая игра в крестики-нолики может иметь четыре возможных состояния:
  • Начало игры
  • Игрок присоединяется к игре
  • Игрок делает ход
  • Конец игры

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

Создайте два аккаунта — разверните смарт-контракт в любую сеть, поддерживаемую Tenderly. Наиболее удобный способ это сделать — с помощью Remix и плагина кошелька MetaMask. Вам нужны два аккаунта с положительным балансом Ether. Эти аккаунты будут представлять двух игроков. Для целей этого туториала мы используем Sepolia. Если вы планируете следовать этому туториалу, вы можете использовать кран Sepolia для пополнения аккаунтов эфиром. Скомпилируйте контракт — используйте IDE Remix для компиляции смарт-контракта. Создайте новый файл контракта и добавьте код, найденный здесь.
Compiling the contract in the Remix IDE
Разверните в тестовую сеть — в плагине MetaMask убедитесь, что выбрана Sepolia. Затем перейдите в раздел «Deploy And Run Transactions» в Remix и выберите Injected Web3. Нажмите «Deploy», убедившись, что аккаунт соответствует аккаунту в MetaMask.
Deploying the contract to a testnet
Copying the contract address
Чтобы получить больше информации о развёрнутом контракте, обратитесь к разделу «Deployed Contracts». Нажмите значок копирования, чтобы скопировать полный адрес контракта (0x133...), и сохраните его где-нибудь, так как он нам понадобится на следующих шагах. Теперь перейдите в Tenderly Dashboard, чтобы верифицировать контракт. Это позволит нам взаимодействовать с контрактом позже в туториале.

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

Чтобы продолжить туториал, на вашей машине должен быть установлен Tenderly CLI. Следуйте этому руководству, чтобы узнать, как его настроить и аутентифицировать доступ. С установленным CLI создайте новый каталог и перейдите в него (cd). Инициализируйте свои Web3 Actions, выполнив команду tenderly actions init:
$> cd tdly-actions
$> tenderly actions init
Вам будет предложено выбрать один из ваших существующих проектов Tenderly. Ваши Web3 Actions будут развёрнуты в этот проект и должны появиться в вашей структуре каталогов так:
example
$> ls actions
  example.ts     # where we write Web3 actions code
  tsconfig.json
  package.json
По умолчанию используется TypeScript, но вы можете переключиться на обычный JavaScript. Перед выполнением команды init задайте JavaScript как предпочитаемый язык так: tenderly actions init --language javascript.
package.json содержит npm-зависимости, которые будут доступны при развёртывании вашего Web3 Action. Файл tsconfig.json содержит конфигурацию TypeScript, относящуюся только к файлам .ts в каталоге actions.

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

Прежде чем углубляться в код, скопируйте ABI, сгенерированный компилятором, в каталог actions. В нашем примере это каталог tdly-actions. В Remix перейдите к files/artifacts/TicTacToe.json, скопируйте содержимое файла и вставьте его в файл TicTacToe.json вашего проекта.
Все файлы, которые использует и на которые ссылается код вашего Web3 Action, должны находиться в каталоге actions (например, TicTacToe.json).

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

По умолчанию TypeScript не позволяет импортировать JSON-файлы как модули. Нужно настроить TypeScript так, чтобы можно было импортировать файл TicTacToe.json как модуль и обращаться к нему как к объекту. Перейдите в файл tsconfig.json и добавьте следующие две настройки в раздел compilerOptions:
example
{
    ...
    "compilerOptions": {
	...
+        "resolveJsonModule": true,
+        "esModuleInterop": true
    },
   ...
}

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

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

2.1. Добавьте Action

example.ts
// ticTacToeActions.ts

export type Game = {
  players: { [address: string]: number };
  board: number[][];
};
На этом этапе мы готовы определить сам Web3 Action так:
example.ts
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.
Замените YOUR_USERNAME и YOUR_PROJECT_SLUG на ваше имя пользователя Tenderly и slug вашего проекта. Их можно скопировать из URL Dashboard: https://dashboard.tenderly.co/{YOUR_USERNAME}/{YOUR_PROJECT_SLUG}/transactions
example.yaml
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. При желании вы можете загрузить контракт через браузер. После загрузки выберите контракт TicTacToe, сеть, в которую вы его развернули, и адрес контракта. Нажмите Add Contract и заполните опции компилятора, как показано на картинке ниже:
Verifying and adding the contract to Tenderly
Adding the contract compiler information

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

Чтобы развернуть ваш Web3 Action, выполните команду deploy через Tenderly CLI:
example
tenderly actions deploy
Вывод должен выглядеть так:
The Web3 Action execution output
Чтобы увидеть детали развёртывания вашего Web3 Action, зайдите в Tenderly Dashboard. Если развёртывание прошло успешно, вы увидите что-то вроде этого:
Opening the Web3 Action deployment information

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

Чтобы убедиться, что всё работает правильно, вернитесь в Remix и создайте новую игру или несколько игр. Откройте контракт TicTacToe и нажмите newGame. MetaMask должен запросить у вас подтверждение выполнения Web3 Action. После того как транзакция отправлена в цепочку, Remix должен вывести результат, похожий на этот:
Creating a new game in Remix
Далее вернитесь в Tenderly Dashboard и откройте раздел Transactions, чтобы увидеть свою транзакцию. В разделе Actions вы заметите, что колонка «Latest Execution» изменилась. Чтобы просмотреть историю выполнения, откройте свой Web3 Action и нажмите вкладку Execution History:
Opening the Execution history tab for your Web3 Action
История выполнения предоставит вам следующие данные:
The execution history data
В верхней панели вы найдёте информацию о полезной нагрузке, приходящей из цепочки. Нижняя панель содержит детали о залогированном номере игры. В нашем случае это 0xb, что означает, что это 11-я игра, начатая для этого контракта.

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

Нажмите кнопку «Go To Storage», чтобы увидеть содержимое Storage. Каждая инициированная игра получит свой слот в этой карте ключ-значение. Когда вы откроете только что созданную игру с ID 11, вы увидите нули во всех полях, что означает, что ни один игрок ещё не играл.
Opening the Storage of your Web3 Action

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

Присоединение игрока к игре должно обрабатываться регистрацией адреса игрока и его хода (1 или 2). Помимо шаблонного кода, который помогает получить данные события, файл playerJoinAction.ts также содержит код, позволяющий:
  • Читать текущее состояние игры из storage по ID игры (storage.getJson).
  • Получать адрес игрока и сохранять его ход: game.players[player] = playerNumber
  • Сохранять обновлённый объект игры в storage Web3 Action с помощью storage.putJson.
example.ts
// 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.
example.yaml
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.
example.js
tenderly action deploy

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

Присоединитесь к новой игре, перейдя в Remix и добавив номер игры из логов в поле ввода newGame. Чтобы отправить транзакцию, нажмите кнопку joinGame:
Joining a new game in Remix
После того как транзакция заминирована, в логах выполнения будет отображаться playerNumber: 1.
The execution log
Чтобы играть в игру, переключитесь на второй аккаунт в MetaMask и снова нажмите joinGame. После завершения транзакции вы увидите похожий вывод в Execution History.

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

Opening the execution of your Web3 Action
Откройте любое из этих выполнений и нажмите кнопку «Go To Storage». Игра, к которой присоединились наши игроки, имеет ID 11, и это состояние до того, как был сделан какой-либо ход. Доска Tic-Tac-Toe содержит только нули, и карта из Ethereum-адреса в ход игрока присутствует.
Opening the Storage key value

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

Используя Ethers, мы получаем ID игры и загружаем экземпляр игры из storage. Далее обновляем поле в строке result.boardRow и колонке result.boardCol вводом игрока game.players[player]. Функция processNewGameState будет выводить доску в консоль, но также вы можете отправить твит при ходе, вызвать новую транзакцию в цепочке или использовать её любым другим способом.
example.ts
// 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:
example.yaml
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).
Making a move in Remix
Когда action выполнится, вывод должен выглядеть так:
The Web3 Action execution output

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

Ниже приведён код для события GameOver. Событие GameOver срабатывает, когда игрок делает ход, выигрывающий игру, или когда доска заполнена. Это событие срабатывает после события PlayerMadeMove. В списке txEvent.logs событие GameOver — это второй элемент. Простой способ получить событие GameOver — обратиться к нему через txEvent.logs[1]. Однако мы реализуем более надёжное решение, не зависящее от порядка и количества сгенерированных событий. Сначала нужно получить gameOverTopic через ethers с помощью iface.getEventTopics. Это даст вам соответствующее шестнадцатеричное значение. Далее нужно найти запись лога в txEvent.logs, список topic’ов которой содержит gameOverTopic. Это лог нашего события GameOver, который мы можем декодировать с помощью Ethers.
example.js
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:
example.yaml
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 должен выглядеть так:
The execution history at the end of the game