alloy_provider/ext/trace/
mod.rs

1//! This module extends the Ethereum JSON-RPC provider with the Trace namespace's RPC methods.
2use crate::Provider;
3use alloy_eips::BlockId;
4use alloy_network::Network;
5use alloy_primitives::TxHash;
6use alloy_rpc_types_eth::Index;
7use alloy_rpc_types_trace::{
8    filter::TraceFilter,
9    parity::{LocalizedTransactionTrace, TraceResults, TraceResultsWithTransactionHash, TraceType},
10};
11use alloy_transport::TransportResult;
12
13mod with_block;
14pub use with_block::{TraceBuilder, TraceParams};
15
16/// List of trace calls for use with [`TraceApi::trace_call_many`]
17pub type TraceCallList<'a, N> = &'a [(<N as Network>::TransactionRequest, &'a [TraceType])];
18
19/// Trace namespace rpc interface that gives access to several non-standard RPC methods.
20#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
21#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
22pub trait TraceApi<N>: Send + Sync
23where
24    N: Network,
25{
26    /// Executes the given transaction and returns a number of possible traces.
27    ///
28    /// Default trace type is [`TraceType::Trace`].
29    ///
30    /// # Note
31    ///
32    /// Not all nodes support this call.
33    fn trace_call<'a>(
34        &self,
35        request: &'a N::TransactionRequest,
36    ) -> TraceBuilder<&'a N::TransactionRequest, TraceResults>;
37
38    /// Traces multiple transactions on top of the same block, i.e. transaction `n` will be executed
39    /// on top of the given block with all `n - 1` transaction applied first.
40    ///
41    /// Allows tracing dependent transactions.
42    ///
43    /// If [`BlockId`] is unset the default at which calls will be executed is [`BlockId::pending`].
44    ///
45    /// # Note
46    ///
47    /// Not all nodes support this call.
48    fn trace_call_many<'a>(
49        &self,
50        request: TraceCallList<'a, N>,
51    ) -> TraceBuilder<TraceCallList<'a, N>, Vec<TraceResults>>;
52
53    /// Parity trace transaction.
54    async fn trace_transaction(
55        &self,
56        hash: TxHash,
57    ) -> TransportResult<Vec<LocalizedTransactionTrace>>;
58
59    /// Traces of the transaction on the given positions
60    ///
61    /// # Note
62    ///
63    /// This function accepts single index and build list with it under the hood because
64    /// trace_get method accepts list of indices but limits this list to len == 1.
65    async fn trace_get(
66        &self,
67        hash: TxHash,
68        index: usize,
69    ) -> TransportResult<LocalizedTransactionTrace>;
70
71    /// Trace the given raw transaction.
72    fn trace_raw_transaction<'a>(&self, data: &'a [u8]) -> TraceBuilder<&'a [u8], TraceResults>;
73
74    /// Traces matching given filter.
75    async fn trace_filter(
76        &self,
77        tracer: &TraceFilter,
78    ) -> TransportResult<Vec<LocalizedTransactionTrace>>;
79
80    /// Trace all transactions in the given block.
81    ///
82    /// # Note
83    ///
84    /// Not all nodes support this call.
85    async fn trace_block(&self, block: BlockId) -> TransportResult<Vec<LocalizedTransactionTrace>>;
86
87    /// Replays a transaction.
88    ///
89    /// Default trace type is [`TraceType::Trace`].
90    fn trace_replay_transaction(&self, hash: TxHash) -> TraceBuilder<TxHash, TraceResults>;
91
92    /// Replays all transactions in the given block.
93    ///
94    /// Default trace type is [`TraceType::Trace`].
95    fn trace_replay_block_transactions(
96        &self,
97        block: BlockId,
98    ) -> TraceBuilder<BlockId, Vec<TraceResultsWithTransactionHash>>;
99}
100
101#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
102#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
103impl<N, P> TraceApi<N> for P
104where
105    N: Network,
106    P: Provider<N>,
107{
108    fn trace_call<'a>(
109        &self,
110        request: &'a <N as Network>::TransactionRequest,
111    ) -> TraceBuilder<&'a <N as Network>::TransactionRequest, TraceResults> {
112        TraceBuilder::new_rpc(self.client().request("trace_call", request)).pending()
113    }
114
115    fn trace_call_many<'a>(
116        &self,
117        request: TraceCallList<'a, N>,
118    ) -> TraceBuilder<TraceCallList<'a, N>, Vec<TraceResults>> {
119        TraceBuilder::new_rpc(self.client().request("trace_callMany", request)).pending()
120    }
121
122    async fn trace_transaction(
123        &self,
124        hash: TxHash,
125    ) -> TransportResult<Vec<LocalizedTransactionTrace>> {
126        self.client().request("trace_transaction", (hash,)).await
127    }
128
129    async fn trace_get(
130        &self,
131        hash: TxHash,
132        index: usize,
133    ) -> TransportResult<LocalizedTransactionTrace> {
134        // We are using `[index]` because API accepts a list, but only supports a single index
135        self.client().request("trace_get", (hash, (Index::from(index),))).await
136    }
137
138    fn trace_raw_transaction<'a>(&self, data: &'a [u8]) -> TraceBuilder<&'a [u8], TraceResults> {
139        TraceBuilder::new_rpc(self.client().request("trace_rawTransaction", data))
140    }
141
142    async fn trace_filter(
143        &self,
144        tracer: &TraceFilter,
145    ) -> TransportResult<Vec<LocalizedTransactionTrace>> {
146        self.client().request("trace_filter", (tracer,)).await
147    }
148
149    async fn trace_block(&self, block: BlockId) -> TransportResult<Vec<LocalizedTransactionTrace>> {
150        self.client().request("trace_block", (block,)).await
151    }
152
153    fn trace_replay_transaction(&self, hash: TxHash) -> TraceBuilder<TxHash, TraceResults> {
154        TraceBuilder::new_rpc(self.client().request("trace_replayTransaction", hash))
155    }
156
157    fn trace_replay_block_transactions(
158        &self,
159        block: BlockId,
160    ) -> TraceBuilder<BlockId, Vec<TraceResultsWithTransactionHash>> {
161        TraceBuilder::new_rpc(self.client().request("trace_replayBlockTransactions", block))
162    }
163}
164
165#[cfg(test)]
166mod test {
167    use super::*;
168    use crate::{ext::test::async_ci_only, ProviderBuilder};
169    use alloy_eips::{BlockNumberOrTag, Encodable2718};
170    use alloy_network::{EthereumWallet, TransactionBuilder};
171    use alloy_node_bindings::{utils::run_with_tempdir, Reth};
172    use alloy_primitives::{address, U256};
173    use alloy_rpc_types_eth::TransactionRequest;
174    use alloy_signer_local::PrivateKeySigner;
175
176    #[tokio::test]
177    async fn trace_block() {
178        let provider = ProviderBuilder::new().on_anvil();
179        let traces = provider.trace_block(BlockId::Number(BlockNumberOrTag::Latest)).await.unwrap();
180        assert_eq!(traces.len(), 0);
181    }
182
183    #[tokio::test]
184    #[cfg_attr(windows, ignore = "no reth on windows")]
185    async fn trace_call() {
186        async_ci_only(|| async move {
187            run_with_tempdir("reth-test-", |temp_dir| async move {
188                let reth = Reth::new().dev().disable_discovery().data_dir(temp_dir).spawn();
189                let provider = ProviderBuilder::new().on_http(reth.endpoint_url());
190
191                let tx = TransactionRequest::default()
192                    .with_from(address!("0000000000000000000000000000000000000123"))
193                    .with_to(address!("0000000000000000000000000000000000000456"));
194
195                let result = provider.trace_call(&tx).await;
196
197                let traces = result.unwrap();
198                similar_asserts::assert_eq!(
199                    serde_json::to_string_pretty(&traces).unwrap().trim(),
200                    r#"
201{
202  "output": "0x",
203  "stateDiff": null,
204  "trace": [
205    {
206      "type": "call",
207      "action": {
208        "from": "0x0000000000000000000000000000000000000123",
209        "callType": "call",
210        "gas": "0x2fa9e78",
211        "input": "0x",
212        "to": "0x0000000000000000000000000000000000000456",
213        "value": "0x0"
214      },
215      "result": {
216        "gasUsed": "0x0",
217        "output": "0x"
218      },
219      "subtraces": 0,
220      "traceAddress": []
221    }
222  ],
223  "vmTrace": null
224}
225"#
226                    .trim(),
227                );
228            })
229            .await;
230        })
231        .await;
232    }
233
234    #[tokio::test]
235    #[cfg_attr(windows, ignore = "no reth on windows")]
236    async fn trace_call_many() {
237        async_ci_only(|| async move {
238            run_with_tempdir("reth-test-", |temp_dir| async move {
239                let reth = Reth::new().dev().disable_discovery().data_dir(temp_dir).spawn();
240                let provider = ProviderBuilder::new().on_http(reth.endpoint_url());
241
242                let tx1 = TransactionRequest::default()
243                    .with_from(address!("0000000000000000000000000000000000000123"))
244                    .with_to(address!("0000000000000000000000000000000000000456"));
245
246                let tx2 = TransactionRequest::default()
247                    .with_from(address!("0000000000000000000000000000000000000456"))
248                    .with_to(address!("0000000000000000000000000000000000000789"));
249
250                let result = provider
251                    .trace_call_many(&[(tx1, &[TraceType::Trace]), (tx2, &[TraceType::Trace])])
252                    .await;
253
254                let traces = result.unwrap();
255                similar_asserts::assert_eq!(
256                    serde_json::to_string_pretty(&traces).unwrap().trim(),
257                    r#"
258[
259  {
260    "output": "0x",
261    "stateDiff": null,
262    "trace": [
263      {
264        "type": "call",
265        "action": {
266          "from": "0x0000000000000000000000000000000000000123",
267          "callType": "call",
268          "gas": "0x2fa9e78",
269          "input": "0x",
270          "to": "0x0000000000000000000000000000000000000456",
271          "value": "0x0"
272        },
273        "result": {
274          "gasUsed": "0x0",
275          "output": "0x"
276        },
277        "subtraces": 0,
278        "traceAddress": []
279      }
280    ],
281    "vmTrace": null
282  },
283  {
284    "output": "0x",
285    "stateDiff": null,
286    "trace": [
287      {
288        "type": "call",
289        "action": {
290          "from": "0x0000000000000000000000000000000000000456",
291          "callType": "call",
292          "gas": "0x2fa9e78",
293          "input": "0x",
294          "to": "0x0000000000000000000000000000000000000789",
295          "value": "0x0"
296        },
297        "result": {
298          "gasUsed": "0x0",
299          "output": "0x"
300        },
301        "subtraces": 0,
302        "traceAddress": []
303      }
304    ],
305    "vmTrace": null
306  }
307]
308"#
309                    .trim()
310                );
311            })
312            .await;
313        })
314        .await;
315    }
316
317    #[tokio::test]
318    #[cfg_attr(windows, ignore = "no reth on windows")]
319    async fn test_replay_tx() {
320        async_ci_only(|| async move {
321            run_with_tempdir("reth-test-", |temp_dir| async move {
322                let reth = Reth::new().dev().disable_discovery().data_dir(temp_dir).spawn();
323                let pk: PrivateKeySigner =
324                    "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
325                        .parse()
326                        .unwrap();
327
328                let wallet = EthereumWallet::new(pk);
329                let provider = ProviderBuilder::new().wallet(wallet).on_http(reth.endpoint_url());
330
331                let tx = TransactionRequest::default()
332                    .with_from(address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"))
333                    .value(U256::from(1000))
334                    .with_to(address!("0000000000000000000000000000000000000456"));
335
336                let res = provider.send_transaction(tx).await.unwrap();
337
338                let receipt = res.get_receipt().await.unwrap();
339
340                let hash = receipt.transaction_hash;
341
342                let result = provider.trace_replay_transaction(hash).await;
343                assert!(result.is_ok());
344
345                let traces = result.unwrap();
346                similar_asserts::assert_eq!(
347                    serde_json::to_string_pretty(&traces).unwrap(),
348                    r#"{
349  "output": "0x",
350  "stateDiff": null,
351  "trace": [
352    {
353      "type": "call",
354      "action": {
355        "from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
356        "callType": "call",
357        "gas": "0x0",
358        "input": "0x",
359        "to": "0x0000000000000000000000000000000000000456",
360        "value": "0x3e8"
361      },
362      "result": {
363        "gasUsed": "0x0",
364        "output": "0x"
365      },
366      "subtraces": 0,
367      "traceAddress": []
368    }
369  ],
370  "vmTrace": null
371}"#
372                );
373            })
374            .await;
375        })
376        .await;
377    }
378
379    #[tokio::test]
380    #[cfg_attr(windows, ignore = "no reth on windows")]
381    async fn trace_raw_tx() {
382        async_ci_only(|| async move {
383            run_with_tempdir("reth-test-", |temp_dir| async move {
384                let reth = Reth::new().dev().disable_discovery().data_dir(temp_dir).spawn();
385                let pk: PrivateKeySigner =
386                    "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
387                        .parse()
388                        .unwrap();
389
390                let provider = ProviderBuilder::new().on_http(reth.endpoint_url());
391
392                let tx = TransactionRequest::default()
393                    .with_from(address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"))
394                    .gas_limit(21000)
395                    .nonce(0)
396                    .value(U256::from(1000))
397                    .with_chain_id(provider.get_chain_id().await.unwrap())
398                    .with_to(address!("0000000000000000000000000000000000000456"))
399                    .with_max_priority_fee_per_gas(1_000_000_000)
400                    .with_max_fee_per_gas(20_000_000_000);
401
402                let wallet = EthereumWallet::new(pk);
403
404                let raw = tx.build(&wallet).await.unwrap().encoded_2718();
405
406                let result = provider.trace_raw_transaction(&raw).await;
407
408                let traces = result.unwrap();
409
410                similar_asserts::assert_eq!(
411                    serde_json::to_string_pretty(&traces).unwrap(),
412                    r#"{
413  "output": "0x",
414  "stateDiff": null,
415  "trace": [
416    {
417      "type": "call",
418      "action": {
419        "from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
420        "callType": "call",
421        "gas": "0x0",
422        "input": "0x",
423        "to": "0x0000000000000000000000000000000000000456",
424        "value": "0x3e8"
425      },
426      "result": {
427        "gasUsed": "0x0",
428        "output": "0x"
429      },
430      "subtraces": 0,
431      "traceAddress": []
432    }
433  ],
434  "vmTrace": null
435}"#
436                );
437            })
438            .await;
439        })
440        .await;
441    }
442
443    #[tokio::test]
444    #[cfg_attr(windows, ignore = "no reth on windows")]
445    async fn trace_replay_block_transactions() {
446        async_ci_only(|| async move {
447            run_with_tempdir("reth-test-", |temp_dir| async move {
448                let reth = Reth::new().dev().disable_discovery().data_dir(temp_dir).spawn();
449                let pk: PrivateKeySigner =
450                    "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
451                        .parse()
452                        .unwrap();
453
454                let wallet = EthereumWallet::new(pk);
455                let provider = ProviderBuilder::new().wallet(wallet).on_http(reth.endpoint_url());
456
457                let tx = TransactionRequest::default()
458                    .with_from(address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"))
459                    .value(U256::from(1000))
460                    .with_to(address!("0000000000000000000000000000000000000456"));
461
462                let res = provider.send_transaction(tx).await.unwrap();
463
464                let receipt = res.get_receipt().await.unwrap();
465
466                let block_num = receipt.block_number.unwrap();
467
468                let result =
469                    provider.trace_replay_block_transactions(BlockId::number(block_num)).await;
470                assert!(result.is_ok());
471
472                let traces = result.unwrap();
473                similar_asserts::assert_eq!(
474                    serde_json::to_string_pretty(&traces).unwrap().trim(),
475                    r#"[
476  {
477    "output": "0x",
478    "stateDiff": null,
479    "trace": [
480      {
481        "type": "call",
482        "action": {
483          "from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
484          "callType": "call",
485          "gas": "0x0",
486          "input": "0x",
487          "to": "0x0000000000000000000000000000000000000456",
488          "value": "0x3e8"
489        },
490        "result": {
491          "gasUsed": "0x0",
492          "output": "0x"
493        },
494        "subtraces": 0,
495        "traceAddress": []
496      }
497    ],
498    "vmTrace": null,
499    "transactionHash": "0x744426e308ba55f122913c74009be469da45153a941932d520aa959d8547da7b"
500  }
501]"#
502                    .trim()
503                );
504            })
505            .await;
506        })
507        .await;
508    }
509}