Using Simulation RPC in Dapp UI
Simulation RPC is compatible with Mainnet, Boba and Polygon, while other networks will be integrated gradually.\ Simulation API is compatible with all Tenderly-supported networks.
You can introduce simulations into your dapp by integrating Simulation RPC or Simulation API.
Benefits of integrating Simulation API
By integrating Simulation API/Simulation RPC
- Dry-run users’ transactions before they sign and submit them on-chain.
- Provide better visibility into potential rollbacks and avoid losing money on gas fees.
- Provide better visibility into the outcomes of successful apps, including moving assets and state changes relevant to the user.
- Build logic around simulation results to show users whether their transactions bring optimal value and suggest improvements.
Here are a few examples:
- If you’re building a wallet-like app, you can show how a transaction moves ERC-20 tokens and enable users to see the shifting of their funds.
- If you’re building a DeFi product, you can use events to analyze whether a transaction meets the optimal conditions set by the user in the UI.
- If you’re preparing a governance proposal, consisting of several transactions, you can do a simulation bundle to see how the proposal is applied.
- You can even bundle the proposal transactions with several test transactions to verify that your proposal executes as intended and identify possible blind spots.
Example: Adding Simulations to Uniswap UI
In this example, we’ll show how to integrate Simulation RPC into the Uniswap UI and show the exact token hops along the exchange path. This way, we’ll give the user higher visibility into the effects of the transaction and allow them to make a well-informed decision on whether or not to proceed.
The Simulation component requirements
Here are the requirements for the simulation component:
- The component should display the number of tokens the user has before and after signing and sending the transaction.
- The simulation should be done every time the trade data changes, so it needs to receive the
tradeandallowedSlippageparameters. - The component must keep track of:
- Current balances (before the swap).
- The
Swapevent we’ll use to calculate the new balances. - An array of
Transferevents we’ll use to calculate the new balances.
Render the Simulation into ConfirmSwapModal
The best place to inform the user about the outcomes of a transaction is ConfirmSwapModal, just before they can sign and send the transaction.
The code is redacted for brevity:
export default function ConfirmSwapModal({
/* ... */
}: {
/* ... */
}) {
// ...
const modalHeader = useCallback(() => {
return trade ? (
<>
<SwapModalHeader.../>
+ <Simulation trade={trade} recipient={null} allowedSlippage={allowedSlippage} />
</>
) : null
}, [/* ... */])
//...
The Simulation component implementation
The logic of the component lies in the simulate function:
- It runs in event of changing
tradeorproviderarguments, so it updates and re-simulates whenever the swap potentially changes. - It calls
setOldBalances, which gets the balances of In and Out ERC-20 tokens and stores them in theoldBalancesstate variable. - It calls
simulateRPC, which performs the Simulation RPC call totenderly_simulateTranscationand then:- It parses through decoded logs to find the
Transferevents and stores them in the component’stransfersstate variable. - It parses through the decoded logs to find the
Swapevent and stores it in the component’sswapstate variable.
- It parses through decoded logs to find the
Finally, to render the simulation results, we display the SimulationDetails component, which is a purely functional component. It calculates the old and new states and renders the data based on trade, swap, transfers, and oldBalances.
const Simulation = ({
trade,
allowedSlippage,
recipient,
}: {
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined;
recipient: string | null;
allowedSlippage: Percent;
}) => {
const { account, provider } = useWeb3React();
const nativeCurrency = useNativeCurrency();
const deadline = useTransactionDeadline();
const signatureData = useERC20PermitFromTrade(trade, allowedSlippage, deadline);
const args = useSwapCallArguments(
trade,
allowedSlippage,
recipient,
signatureData.signatureData,
deadline,
undefined,
);
const [oldBalances, setOldBalances] = useState<any[]>([]);
const [transfers, setTransfers] = useState<any[]>([]);
const [swap, setSwap] = useState<any>({});
useEffect(() => {
onSimulate();
}, [trade, provider]);
async function onSimulate() {
setOldBalances([]);
setSwap({});
const { address, calldata, value } = args[0];
const provider = RPC_PROVIDERS[SupportedChainId.MAINNET];
await getCurrentBalances();
await simulateRPC(account, address, calldata, value);
}
async function getCurrentBalances() {
return Promise.all([
provider.send('eth_getBalance', [account, 'latest']),
provider.send('eth_call', [
{
to: '0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984',
data: `0x70a0823100000000000000000000000000${stripX(account)}`,
},
'latest',
]),
]).then(oldBalances => {
setOldBalances(oldBalances);
});
}
async function simulateRPC(account: string, address: string, calldata: string, value: string) {
RPC_PROVIDERS[SupportedChainId.MAINNET]
.send('tenderly_simulateTransaction', [
{
from: account,
to: address,
data: calldata,
value: stripHexZero(value),
},
'latest', // block number to simulate on
null, // optional state overrides for involved contracts
])
.then(simulationResponse => {
console.log('Simulation Response', simulationResponse);
setTransfers(simulationResponse.logs.filter((log: any) => log.name === 'Transfer'));
setSwap(simulationResponse.logs.filter((log: any) => log.name === 'Swap')[0]);
});
}
return (
<>
...
<SimulationDetails
trade={trade}
syncing={oldBalances.length == 0 && transfers.length == 0 && !!swap.inputs}
transfers={transfers}
oldBalances={oldBalances}
swap={swap}
/>
</>
);
};