Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use Tenderly testnets intead of forks for e2e tests #883

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/dex/generic-rfq/e2e-test-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@ export const testConfig: {
amount: string;
}>;
} = {
[Network.ARBITRUM]: [
[Network.MAINNET]: [
{
srcToken: 'WETH',
srcToken: 'USDT',
destToken: 'USDC',
amount: '10000',
swapSide: SwapSide.BUY,
},
{
srcToken: 'USDC',
destToken: 'WETH',
destToken: 'USDT',
amount: '10000',
swapSide: SwapSide.SELL,
},
Expand Down
14 changes: 8 additions & 6 deletions src/dex/generic-rfq/generic-rfq-e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,21 +115,23 @@ describe(`GenericRFQ ${dexKey} E2E`, () => {
`Please add "addBalance" and "addAllowance" functions for ${testCase.destToken} on ${Network[network]} (in constants-e2e.ts).`,
);
}
srcToken = smartTokens[testCase.srcToken];
destToken = smartTokens[testCase.destToken];
srcToken = new SmartToken(tokens[testCase.srcToken]);
destToken = new SmartToken(tokens[testCase.destToken]);

srcToken.addBalance(testAccount.address, MAX_UINT);
const amount = (BigInt(MAX_UINT) / 10n ** 10n).toString();

srcToken.addBalance(testAccount.address, amount);
srcToken.addAllowance(
testAccount.address,
config.augustusRFQAddress,
MAX_UINT,
amount,
);

destToken.addBalance(testAccount.address, MAX_UINT);
destToken.addBalance(testAccount.address, amount);
destToken.addAllowance(
testAccount.address,
config.augustusRFQAddress,
MAX_UINT,
amount,
);
}
const contractMethod =
Expand Down
2 changes: 1 addition & 1 deletion tests/constants-e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export const Tokens: {
address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
decimals: 6,
symbol: 'USDC',
addBalance: balancesFn,
addBalance: balanceAndBlacklistStatesFn,
addAllowance: allowedFn,
},
INST: {
Expand Down
4 changes: 1 addition & 3 deletions tests/smart-tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ export type StateOverride = {
};

export type StateSimulateApiOverride = {
storage: {
value: Record<string, string>;
};
storage: Record<string, string>;
};

export type StateOverrides = {
Expand Down
200 changes: 117 additions & 83 deletions tests/tenderly-simulation.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
/* eslint-disable no-console */
import axios from 'axios';
import { Address } from '@paraswap/core';
import { TxObject } from '../src/types';
import { StateOverrides, StateSimulateApiOverride } from './smart-tokens';
import { StaticJsonRpcProvider, Provider } from '@ethersproject/providers';
import Web3 from 'web3';
import { Provider, StaticJsonRpcProvider } from '@ethersproject/providers';
import { ethers } from 'ethers';
import { Network } from '../build/constants';
import { Address } from '@paraswap/core';

const TENDERLY_TOKEN = process.env.TENDERLY_TOKEN;
const TENDERLY_ACCOUNT_ID = process.env.TENDERLY_ACCOUNT_ID;
const TENDERLY_PROJECT = process.env.TENDERLY_PROJECT;
const TENDERLY_FORK_ID = process.env.TENDERLY_FORK_ID;
const TENDERLY_TEST_NET_RPC = process.env.TENDERLY_TEST_NET_RPC;
const TENDERLY_FORK_LAST_TX_ID = process.env.TENDERLY_FORK_LAST_TX_ID;
const TENDERLY_VNET_ID = process.env.TENDERLY_VNET_ID;

export type SimulationResult = {
success: boolean;
Expand All @@ -21,22 +20,28 @@ export type SimulationResult = {
};

export interface TransactionSimulator {
forkId: string;
vnetId: string;
setup(): Promise<void>;

getChainNameByChainId(network: number): string;

simulate(
params: TxObject,
stateOverrides?: StateOverrides,
): Promise<SimulationResult>;
}

export class EstimateGasSimulation implements TransactionSimulator {
forkId: string = '0';
vnetId: string = '0';

constructor(private provider: Provider) {}

async setup() {}

getChainNameByChainId(network: number) {
return '';
}

async simulate(
params: TxObject,
_: StateOverrides,
Expand All @@ -57,78 +62,91 @@ export class EstimateGasSimulation implements TransactionSimulator {
}

export class TenderlySimulation implements TransactionSimulator {
testNetRPC: StaticJsonRpcProvider | null = null;
lastTx: string = '';
forkId: string = '';
vnetId: string = '';
rpcURL: string = '';
maxGasLimit = 80000000;

constructor(
private network: Number = 1,
forkId?: string,
lastTransactionId?: string,
) {
if (forkId && lastTransactionId) {
this.forkId = forkId;
this.lastTx = lastTransactionId;
private readonly chainIdToChainNameMap: { [key: number]: string } = {
[Network.MAINNET]: 'mainnet',
[Network.BSC]: 'bnb',
[Network.POLYGON]: 'polygon',
[Network.AVALANCHE]: 'avalanche-mainnet',
[Network.FANTOM]: 'fantom',
[Network.ARBITRUM]: 'arbitrum',
[Network.OPTIMISM]: 'optimistic',
[Network.GNOSIS]: 'gnosis-chain',
[Network.BASE]: 'base',
};

constructor(private network: number = 1, vnetId?: string) {
if (vnetId) {
this.vnetId = vnetId;
}
}

getChainNameByChainId(network: number): string {
return this.chainIdToChainNameMap[network];
}

async setup() {
// Fork the mainnet
if (!TENDERLY_TOKEN)
throw new Error(
`TenderlySimulation_setup: TENDERLY_TOKEN not found in the env`,
);

if (this.forkId && this.lastTx) return;
if (this.vnetId) return;

if (TENDERLY_FORK_ID) {
if (!TENDERLY_FORK_LAST_TX_ID) throw new Error('Always set last tx id');
this.forkId = TENDERLY_FORK_ID;
this.lastTx = TENDERLY_FORK_LAST_TX_ID;
return;
}

if (TENDERLY_TEST_NET_RPC) {
this.testNetRPC = new StaticJsonRpcProvider(TENDERLY_TEST_NET_RPC);
if (TENDERLY_VNET_ID) {
this.vnetId = TENDERLY_VNET_ID;
return;
}

try {
await process.nextTick(() => {}); // https://stackoverflow.com/questions/69169492/async-external-function-leaves-open-handles-jest-supertest-express
let res = await axios.post(
`https://api.tenderly.co/api/v1/account/${TENDERLY_ACCOUNT_ID}/project/${TENDERLY_PROJECT}/fork`,
`https://api.tenderly.co/api/v1/account/${TENDERLY_ACCOUNT_ID}/project/${TENDERLY_PROJECT}/vnets`,
{
network_id: this.network.toString(),
slug: `e2e-tests-testnetwork-${this.network.toString()}-${Date.now()}`,
fork_config: {
network_id: this.network,
},
virtual_network_config: {
chain_config: {
chain_id: this.network,
},
},
sync_state_config: {
enabled: false,
},
explorer_page_config: {
enabled: true,
verification_visibility: 'bytecode',
},
},
{
timeout: 20000,
timeout: 200000,
headers: {
'x-access-key': TENDERLY_TOKEN,
'X-Access-Key': TENDERLY_TOKEN,
},
},
);
this.forkId = res.data.simulation_fork.id;
this.lastTx = res.data.root_transaction.id;

const rpc: { name: string; url: string } = res.data.rpcs.find(
(rpc: { name: string; url: string }) =>
rpc.name.toLowerCase() === 'Admin RPC'.toLowerCase(),
);

this.vnetId = res.data.id;
this.rpcURL = rpc.url;
} catch (e) {
console.error(`TenderlySimulation_setup:`, e);
throw e;
}
}

async simulate(params: TxObject, stateOverrides?: StateOverrides) {
let _params = {
from: params.from,
to: params.to,
save: true,
root: this.lastTx,
value: params.value || '0',
gas: this.maxGasLimit,
input: params.data,
state_objects: {},
};
try {
if (this.testNetRPC) return this.executeTransactionOnTestnet(params);
let stateOverridesParams = {};

if (stateOverrides) {
await process.nextTick(() => {}); // https://stackoverflow.com/questions/69169492/async-external-function-leaves-open-handles-jest-supertest-express
Expand All @@ -143,7 +161,7 @@ export class TenderlySimulation implements TransactionSimulator {
},
);

_params.state_objects = Object.keys(result.data.stateOverrides).reduce(
stateOverridesParams = Object.keys(result.data.stateOverrides).reduce(
(acc, contract) => {
const _storage = result.data.stateOverrides[contract].value;

Expand All @@ -154,12 +172,30 @@ export class TenderlySimulation implements TransactionSimulator {
},
{} as Record<Address, StateSimulateApiOverride>,
);

await this.executeStateOverrides(stateOverridesParams);
}

await process.nextTick(() => {}); // https://stackoverflow.com/questions/69169492/async-external-function-leaves-open-handles-jest-supertest-express
const { data } = await axios.post(
`https://api.tenderly.co/api/v1/account/${TENDERLY_ACCOUNT_ID}/project/${TENDERLY_PROJECT}/fork/${this.forkId}/simulate`,
_params,
`https://api.tenderly.co/api/v1/account/${TENDERLY_ACCOUNT_ID}/project/${TENDERLY_PROJECT}/vnets/${this.vnetId}/transactions`,
{
callArgs: {
from: params.from,
to: params.to,
value:
params.value === '0'
? '0x0'
: ethers.utils.hexStripZeros(
ethers.utils.hexlify(BigInt(params.value)),
),
gas: ethers.utils.hexStripZeros(
ethers.utils.hexlify(BigInt(this.maxGasLimit)),
),
data: params.data,
},
blockNumber: 'pending',
},
{
timeout: 30 * 1000,
headers: {
Expand All @@ -168,20 +204,22 @@ export class TenderlySimulation implements TransactionSimulator {
},
);

const lastTx = data.simulation.id;
if (data.transaction.status) {
this.lastTx = lastTx;
if (data.status === 'success') {
return {
success: true,
gasUsed: data.transaction.gas_used,
url: `https://dashboard.tenderly.co/${TENDERLY_ACCOUNT_ID}/${TENDERLY_PROJECT}/fork/${this.forkId}/simulation/${lastTx}`,
transaction: data.transaction,
gasUsed: data.gasUsed,
url: `https://dashboard.tenderly.co/${TENDERLY_ACCOUNT_ID}/${TENDERLY_PROJECT}/testnet/${
this.vnetId
}/tx/${this.chainIdToChainNameMap[this.network]}/${data.id}`,
transaction: data.input,
};
} else {
return {
success: false,
url: `https://dashboard.tenderly.co/${TENDERLY_ACCOUNT_ID}/${TENDERLY_PROJECT}/fork/${this.forkId}/simulation/${lastTx}`,
error: `Simulation failed: ${data.transaction.error_info.error_message} at ${data.transaction.error_info.address}`,
url: `https://dashboard.tenderly.co/${TENDERLY_ACCOUNT_ID}/${TENDERLY_PROJECT}/testnet/${
this.vnetId
}/tx/${this.chainIdToChainNameMap[this.network]}/${data.id}`,
error: `Simulation failed ${data.error_reason}`,
};
}
} catch (e) {
Expand All @@ -191,31 +229,27 @@ export class TenderlySimulation implements TransactionSimulator {
}
}

async executeTransactionOnTestnet(params: TxObject) {
const txParams = {
from: params.from,
to: params.to,
value: Web3.utils.toHex(params.value || '0'),
data: params.data,
gas: '0x4c4b40', // 5,000,000
gasPrice: '0x0', // 0
};
const txHash = await this.testNetRPC!.send('eth_sendTransaction', [
txParams,
]);
const transaction = await this.testNetRPC!.waitForTransaction(txHash);
if (transaction.status) {
return {
success: true,
url: txHash,
gasUsed: transaction.gasUsed.toString(),
transaction,
};
} else {
return {
success: false,
error: `Transaction on testnet failed, hash: ${txHash}`,
};
async executeStateOverrides(
stateOverridesParams: Record<string, StateSimulateApiOverride>,
) {
const testNetRPC = new StaticJsonRpcProvider(this.rpcURL);

// need to execute promises sequentially here
for await (const addr of Object.keys(stateOverridesParams)) {
const storage = stateOverridesParams[addr].storage;

for await (const slot of Object.keys(storage)) {
const txHash = await testNetRPC!.send('tenderly_setStorageAt', [
addr,
slot,
storage[slot],
]);

const transaction = await testNetRPC!.waitForTransaction(txHash);
if (!transaction.status) {
console.log(`Transaction failed: ${txHash}`);
}
}
}
}
}
Loading
Loading