Dry-run User Transactions

Overview

Getting failed transactions is a kind of necessary evil when it comes to using dApps. But what if it doesnโ€™t need to be?
Here we are going to take a look at how to leverage Tenderly Simulations API to detect if a transaction would fail before you even send it on-chain.
Here are the steps to be taken in order to achieve this:
  1. 1.
    Populate raw transactions from ethers.js library.
  2. 2.
    Simulate transactions before sending them.
  3. 3.
    (optional) Wrap every blockchain interaction into a Tenderly Simulation.

Populate raw transactions from ethers.js

The first step is to create a transaction object that is going to be sent to our simulation endpoint. We are going to generate it directly from the ethers.js library. In the example below we are going to see how we can achieve that for transfer() function on top of DAI contract:
1
const dai = new ethers.Contract(DAI_ADDRESS , DAI_ABI , tenderlyForkProvider);
2
โ€‹
3
const unsignedTx = await dai.populateTransaction.transfer(ZERO_ADDRESS, YOUR_ADDRESS, util.ether(1));
Copied!

Simulate transactions before sending

Now that we have extracted an unsigned raw transaction, let's simulate it before sending it on-chain (to Ethereum Mainnet):
1
...
2
โ€‹
3
const body = {
4
"network_id": "1",
5
"from": senderAddr,
6
"to": contract.address,
7
"input": unsignedTx.data,
8
"gas": 21204,
9
"gas_price": "0",
10
"value": 0,
11
"save_if_fails": true
12
}
13
โ€‹
14
const headers = {
15
headers: {
16
'content-type': 'application/JSON',
17
'X-Access-Key': TENDERLY_ACCESS_KEY,
18
}
19
}
20
const resp = await axios.post(apiURL, body, headers);
21
โ€‹
22
if (resp.data.simulation.status === false) {
23
// it failed, do as you please
24
}
Copied!

(optional) Wrap every blockchain interaction into Tenderly Simulation

In order to ensure that every blockchain interaction is simulated first, you can write a simple wrapper around the ethers.js signer object that would always simulate transactions.
Additionally, going forwards, you can introduce typed errors with which your logic can interact:
1
export class TenderlySimulationSigner {
2
public _provider: ethers.Provider;
3
โ€‹
4
constructor(
5
provider: ethers.Provider,
6
) {
7
this._provider = provider;
8
}
9
โ€‹
10
public async sendTransaction(
11
transaction: Deferrable<TransactionRequest>
12
): Promise<TransactionResponse> {
13
await this._simulateTx(transaction)
14
15
return this._signer.sendTransaction(transaction);
16
}
17
โ€‹
18
public async getAddress(): Promise<string> {
19
return this._signer.getAddress();
20
}
21
โ€‹
22
public async signTransaction(
23
transaction: Deferrable<TransactionRequest>
24
): Promise<string> {
25
return this._signer.signTransaction(transaction);
26
}
27
โ€‹
28
_simulateTx(transaction: Deferrable<TransactionRequest>): Promise<void> {
29
const unsignedTx = await contract.populateTransaction[funcName](...args)
30
โ€‹
31
const apiURL = `https://api.tenderly.co/api/v1/account/me/project/project/simulate`
32
const body = {
33
"network_id": "1",
34
"from": senderAddr,
35
"to": contract.address,
36
"input": unsignedTx.data,
37
"gas": 21204,
38
"gas_price": "0",
39
"value": 0,
40
"save_if_fails": true
41
}
42
โ€‹
43
const headers = {
44
headers: {
45
'content-type': 'application/JSON',
46
'X-Access-Key': REACT_APP_TENDERLY_ACCESS_KEY as string,
47
}
48
}
49
const resp = await axios.post(apiURL, body, headers);
50
if (resp.data.simulation.status == false) {
51
throw new Error("Transaction is going to fail")
52
}
53
โ€‹
54
return;
55
}
56
}
Copied!
Here is the link to the GitHub repo where you can find an example of this implementation.