Check out this Github repo to find the
source code for this project. Feel free to clone the repo and play around with it, or code along.
Understanding Web3 Actions
Smart contracts allow you to execute custom code when an event is triggered, a function is invoked, or perhaps periodically. Tenderly helps you streamline this process with Web3 Actions. You can write your Web3 Actions in a single Javascript file or deploy them as a NodeJS project. Web3 Actions also allow you to perform any action you normally would with NodeJS. This tutorial will show you how to use the Tenderly CLI to deploy your Web3 Actions and ensure they run when specific conditions are met.Learn more about Web3 Actions and explore how you can use them in your projects with Basics of
Web3 Actions.
Project Overview
The purpose of this project is to show you how to deploy a smart contract and write several Javascript functions that Tenderly will invoke when an event occurs. You will learn how to use the Tenderly CLI to deploy your Web3 Actions to Tenderly’s infrastructure. Here’s a step-by-step overview of how we will go about doing this:- Deploy the smart contract to a test network
- Use the Tenderly CLI to set up Javascript and config files needed to run the Web3 Actions
- Develop functions that respond to game events
- Deploy those functions to Tenderly’s infrastructure using the CLI
0: Deploy the smart contract
If you’re coding along, you can skip this step and use the contract we deployed and verified in
Tenderly.
- Game start
- Player joins the game
- Player makes a move
- Game over
Prerequisite
Create Two Accounts - deploy the smart contract to any network that is supported by Tenderly. The most convenient way to do this is with Remix and the Metamask wallet plugin. You need two accounts that have a positive Ether balance. These accounts will represent the two players. For the purpose of this tutorial we will use Sepolia. If you plan to follow along with this tutorial, you can use the Sepolia faucet to add Ether to the accounts. Compile the contract - use the Remix IDE to compile the smart contract. Create a new contract file and add the code found here..webp?fit=max&auto=format&n=XsEZlaGXYskrtN68&q=85&s=6f20b3090790b771b014692cdcf7be9a)
Injected Web3. Click “Deploy” once you’ve made sure that the account references the account in Metamask.
.webp?fit=max&auto=format&n=XsEZlaGXYskrtN68&q=85&s=a8cfef3f43f563adbb3dfdd62177b50f)
.webp?fit=max&auto=format&n=XsEZlaGXYskrtN68&q=85&s=4a2339153a44fa17921bce22e01e703b)
0x133...) and store it somewhere because we’ll need it for the steps that follow.
Now go to the Tenderly Dashboard to verify the contract. This will make it possible for us to interact with the contract later on in the tutorial.
1: Set Up Web3 Actions via the Tenderly CLI
To continue with this tutorial, you need to have the Tenderly CLI installed on your machine. Follow this guide to learn how to set it up and authenticate access. With the CLI installed, create a new directory andcd into it. Initialize your Web3 Actions by running the tenderly actions init command:
example
tenderly actions init --language javascript.
The
package.json holds npm dependencies which will be available when you deploy your Web3
Action. The tsconfig.json file holds Typescript
configuration related only to .ts files within the
actions directory.1.1. Add Tic-Tac-Toe Contract’s ABI
Before we dive deeper into the code, copy the ABI generated by the compiler to the actions directory. In our example, this is thetdly-actions directory.
In Remix, go to files/artifacts/TicTacToe.json, copy/paste the file contents and paste them into your project’s TicTacToe.json file.
1.2. Configure Typescript to Import a JSON File as a Module
By default, Typescript doesn’t allow you to import JSON files as modules. You need to configure Typescript to be able to import theTicTacToe.json file as a module and access it as an object.
Go to your tsconfig.json file and include the following two configurations under the compilerOptions entry:
example
2: Write a Function to Handle New Game Event
Write a function that will execute when a new game is started. Let’s rename the Typescript file to something more descriptive:example
players object.
2.1. Add an Action
example.ts
example.ts
GameCreated event that is defined in the smart contract.
Looking at the smart contract, we can see that only one event is getting fired from the newGame function, so we’re interested in the first log entry. We can get the result with ethers.js by decoding txEvent.logs[0].data of the GameCreated event, based on the TicTacToe.abi.
Here we can access the ID associated with this particular game once it has been created: result.gameId. We want to track the data for this particular game: players and moves they made, using a brand new Game instance. We’re saving the object representing the new game in the Storage with this command: context.storage.putJson(gameId, game).
With an empty board, we are setting the foundation to persist future changes which are handled by other actions.
You may also write automated tests to verify the behaviour of the Web3 Action. Some tests are available in the repository, but this is out of the scope of this tutorial.
2.2. Specify New Game Action Invocation
Open up tenderly.yaml. In thespecs section, define the specs for newGame (arbitrary name) to invoke the function newGameAction. You can do so like this newGameAction:newGameAction, first we define the name of the file containing the function and then the function name.
Next, specify the trigger that Tenderly is supposed to watch out for to invoke the action. This is a transaction trigger that will run when the block is mined. We’re doing this for network 3 when the event NewGame is emitted from the contract on the specified address.
Replace TTT_CONTRACT_ADDRESS with the actual address of your smart contract. If you want to deploy the contract to a network other than Sepolia, specify the network’s ID as the value of the network.
Replace
YOUR_USERNAME and
YOUR_PROJECT_SLUG with your Tenderly username and the
slug of your project. You can copy those from the Dashboard URL:example.yaml
2.3. Verify Your Tic-Tac-Toe Smart Contract on Tenderly
Before deploying the contract, you need to verify it in your Tenderly Dashboard. If you wish, you can upload the contract through your browser. Once uploaded, select the TicTacToe contract, the network you deployed it to, and the contract address. Click Add Contract and fill out the compiler options as shown in the picture below:.webp?fit=max&auto=format&n=XsEZlaGXYskrtN68&q=85&s=01b598bfc2a6fbdf63ddb1533f10d40c)
.webp?fit=max&auto=format&n=XsEZlaGXYskrtN68&q=85&s=e99c1eb47857101c573bddf8131cf40d)
2.4. Deploy the Web3 Action to Tenderly
To deploy your Web3 Action, execute thedeploy command using the Tenderly CLI:
example
.webp?fit=max&auto=format&n=XsEZlaGXYskrtN68&q=85&s=265837ec27e0378acfbc192074661c47)
.webp?fit=max&auto=format&n=XsEZlaGXYskrtN68&q=85&s=c35821d0a7d044029d8e1047cfa89ff6)
2.5. Try Joining a New Game 🎉
To verify that everything is working properly, head back to Remix and create a new game or even several games. Open up the TicTacToe contract and hitnewGame. Metamask should prompt you to confirm the execution of the Web3 Action.
Once the transaction is submitted to the chain, Remix should produce an output similar to this:
.webp?fit=max&auto=format&n=XsEZlaGXYskrtN68&q=85&s=a0aad1196ca95ba6470b8b5f0edcee12)
.webp?fit=max&auto=format&n=XsEZlaGXYskrtN68&q=85&s=7b2dedf972e0e4a6f9bc11fe27a33f85)
.webp?fit=max&auto=format&n=XsEZlaGXYskrtN68&q=85&s=ec2d6994130b322e35d858124d104898)
0xb, which means it’s the 11th game started for this contract.
2.6. Check the Storage of Your Web 3 Actions
Click the “Go To Storage” button to see the contents of the Storage. Each game that is initiated will get its own storage slot in this key-value map. When you open the game we just created with the ID 11, you’ll see zeros across all the fields, meaning no players have played the game yet..webp?fit=max&auto=format&n=XsEZlaGXYskrtN68&q=85&s=88f11391c94122ab621cac47f2bc8bdd)
3: Add a Web3 Action to Handle New Players Joining a Game
The player joining the game should be processed by registering the address of the player and their move (1 or 2). Besides the boilerplate code that helps us retrieve the event data, theplayerJoinAction.ts file also contains the code that allows us to:
- Read the current game state from the storage using the game’s ID (
storage.getJson). - Retrieve the player’s address and store their turn:
game.players[player] = playerNumber - Save the updated game object to the storage of Web3 Action using
storage.putJson.
example.ts
3.1. Specify PlayerJoinedGame Action Invocation
We also need to extend thespecs from the tenderly.yaml file to include the specifications needed to invoke the Web3 Action.
example.yaml
3.2. Deploy the action to Tenderly
Deploy your Web3 Action by running thedeploy command. This command will also redeploy previously deployed Web3 Actions.
example.js
3.3. Try Joining a New Game
Join a new game by going over to Remix and adding the game number from the logs in thenewGame input field. To submit the transaction, click the joinGame button:
.webp?fit=max&auto=format&n=XsEZlaGXYskrtN68&q=85&s=b775e77378e6a2706fe73eed98a3d79d)
playerNumber: 1.
.webp?fit=max&auto=format&n=XsEZlaGXYskrtN68&q=85&s=c528321ee8ce731435000c561537e60a)
joinGame again. Upon the completion of the transaction, you’ll see a similar log output in the Execution History.
3.4. Check the Storage of Your Web 3 Actions
.webp?fit=max&auto=format&n=XsEZlaGXYskrtN68&q=85&s=0e7db1401134a1407e2857804a9ec87e)
.webp?fit=max&auto=format&n=XsEZlaGXYskrtN68&q=85&s=d98b4d0220c38499bde2925eabcc1a6c)
3.5. Add an Action to Handle When Player Makes a Move
Using Ethers, we get the game ID and load the game instance from storage. Next, update the field in rowresult.boardRow and column result.boardCol with the player’s input game.players[player].
The function processNewGameState, will log the board to the console, but you can also send a tweet when a move is made, trigger a new transaction on the chain, or use it in any other way.
example.ts
example.yaml
3.6. Make a Move to Play the Game
To make a move, use Metamask to switch to the first player who joined the game. Next, enter the game number you received, as well as the row and column on the board (0, 1)..webp?fit=max&auto=format&n=XsEZlaGXYskrtN68&q=85&s=d87ef8ceb06a0bbd9474af3eb12c2169)
.webp?fit=max&auto=format&n=XsEZlaGXYskrtN68&q=85&s=781967b49bfe714430a3558cbdb95f5d)
Step 4. Add a Web3 Action to Handle When the Game is Over
Below you’ll find the code for theGameOver event. The GameOver event is fired when a player makes a move that wins the game or when the board is full. This event is fired after the PlayerMadeMove event. In the txEvent.logs list, the GameOver event is the second element.
An easy way to get GameOver event is to access it via the txEvent.logs[1]. However, we’ll implement a more robust solution which doesn’t depend on the order and number of events fired.
First, you need to get the gameOverTopic using ethers via iface.getEventTopics. This will give you the corresponding hexadecimal value. Next, you need to find the log entry in the txEvent.logs, whose topics list contains the gameOverTopic. This is our GameOver event log that we can decode using Ethers.
example.js
4.1. Specify GameOver Action Invocation
Finally, we need to add the specification to invoke theGameOver action by extending the specs in tenderly.yaml:
example.yaml
4.2. Play the Game
Keep playing until one player wins the game or the board is entirely filled out. At the end of the game, the Execution History should look like this:.webp?fit=max&auto=format&n=XsEZlaGXYskrtN68&q=85&s=eb6b64f3d40bbc512205c312c51d4c04)