Skip to main content

alloy_provider/ext/
tenderly.rs

1//! This module extends the Ethereum JSON-RPC provider with the Tenderly namespace's RPC methods.
2use crate::Provider;
3use alloy_eips::BlockNumberOrTag;
4use alloy_network::Network;
5use alloy_primitives::{Address, Bytes, TxHash, B256};
6use alloy_rpc_types_eth::{state::StateOverride, BlockOverrides};
7use alloy_rpc_types_tenderly::{
8    TenderlyDecodeInputResult, TenderlyEstimateGasResult, TenderlyFunctionSignature,
9    TenderlyGasPriceResult, TenderlySimulationResult, TenderlyStorageChange,
10    TenderlyStorageQueryParams, TenderlyTransactionRangeParams,
11};
12use alloy_transport::TransportResult;
13
14/// Tenderly namespace rpc interface that gives access to several non-standard RPC methods.
15#[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
16#[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
17pub trait TenderlyApi<N: Network>: Send + Sync {
18    /// Simulates a transaction as it would execute on the given block, allowing overrides of state
19    /// variables and balances of all accounts
20    async fn tenderly_simulate_transaction(
21        &self,
22        tx: N::TransactionRequest,
23        block: BlockNumberOrTag,
24        state_overrides: Option<StateOverride>,
25        block_overrides: Option<BlockOverrides>,
26    ) -> TransportResult<TenderlySimulationResult>;
27
28    /// Simulates a transaction as it would execute on the given block, allowing overrides of state
29    /// variables and balances of all accounts
30    async fn tenderly_simulate_bundle(
31        &self,
32        txs: &[N::TransactionRequest],
33        block: BlockNumberOrTag,
34        state_overrides: Option<StateOverride>,
35        block_overrides: Option<BlockOverrides>,
36    ) -> TransportResult<Vec<TenderlySimulationResult>>;
37
38    /// Replays transaction on the blockchain and provides information about the execution.
39    async fn tenderly_trace_transaction(
40        &self,
41        txs: &[TxHash],
42    ) -> TransportResult<TenderlySimulationResult>;
43
44    /// Estimates the gas required for a transaction to execute.
45    async fn tenderly_estimate_gas(
46        &self,
47        tx: N::TransactionRequest,
48        block: BlockNumberOrTag,
49    ) -> TransportResult<TenderlyEstimateGasResult>;
50
51    /// Gets the current gas price information with tiered pricing.
52    async fn tenderly_gas_price(&self) -> TransportResult<TenderlyGasPriceResult>;
53
54    /// Suggests gas fee information with tiered pricing.
55    async fn tenderly_suggest_gas_fee(&self) -> TransportResult<TenderlyGasPriceResult>;
56
57    /// Estimates the gas required for a bundle of transactions to execute.
58    async fn tenderly_estimate_gas_bundle(
59        &self,
60        txs: &[N::TransactionRequest],
61        block: BlockNumberOrTag,
62    ) -> TransportResult<Vec<TenderlyEstimateGasResult>>;
63
64    /// Heuristically decodes external function calls. Use for unverified contracts.
65    async fn tenderly_decode_input(
66        &self,
67        call_data: Bytes,
68    ) -> TransportResult<TenderlyDecodeInputResult>;
69
70    /// Heuristically decodes custom errors. Use for unverified contracts.
71    async fn tenderly_decode_error(
72        &self,
73        error_data: Bytes,
74    ) -> TransportResult<TenderlyDecodeInputResult>;
75
76    /// Retrieve function interface based on 4-byte function selector.
77    async fn tenderly_function_signatures(
78        &self,
79        selector: Bytes,
80    ) -> TransportResult<Vec<TenderlyFunctionSignature>>;
81
82    /// Heuristically decodes emitted events. Use for unverified contracts.
83    async fn tenderly_decode_event(
84        &self,
85        topics: Vec<B256>,
86        data: Bytes,
87    ) -> TransportResult<TenderlyDecodeInputResult>;
88
89    /// Retrieve error interface based on 4-byte error selector.
90    async fn tenderly_error_signatures(
91        &self,
92        selector: Bytes,
93    ) -> TransportResult<Vec<TenderlyFunctionSignature>>;
94
95    /// Retrieve event interface based on 32-byte event signature.
96    async fn tenderly_event_signature(
97        &self,
98        signature: B256,
99    ) -> TransportResult<TenderlyFunctionSignature>;
100
101    /// Returns an array of transactions between specified addresses within a given block range.
102    async fn tenderly_get_transactions_range(
103        &self,
104        params: TenderlyTransactionRangeParams,
105    ) -> TransportResult<Vec<N::TransactionResponse>>;
106
107    /// Returns the ABI for a given contract address.
108    ///
109    /// The ABI describes the contract's interface including function definitions, event
110    /// definitions, constructor arguments, and state variable definitions.
111    async fn tenderly_get_contract_abi(
112        &self,
113        address: Address,
114    ) -> TransportResult<Vec<serde_json::Value>>;
115
116    /// Returns an array of storage changes for a given contract address starting from the specified
117    /// offset.
118    ///
119    /// This method returns storage slot changes, block numbers where changes occurred,
120    /// transaction hashes that caused the changes, and previous and new values for each change.
121    /// The changes are returned in chronological order, with newer changes appearing first.
122    async fn tenderly_get_storage_changes(
123        &self,
124        params: TenderlyStorageQueryParams,
125    ) -> TransportResult<Vec<TenderlyStorageChange>>;
126}
127
128#[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
129#[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
130impl<N, P> TenderlyApi<N> for P
131where
132    N: Network,
133    P: Provider<N>,
134{
135    async fn tenderly_simulate_transaction(
136        &self,
137        tx: N::TransactionRequest,
138        block: BlockNumberOrTag,
139        state_overrides: Option<StateOverride>,
140        block_overrides: Option<BlockOverrides>,
141    ) -> TransportResult<TenderlySimulationResult> {
142        self.client()
143            .request("tenderly_simulateTransaction", (tx, block, state_overrides, block_overrides))
144            .await
145    }
146
147    async fn tenderly_simulate_bundle(
148        &self,
149        txs: &[N::TransactionRequest],
150        block: BlockNumberOrTag,
151        state_overrides: Option<StateOverride>,
152        block_overrides: Option<BlockOverrides>,
153    ) -> TransportResult<Vec<TenderlySimulationResult>> {
154        self.client()
155            .request("tenderly_simulateBundle", (txs, block, state_overrides, block_overrides))
156            .await
157    }
158
159    async fn tenderly_trace_transaction(
160        &self,
161        txs: &[TxHash],
162    ) -> TransportResult<TenderlySimulationResult> {
163        self.client().request("tenderly_traceTransaction", txs).await
164    }
165
166    async fn tenderly_estimate_gas(
167        &self,
168        tx: N::TransactionRequest,
169        block: BlockNumberOrTag,
170    ) -> TransportResult<TenderlyEstimateGasResult> {
171        self.client().request("tenderly_estimateGas", (tx, block)).await
172    }
173
174    async fn tenderly_gas_price(&self) -> TransportResult<TenderlyGasPriceResult> {
175        self.client().request_noparams("tenderly_gasPrice").await
176    }
177
178    async fn tenderly_suggest_gas_fee(&self) -> TransportResult<TenderlyGasPriceResult> {
179        self.client().request_noparams("tenderly_suggestGasFee").await
180    }
181
182    async fn tenderly_estimate_gas_bundle(
183        &self,
184        txs: &[N::TransactionRequest],
185        block: BlockNumberOrTag,
186    ) -> TransportResult<Vec<TenderlyEstimateGasResult>> {
187        self.client().request("tenderly_estimateGasBundle", (txs, block)).await
188    }
189
190    async fn tenderly_decode_input(
191        &self,
192        call_data: Bytes,
193    ) -> TransportResult<TenderlyDecodeInputResult> {
194        self.client().request("tenderly_decodeInput", (call_data,)).await
195    }
196
197    async fn tenderly_decode_error(
198        &self,
199        error_data: Bytes,
200    ) -> TransportResult<TenderlyDecodeInputResult> {
201        self.client().request("tenderly_decodeError", (error_data,)).await
202    }
203
204    async fn tenderly_function_signatures(
205        &self,
206        selector: Bytes,
207    ) -> TransportResult<Vec<TenderlyFunctionSignature>> {
208        self.client().request("tenderly_functionSignatures", (selector,)).await
209    }
210
211    async fn tenderly_decode_event(
212        &self,
213        topics: Vec<B256>,
214        data: Bytes,
215    ) -> TransportResult<TenderlyDecodeInputResult> {
216        self.client().request("tenderly_decodeEvent", (topics, data)).await
217    }
218
219    async fn tenderly_error_signatures(
220        &self,
221        selector: Bytes,
222    ) -> TransportResult<Vec<TenderlyFunctionSignature>> {
223        self.client().request("tenderly_errorSignatures", (selector,)).await
224    }
225
226    async fn tenderly_event_signature(
227        &self,
228        signature: B256,
229    ) -> TransportResult<TenderlyFunctionSignature> {
230        self.client().request("tenderly_eventSignature", (signature,)).await
231    }
232
233    async fn tenderly_get_transactions_range(
234        &self,
235        params: TenderlyTransactionRangeParams,
236    ) -> TransportResult<Vec<N::TransactionResponse>> {
237        self.client().request("tenderly_getTransactionsRange", (params,)).await
238    }
239
240    async fn tenderly_get_contract_abi(
241        &self,
242        address: Address,
243    ) -> TransportResult<Vec<serde_json::Value>> {
244        self.client().request("tenderly_getContractAbi", (address,)).await
245    }
246
247    async fn tenderly_get_storage_changes(
248        &self,
249        params: TenderlyStorageQueryParams,
250    ) -> TransportResult<Vec<TenderlyStorageChange>> {
251        self.client().request("tenderly_getStorageChanges", (params,)).await
252    }
253}
254
255#[cfg(test)]
256mod test {
257    use std::{env, str::FromStr};
258
259    use alloy_primitives::{address, bytes, utils::parse_ether, Address, U256};
260    use alloy_rpc_types_eth::{
261        state::{AccountOverride, StateOverridesBuilder},
262        TransactionRequest,
263    };
264
265    use crate::ProviderBuilder;
266
267    use super::*;
268
269    #[tokio::test]
270    #[ignore]
271    async fn test_tenderly_simulate_transaction_erc20() {
272        let url = env::var("TENDERLY_URL").unwrap().parse().unwrap();
273        let provider = ProviderBuilder::new().connect_http(url);
274
275        let gas_price = provider.get_gas_price().await.unwrap();
276        let block = BlockNumberOrTag::Latest;
277        let value = parse_ether("1").unwrap();
278
279        // send to WETH9 to cause an erc20 transfer
280        let tx = TransactionRequest::default()
281            .from(Address::ZERO)
282            .to(address!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"))
283            .value(value)
284            .max_fee_per_gas(gas_price + 1)
285            .max_priority_fee_per_gas(gas_price + 1);
286
287        let account_override = AccountOverride::default().with_balance(U256::MAX);
288        let state_override =
289            StateOverridesBuilder::default().append(Address::ZERO, account_override).build();
290
291        let _res = provider
292            .tenderly_simulate_transaction(tx, block, Some(state_override), None)
293            .await
294            .unwrap();
295    }
296
297    #[tokio::test]
298    #[ignore]
299    async fn test_tenderly_simulate_transaction_native() {
300        let url = env::var("TENDERLY_URL").unwrap().parse().unwrap();
301        let provider = ProviderBuilder::new().connect_http(url);
302
303        let gas_price = provider.get_gas_price().await.unwrap();
304        let block = BlockNumberOrTag::Latest;
305        let value = parse_ether("1").unwrap();
306
307        let tx = TransactionRequest::default()
308            .from(Address::ZERO)
309            .to(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"))
310            .value(value)
311            .max_fee_per_gas(gas_price + 1)
312            .max_priority_fee_per_gas(gas_price + 1);
313
314        let account_override = AccountOverride::default().with_balance(U256::MAX);
315        let state_override =
316            StateOverridesBuilder::default().append(Address::ZERO, account_override).build();
317
318        let _res = provider
319            .tenderly_simulate_transaction(tx, block, Some(state_override), None)
320            .await
321            .unwrap();
322    }
323
324    #[tokio::test]
325    #[ignore]
326    async fn test_tenderly_simulate_batch() {
327        let url = env::var("TENDERLY_URL").unwrap().parse().unwrap();
328        let provider = ProviderBuilder::new().connect_http(url);
329
330        let gas_price = provider.get_gas_price().await.unwrap();
331        let block = BlockNumberOrTag::Latest;
332        let value = parse_ether("1").unwrap();
333
334        let tx = TransactionRequest::default()
335            .from(Address::ZERO)
336            .to(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"))
337            .value(value)
338            .max_fee_per_gas(gas_price + 1)
339            .max_priority_fee_per_gas(gas_price + 1);
340
341        let account_override = AccountOverride::default().with_balance(U256::MAX);
342        let state_override =
343            StateOverridesBuilder::default().append(Address::ZERO, account_override).build();
344
345        let _res = provider
346            .tenderly_simulate_bundle(&[tx.clone(), tx], block, Some(state_override), None)
347            .await
348            .unwrap();
349    }
350
351    #[tokio::test]
352    #[ignore]
353    async fn test_tenderly_trace_transaction() {
354        let url = env::var("TENDERLY_URL").unwrap().parse().unwrap();
355        let provider = ProviderBuilder::new().connect_http(url);
356
357        let hash =
358            TxHash::from_str("0x6b2264fa8e28a641d834482d250080b39cbbf39251344573c7504d6137c4b793")
359                .unwrap();
360
361        let _res = provider.tenderly_trace_transaction(&[hash]).await.unwrap();
362    }
363
364    #[tokio::test]
365    #[ignore]
366    async fn test_tenderly_estimate_gas() {
367        let url = env::var("TENDERLY_URL").unwrap().parse().unwrap();
368        let provider = ProviderBuilder::new().connect_http(url);
369
370        let tx = TransactionRequest::default()
371            .from(address!("8516feaea147ea0db64d1c5b97bb651ca5435155"))
372            .to(address!("6b175474e89094c44da98b954eedeac495271d0f"))
373            .input(bytes!("a9059cbb0000000000000000000000003fc3c4c84bdd2db5ab2cc62f93b2a9a347de25fb00000000000000000000000000000000000000000000001869c36187f3430000").into());
374
375        let block = BlockNumberOrTag::Number(21285787);
376
377        let res = provider.tenderly_estimate_gas(tx, block).await.unwrap();
378
379        assert!(res.gas > 0);
380        assert!(res.gas_used > 0);
381    }
382
383    #[tokio::test]
384    #[ignore]
385    async fn test_tenderly_gas_price() {
386        let url = env::var("TENDERLY_URL").unwrap().parse().unwrap();
387        let provider = ProviderBuilder::new().connect_http(url);
388
389        let res = provider.tenderly_gas_price().await.unwrap();
390
391        assert!(res.current_block_number > 0);
392        assert!(res.base_fee_per_gas > 0);
393        assert!(res.price.low.max_priority_fee_per_gas > 0);
394        assert!(res.price.low.max_fee_per_gas > 0);
395        assert!(res.price.medium.max_priority_fee_per_gas > 0);
396        assert!(res.price.medium.max_fee_per_gas > 0);
397        assert!(res.price.high.max_priority_fee_per_gas > 0);
398        assert!(res.price.high.max_fee_per_gas > 0);
399    }
400
401    #[tokio::test]
402    #[ignore]
403    async fn test_tenderly_suggest_gas_fee() {
404        let url = env::var("TENDERLY_URL").unwrap().parse().unwrap();
405        let provider = ProviderBuilder::new().connect_http(url);
406
407        let res = provider.tenderly_suggest_gas_fee().await.unwrap();
408
409        assert!(res.current_block_number > 0);
410        assert!(res.base_fee_per_gas > 0);
411        assert!(res.price.low.max_priority_fee_per_gas > 0);
412        assert!(res.price.low.max_fee_per_gas > 0);
413        assert!(res.price.medium.max_priority_fee_per_gas > 0);
414        assert!(res.price.medium.max_fee_per_gas > 0);
415        assert!(res.price.high.max_priority_fee_per_gas > 0);
416        assert!(res.price.high.max_fee_per_gas > 0);
417    }
418
419    #[tokio::test]
420    #[ignore]
421    async fn test_tenderly_estimate_gas_bundle() {
422        let url = env::var("TENDERLY_URL").unwrap().parse().unwrap();
423        let provider = ProviderBuilder::new().connect_http(url);
424
425        let tx1 = TransactionRequest::default()
426            .from(address!("7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0"))
427            .to(address!("ae7ab96520DE3A18E5e111B5EaAb095312D7fE84"))
428            .input(bytes!("095ea7b30000000000000000000000009008d19f58aabd9ed0d60971565aa8510560ab410000000000000000000000000000000000000000000000000c1291a92f17a100").into());
429
430        let tx2 = TransactionRequest::default()
431            .from(address!("9008D19f58AAbD9eD0D60971565AA8510560ab41"))
432            .to(address!("ae7ab96520DE3A18E5e111B5EaAb095312D7fE84"))
433            .input(bytes!("23b872dd0000000000000000000000007f39c581f595b53c5cb19bd0b3f8da6c935e2ca00000000000000000000000009008d19f58aabd9ed0d60971565aa8510560ab410000000000000000000000000000000000000000000000000c1291a92f17a100").into());
434
435        let block = BlockNumberOrTag::Latest;
436
437        let res = provider.tenderly_estimate_gas_bundle(&[tx1, tx2], block).await.unwrap();
438
439        assert!(!res.is_empty());
440        assert_eq!(res.len(), 2);
441        assert!(res[0].gas > 0);
442        assert!(res[0].gas_used > 0);
443        assert!(res[1].gas > 0);
444        assert!(res[1].gas_used > 0);
445    }
446
447    #[tokio::test]
448    #[ignore]
449    async fn test_tenderly_decode_input() {
450        let url = env::var("TENDERLY_URL").unwrap().parse().unwrap();
451        let provider = ProviderBuilder::new().connect_http(url);
452
453        let call_data = bytes!("a9059cbb00000000000000000000000011223344551122334455112233445511223344550000000000000000000000000000000000000000000000000000000000000999");
454
455        let res = provider.tenderly_decode_input(call_data).await.unwrap();
456
457        assert!(!res.name.is_empty());
458        assert!(!res.decoded_arguments.is_empty());
459    }
460}