How to Add Transaction Preview to a Rabby Wallet Using Tenderly Simulation API
This tutorial shows how to integrate a âtransaction previewâ feature into a Rabby Wallet example using the Tenderly Simulation API.
Transaction Preview enables wallet users to simulate transaction execution and preview the exact outcomes without sending transactions on-chain. This way, users can better understand the financial implications of their transactions and send them with confidence. Additionally, you help protect usersâ funds, eliminate unnecessary gas costs, and improve the overall user experience and security of your product.
Hereâs the GitHub repository of this open-source example:
Tenderly Rabby Transaction Preview Demo
This tutorial is solely for illustrative purposes, showcasing how to integrate Transaction Preview into Rabby Wallet. Itâs not intended or recommended for any other usage.
Project overview
In this tutorial, youâll learn how to integrate the Tenderly Simulation API into the Rabby Wallet Chrome extension. First, youâll learn how to interact with the API and implement the transaction simulation logic. Then, youâll group the simulation data and expose it to the user. This way, youâll improve the user experience in this Rabby Wallet example by adding new functionalities.
Prerequisites
For this tutorial, you need:
- Node.js (version 14 or later)
- Yarn
- Knowledge of React.js, TypeScript, and Tailwind CSS
Letâs start building! đˇââď¸
Step 1: Clone the Rabby Wallet project
First, fork or clone the official Rabby Wallet GitHub repository. To do this, run:
git clone https://github.com/RabbyHub/Rabby.git
Next, navigate to the project root and install the dependencies using the Yarn package manager:
yarn install
Then, use the following command to run the repo locally:
yarn build:dev
The dist
folder will be generated. You can use it to load your Chrome extension.
Go to chrome://extensions
and enable Developer mode. Then, click the âLoad unpackedâ button, navigate to your Rabby directory, and select the dist
folder. The unpacked extension will be visible on the screen, and you can start using Rabby Wallet.
Now, you can import your existing wallet or create a new one.
Before you start, make sure to disable the production version of the Rabby Wallet Chrome Extension if you have it installed.
Please follow the official guidelines from the Rabby team in case you encounter any connection issues.
Step 2: Generate a Tenderly access token
To use the Tenderly Simulation API, you need to generate an access token. Follow these steps:
- Create a free Tenderly account or log into your existing one.
- Go to the Authorization page and click the Generate Access Token button.
- The token secret is displayed only once. Be sure to copy and store it somewhere safe as you wonât be able to retrieve it later.
Follow the guide below if you need any help with the API authentication process:
Step 3: Build the simulation logic
Next, weâll cover the following steps:
- Set up a secure storage system for the Tenderly credentials.
- Create the necessary code to connect with the Tenderly Simulation API.
- Use both private and public endpoints to interact with the API.
- Construct a proper payload for these endpoints.
Handling Tenderly Credentials
First, you need to retrieve the Tenderly credentials. Create a file /_raw/tenderly.json
and update the Tenderly credentials:
{
"tenderlyAccount": "YOUR_ACCOUNT_NAME",
"tenderlyProject": "YOUR_PROJECT_NAME",
"tenderlyAccessToken": "YOUR_ACCESS_TOKEN"
}
To access the Tenderly credentials in your project, you need to update build/webpack.common.config.js
with the following:
const { tenderlyAccount, tenderlyProject, tenderlyAccessToken } = require('../_raw/tenderly.json');
// other code...
const config = {
plugins: [
// abbreviated
new webpack.DefinePlugin({
'process.env.version': JSON.stringify(`version: ${version}`),
'process.env.release': JSON.stringify(version),
// Tenderly Credentials
'process.env.TENDERLY_ACCOUNT_SLUG': JSON.stringify(tenderlyAccount),
'process.env.TENDERLY_PROJECT_SLUG': JSON.stringify(tenderlyProject),
'process.env.TENDERLY_ACCESS_TOKEN': JSON.stringify(tenderlyAccessToken),
}),
],
resolve: {
// abbreviated
},
};
You can find the implementation here.
Implementing the transaction simulation logic
Next, create a src/ui/utils/tenderly.ts
file and implement the logic using the Tenderly Simulation API. Get the complete source code here.
import { Tx } from 'background/service/openapi';
import axios from 'axios';
export const simulateTransaction = async (tx: Tx): Promise<Record<string, any> | null> => {
const TENDERLY_ACCOUNT_SLUG = process.env.TENDERLY_ACCOUNT_SLUG;
const TENDERLY_PROJECT_SLUG = process.env.TENDERLY_PROJECT_SLUG;
const TENDERLY_ACCESS_TOKEN = process.env.TENDERLY_ACCESS_TOKEN;
if (!TENDERLY_ACCOUNT_SLUG || !TENDERLY_PROJECT_SLUG || !TENDERLY_ACCESS_TOKEN) {
throw new Error('Tenderly credentials are not set up correctly.');
}
const txPayload: any = {
...tx,
save: true,
save_if_fails: true,
simulation_type: 'full',
source: 'rabby-wallet',
network_id: tx.chainId.toString(),
gas: parseInt(tx.gas as string, 16) || null,
gas_price: parseInt(tx.gasPrice as string, 16) || null,
value: parseInt(tx.value, 16) || null,
input: tx.data,
};
try {
const simulationResponse = await axios.post(
`https://api.tenderly.co/api/v1/account/${TENDERLY_ACCOUNT_SLUG}/project/${TENDERLY_PROJECT_SLUG}/simulate`,
txPayload,
{
headers: {
'Content-Type': 'application/json',
'X-Access-Key': TENDERLY_ACCESS_TOKEN as string,
},
},
);
// Make the simulation publicly accessible
if (simulationResponse?.data?.simulation?.id) {
await axios.post(
`https://api.tenderly.co/api/v1/account/${TENDERLY_ACCOUNT_SLUG}/project/${TENDERLY_PROJECT_SLUG}/simulations/${simulationResponse.data.simulation.id}/share`,
{},
{
headers: {
'Content-Type': 'application/json',
'X-Access-Key': TENDERLY_ACCESS_TOKEN as string,
},
},
);
}
return simulationResponse?.data;
} catch (e) {
console.error({ e });
return null;
}
};
Here, we used two endpoints.
The first endpoint allows you to simulate a transaction with different parameters on any of the 78 networks supported by Tenderly. Go to our Simulation API reference to see all required and optional fields for this endpoint.
https://api.tenderly.co/api/v1/account/{accountId}/project/{projectId}/simulate
The second endpoint enables you to make a simulated transaction publicly accessible. This way, you can share it with your team members or associates for additional insights or quick issue resolution.
https://api.tenderly.co/api/v1/account/{accountId}/project/{projectId}/simulations/{simulationID}/share
The last step is to call the simulateTransaction
function within the SignTx.tx
component by placing it inside the explainTx
function, which is invoked before a transaction is signed. For code details, refer to this link.
Step 4: Display a simulated transaction preview
To help users understand what will happen when sending transactions, you can show human-readable simulation data in the UI. For instance, once wallet users simulate transactions, you can display which assets have been transferred.
In the Rabby Wallet example, we created three easy-to-understand sections:
Tenderly Simulation Summary
: Think of this as a transaction report card. It gives you the basic information about your transaction, with a direct link to a detailed simulation overview in the Tenderly Dashboard.Assets In & Assets Out
: Get a clear list of all assets (either ERC20 or NFT tokens) your wallet is about to interact with.Tokens Transferred
: View every token transfer that happens within a transaction. For added clarity, group all transfers by address and highlight native coin transfers.
Tenderly Simulation Summary
First, letâs create the Tenderly Simulation Summary section. Here, we show the simulation status, a network where the transaction is executed, and the gas value.
To build this section, you need to interact with the Tenderly Simulation API response. Start by creating a TenderlySimulationSummary.tsx
file. You can find the whole code implementation below or in the GitHub repo here.
import React from 'react';
import { Button } from 'antd';
import TenderlySvg from 'ui/assets/tenderly/tenderly.svg';
import { ReactComponent as IconRightArrow } from '@/ui/assets/arrow-right-gray.svg';
import { TenderlyNetwork } from 'ui/utils/tenderly';
import './style.less';
const TenderlySimulationSummary = ({ simulation }: { simulation: Record<string, any> | null }) => {
const isSuccess = !!simulation?.status;
const simulationUrl = `https://dashboard.tenderly.co/${process.env.TENDERLY_ACCOUNT_SLUG}/${process.env.TENDERLY_PROJECT_SLUG}/simulator/${simulation?.id}`;
const sharedSimulationUrl = `https://tdly.co/shared/simulation/${simulation?.id}`;
return (
<div className="tenderly-simulation-summary container">
<p className="text-16 text-gray-title font-medium mb-12">Tenderly Simulation Summary</p>
{!simulation && <div>Something went wrong.</div>}
{simulation && (
<div className="flex flex-col gap-8">
<div className="flex items-center justify-between gap-2">
<div>Status</div>
<div className={isSuccess ? 'text-green' : 'text-red'}>
{isSuccess ? 'Success' : 'Failed'}
</div>
</div>
<div className="flex items-center justify-between gap-2">
<div>Network</div>
<div className="flex items-center gap-4">
<div className="flex items-center rounded-full">
<img
src={TenderlyNetwork[simulation.network_id]?.image}
className="h-16 rounded-full"
alt="Tenderly"
/>
</div>
<div>{TenderlyNetwork[simulation.network_id]?.name}</div>
</div>
</div>
<div className="flex items-center justify-between gap-2">
<div>Gas</div>
<div>{simulation.gas}</div>
</div>
<Button
className="tenderly-simulation-summary-button flex items-center justify-between gap-2 pb-0"
href={simulationUrl}
target="_blank"
rel="noreferrer"
size="large"
block
>
<div>View details</div>
<div className="flex items-center gap-2">
<img src={TenderlySvg} className="h-32" alt="Tenderly" />
<IconRightArrow />
</div>
</Button>
<Button
className="text-[#6E56CF] bg-white flex items-center justify-center gap-2 pb-0"
href={sharedSimulationUrl}
target="_blank"
rel="noreferrer"
type="link"
size="middle"
block
>
<div>Share simulation â¨</div>
</Button>
</div>
)}
</div>
);
};
export default TenderlySimulationSummary;
Notice how we generated the links that lead users to the Tenderly Dashboard. The first link is for the authenticated users and the second one is a shareable transaction simulation link:
const simulationUrl = `https://dashboard.tenderly.co/${process.env.TENDERLY_ACCOUNT_SLUG}/${process.env.TENDERLY_PROJECT_SLUG}/simulator/${simulation?.id}`;
const sharedSimulationUrl = `https://tdly.co/shared/simulation/${simulation?.id}`;
To retrieve the data from a simulated transaction, refer to the Tenderly Simulation API reference:
Assets In & Assets Out
The second section provides insights into managing and monitoring the flow of assets within the Rabby Wallet example. It shows both the incoming and outgoing assets to give you a better view of the wallet activity.
To achieve this, start by creating a TenderlyWalletChanges.tsx
file:
import React from 'react';
import clsx from 'clsx';
import BigNumber from 'bignumber.js';
import { formatAmount } from 'ui/utils/number';
import { formatUsdValue } from 'ui/utils/number';
import { Account } from 'background/service/preference';
import { TenderlyCredits } from 'ui/views/Approval/components/TxComponents/Tenderly';
import './TenderlyWalletChanges.less';
const TenderlyWalletChanges = ({
account,
assetChanges,
}: {
account?: Account;
assetChanges: any;
}) => {
const { assetsInList, assetsOutList } = React.useMemo(() => {
const assetsInList = assetChanges.filter(asset => asset.to === account?.address) || [];
const assetsOutList = assetChanges.filter(asset => asset.from === account?.address) || [];
return {
assetsInList,
assetsOutList,
};
}, [account, assetChanges]);
return (
<div className="TenderlyWalletChanges">
<div className="TenderlyWalletChanges__Content">
{/* Assets Out */}
{assetsOutList?.length > 0 && (
<>
<p className="text-16 text-gray-title font-medium mb-4">Assets Out</p>
<div className="flex flex-col gap-4 border border-gray-divider rounded-sm">
{assetsOutList.map((token, index) => (
<div
key={index}
className={clsx('flex gap-4 p-4', {
'border-t-[1px] border-gray-divider': index !== 0,
})}
>
<div className="flex items-center justify-center p-4">
<img src={token.token_info.logo} className="w-60 rounded" alt="Tenderly" />
</div>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-1">
<div className="font-semibold">
{token.token_info.symbol.toUpperCase()}
{token.token_id && <span className="ml-2">#{Number(token.token_id)}</span>}
</div>
<div className="text-12 text-gray-content">{token.token_info.name}</div>
</div>
<div className="flex flex-wrap items-center gap-4">
<div className="font-semibold">{token.type}:</div>
<div className="text-red-forbidden">- {formatAmount(token.amount)}</div>
<div className="text-12 text-gray-content">
(â {formatUsdValue(new BigNumber(token.dollar_value).toFixed())})
</div>
</div>
</div>
</div>
))}
</div>
</>
)}
{/* Assets In */}
{assetsInList?.length > 0 && (
<>
<p className="text-16 text-gray-title font-medium mt-4 mb-4">Assets In</p>
<div className="flex flex-col gap-4 border border-gray-divider rounded-sm">
{assetsInList.map((token, index) => (
<div
key={index}
className={clsx('flex gap-4 p-4', {
'border-t-[1px] border-gray-divider': index !== 0,
})}
>
<div className="flex items-center justify-center p-4">
<img src={token.token_info.logo} className="w-60 rounded" alt="Tenderly" />
</div>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-1">
<div className="font-semibold">
{token.token_info.symbol.toUpperCase()}
{token.token_id && <span className="ml-2">#{Number(token.token_id)}</span>}
</div>
<div className="text-12 text-gray-content">{token.token_info.name}</div>
</div>
<div className="flex flex-wrap items-center gap-4">
<div className="font-semibold">{token.type}:</div>
<div className="text-green">+ {formatAmount(token.amount)}</div>
<div className="text-12 text-gray-content">
(â {formatUsdValue(new BigNumber(token.dollar_value).toFixed())})
</div>
</div>
</div>
</div>
))}
</div>
</>
)}
</div>
<TenderlyCredits classname="mt-12" />
</div>
);
};
export default TenderlyWalletChanges;
Here, we filtered out the asset changes array and created two lists:
- Assets getting into the wallet
- Assets going out of the wallet
Then, we used helper functions to preview and format the data.
However, you can also create your own UI layout to show the important asset information.
Learn more about simulation asset changes here:
Tokens Transferred
The Token Transfers section is a more complex component. This file primarily manages the layout, so letâs first discuss the sorting logic. You can find the implementation of this section here.
Start by creating a TenderlyAssetChanges.tsx
file. It contains the logic for presenting two tabs: Tokens Transferred and Native Coins Transferred. To do this, you need to add the logic for sorting, switching between tabs, and grouping the data by address.
First, letâs group the data and create two arrays: tokens transferred and native coins transferred:
const assetChangesMappedByType = React.useMemo<
Record<AssetChangesTabs, TransactionAssetChangeData[]>
>(
() =>
transactionAssetChanges.reduce(
(
acc: {
nativeCoinsTransferred: TransactionAssetChangeData[];
tokensTransferred: TransactionAssetChangeData[];
},
assetChange: TransactionAssetChangeData,
) => {
const { type } = assetChange.tokenInfo;
if (type === TokenType.NATIVE) {
return {
...acc,
[AssetChangesTabs.nativeCoins]: [...(acc?.nativeCoinsTransferred ?? []), assetChange],
};
}
return {
...acc,
[AssetChangesTabs.tokens]: [...(acc?.tokensTransferred ?? []), assetChange],
};
},
{ [AssetChangesTabs.nativeCoins]: [], [AssetChangesTabs.tokens]: [] },
),
[assetChanges],
);
Next, letâs write the logic for handling sorting types in two ways: chronologically and per address. Here, we need two components: ChronologicalContent.tsx and PerAddressContent.tsx.
You can find the implementation in the GitHub repository:
Grouping and previewing all components
Finally, letâs put all of the components together. To do this, you need to create a TenderlySimulationResult.tsx
file. You also need to use the useWallet()
hook provided by the Rabby team to interact with the connected wallet address.
Hereâs the code overview:
import React from 'react';
import { ActionWrapper } from 'ui/views/Approval/components/Actions';
import {
TenderlyAssetChanges,
TenderlySimulationSummary,
TenderlyWalletChanges,
} from 'ui/views/Approval/components/TxComponents/Tenderly';
import { useWallet } from 'ui/utils';
import { Account } from 'background/service/preference';
const TenderlySimulationResult = ({ data }: { data: Record<string, any> | null }) => {
const [account, setAccount] = React.useState<Account>();
const wallet = useWallet();
const init = async () => {
const currentAccount = await wallet.syncGetCurrentAccount();
if (currentAccount) {
setAccount(currentAccount);
}
};
React.useEffect(() => {
init();
}, []);
return (
<div className="tenderly-simulation-result mt-16">
<ActionWrapper>
{!data && <div className="bg-[#f5f6fa] sticky top-[64px] z-10">Something went wrong.</div>}
{data && <TenderlySimulationSummary simulation={data.simulation} />}
</ActionWrapper>
{data?.transaction?.transaction_info.asset_changes && (
<>
<ActionWrapper>
<TenderlyWalletChanges
account={account}
assetChanges={data.transaction.transaction_info.asset_changes}
/>
</ActionWrapper>
<ActionWrapper>
<TenderlyAssetChanges
assetChanges={data.transaction.transaction_info.asset_changes}
contracts={data.contracts}
/>
</ActionWrapper>
</>
)}
</div>
);
};
export default TenderlySimulationResult;
Whatâs next?
Congrats! You added the Transaction Preview feature to a Rabby Wallet example using the Tenderly Simulation API. đ
To dive into the logic, check out the complete source code on GitHub.
Additionally, you can initiate Tenderly transaction simulations using three different methods depending on your project requirements. To learn about other ways to integrate transaction simulations into your wallet or dapp, take a look at the following resources: