How to Add Transaction Preview to a MetaMask Snap Using Tenderly Simulation API
This tutorial demonstrates how to add a “transaction preview” option to a wallet using Tenderly Simulation API. We’ll show you how to add this option to the MetaMask wallet. The “transaction preview” feature allows users to see the exact outcome of their transactions before submitting them to the production network. Wallets equipped with this feature make it easier for users to understand the financial implications of their transactions before making a commitment. Simulations are executed on a fork of the latest state of the blockchain in a safe, risk-free environment. This helps wallet users prevent sending malicious or error-prone transactions. You can find the complete demo repository on GitHub:Tenderly Simulate Asset Changes Snap
Tenderly MetaMask Snap Demo
Project overview
This project shows you how to integrate transaction simulations into the MetaMask wallet. We’ll be working with MetaMask Snaps. You’ll learn how to connect to the Tenderly Simulation API, send the transaction data, retrieve the simulation results, and display them to the user via the MetaMask UI. This includes asset changes in dollars, native-asset balance changes, output value, storage changes, event logs, and call traces. Simulated transactions can then also be opened in Tenderly for further debugging and testing. Below is a list of features that we’ll be adding to the MetaMask Snap. Similar features can also be added to any wallet to help users build confidence when sending transactions.| Feature | Description |
|---|---|
| Simulation debugging | Auto-generated link to inspect and debug the simulation in Tenderly. |
| Publicly sharable simulations | Ability to enable/disable public sharing of simulations. |
| Asset changes displayed in dollar value | Human-readable data, including asset changes automatically converted into dollars. |
| Native-asset balance changes | Ability to track changes in their native-asset balance during the execution of the contract. |
| Output value | See the result of the contract call, displaying what the contract is set to return. |
| Storage changes | See any changes made to the contract’s storage during the execution. |
| Event logs | See events that were emitted during the contract’s execution. |
| Call traces | Breakdown of all the function calls that happened during the execution. |
Prerequisites
- MetaMask Flask browser extension installed
- Node.js (version 16 or later)
- Yarn (version 3)
Before you start building the MetaMask Snap, make sure to disable the “real” version of MetaMask
and install the MetaMask Flask Development
Plugin.
Step 1: Setting up a MetaMask Snap project
Install MetaMask’s official Create Snap CLI and create a new project.example
example
example
Setting the correct permissions
Once you’ve installed the Snap, find the/packages/snap/snap.manifest.json file and set the permissions as shown below. Learn about MetaMask permissions here.
example.json
.webp?fit=max&auto=format&n=XsEZlaGXYskrtN68&q=85&s=fe280e95a931009bdc123e0018703f04)
Step 2: Generating Tenderly API access tokens
The transaction preview option in the MetaMask Snap is powered by the Tenderly Simulation API. Before you can start using the API, you need to generate access tokens. Follow these steps:- Log into Tenderly or create a free account here.
- Go to the Authorization page and click on the Generate Access Token button.
How to Generate API Access Tokens
.webp?fit=max&auto=format&n=XsEZlaGXYskrtN68&q=85&s=605d334b40293766a111dd1651f30d92)
Step 3: Writing the logic for the Snap
The core logic that makes the Snap work is stored in three files:- Credentials Access (
credentials-access.ts): Manages the Tenderly API authentication and access. - Simulation (
simulation.ts): The core logic for interacting with the Tenderly API to simulate transactions and fetch simulation results. - Formatter (
formatter.ts): Logic for parsing simulation results and formatting them in a user-friendly way in the MetaMask UI.
Credentials Access
We can store all methods responsible for requesting, updating, and fetching Tenderly API credentials in a file called credentials-access.ts. View the source code here. The most important methods include:fetchCredentials(): This function retrieves the credentials of a Tenderly project. If no credentials are stored, it triggershandleUpdateTenderlyCredentialsto request new ones.handleUpdateTenderlyCredentials(): This function handles the process of updating Tenderly project credentials. It gets new credentials by callingrequestNewTenderlyCredentialsand then saves them using thesnap_manageStatemethod with an'update'operation.requestNewTenderlyCredentials(): This function requests new Tenderly credentials. It callsrequestCredentialsto receive the raw credentials data and verifies its correctness before returning an object withaccountId,projectId, andaccessToken.
snap_manageState is a built-in method that allows the Snap to persist up to 100 MB of data to
the disk. Learn more here.
Simulation
Next, we need to create the logic that sends transaction data to the Tenderly Simulation API, simulates it, and retrieves the results. We’ll use thesimulate API endpoint for that.
Endpoint: https://api.tenderly.co/api/v1/account/{accountId}/project/{projectId}/simulate
To keep things organized, let’s store the simulation logic in a file called simulation.ts. View the source code here.
To send transaction data to the Tenderly Simulation API, we need to create two key methods:
- simulate(): This is the main function that handles the simulation of a transaction. It fetches the API access credentials, submits the transaction data to the Tenderly Simulation API, and handles any errors returned by the API.
example.tsx
- submitSimulation(): This function sends a request to the API along with the transaction data to be simulated.
example.tsx
Formatter
Once the API returns the simulation results, we need to parse this data and format it in a user-friendly way to be displayed in the MetaMask UI. We can store this logic in a file called formatter.ts. View the source code here. The core formatting methods include:formatResponse(): This function receives the raw data and credentials of a Tenderly project simulation, calls the individual formatter functions for each relevant section (like balance changes, output value, asset changes, etc.), and returns a panel with all the formatted outputs.formatBalanceDiff(): This function generates a panel showing balance changes for each account involved in the transaction.formatOutputValue(): This function creates a panel that presents the output values of the transaction if any exist. It also decodes the output, if possible.formatAssetChanges(): This function formats a panel to show any asset changes, differentiating between ERC20, ERC721, and other changes.formatStorageChanges(): This function creates a panel that lists any changes to storage that occurred during the transaction. It uniquely formats addresses and nested data structures for clarity.formatEventLogs(): This function presents the event logs, if they exist, for each transaction. The logs are formatted for readability and include input values.formatCallTrace(): This function produces a formatted visual hierarchy of call traces, showing nested calls recursively.formatSimulationUrl(): This function returns a link to the full details of the simulation on the Tenderly Dashboard and a separate shareable link.
Step 4: Displaying simulation data in the MetaMask UI
The MetaMask Snap mono repo provides a set of predefined UI elements and layout configurations. We’ll use these elements to extend the existing MetaMask UI to display the simulation results. When you install our MetaMask example, we show you how to run different types of simulations:- Successful transaction simulation with a predefined payload (successful ERC-20 transfer and NFT transfer)
- Failed transaction simulation with a predefined payload
- Custom transaction simulation with any payload
%20(1).webp?fit=max&auto=format&n=XsEZlaGXYskrtN68&q=85&s=c6f3c33067b72e0ac097fac8095498b0)
Successful simulation with a predefined payload
To initiate a simulation, we’ll write a function that will send the predefined transaction data from our wallet address and call theeth_sendTransaction RPC method.
example.tsx
eth_sendTransaction method call will trigger the onTransaction handler, which will call our custom simulate function, which was defined in simulation.ts.
example.tsx
onTransaction handler, the simulate function is called with the transaction and its origin as arguments.
The simulate function is responsible for fetching access credentials, submitting the transaction data to the Tenderly API, handling any errors, and formatting the API response for output.
The results of the simulation are displayed within the MetaMask UI. The images below show a successful ERC20 token transfer of 1 USDC to demo.eth. The output is generated by the functions defined in the formatter.ts file.
.webp?fit=max&auto=format&n=XsEZlaGXYskrtN68&q=85&s=bb92ed11d3534aae5065bb162b2d2a83)
formatAssetChanges() function.
You also get a link to the simulated transaction which you can open in Tenderly and inspect it further with Debugger or resimulate it with different values.
This link is generated by the formatSimulationUrl() function defined in the formatter.ts file.
example.ts
Failed transaction with a predefined payload
For the second example, we’ll simulate a failed transaction with a predefined payload. The image below shows a failed ERC20 token transfer of 1,000,000 USDC todemo.eth.
.webp?fit=max&auto=format&n=XsEZlaGXYskrtN68&q=85&s=aba83c7ebdcd5583f17f53c9b21fe306)
Simulation with a custom payload
To run a simulation with a custom payload, you need to add an object that aligns with the Ethereum transaction specification..webp?fit=max&auto=format&n=XsEZlaGXYskrtN68&q=85&s=3910b1ef37fc7a16edfc5718e7446df5)
from:DATA, 20 Bytes - The address the transaction is sent from.to:DATA, 20 Bytes - (optional when creating a new contract) The address the transaction is directed to.gas:QUANTITY- (optional, default: 90000) Integer of the gas provided for the transaction execution. It will return unused gas.gasPrice:QUANTITY- (optional, default: To-Be-Determined) Integer of thegasPriceused for each paid gas.value:QUANTITY- (optional) Integer of the value sent with this transaction.data:DATA- The compiled code of a contract OR the hash of the invoked method signature and encoded parameters.nonce:QUANTITY- (optional) Integer of a nonce. This allows you to overwrite your own pending transactions that use the same nonce.
.webp?fit=max&auto=format&n=XsEZlaGXYskrtN68&q=85&s=6544e62caa22bfd1c5affec7c1f52792)
sendTransaction function as before. This passes the custom payload to the onTransaction handler, which then triggers the simulate function to run the simulation.