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_family = "wasm", async_trait::async_trait(?Send))]
21#[cfg_attr(not(target_family = "wasm"), 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_family = "wasm", async_trait::async_trait(?Send))]
102#[cfg_attr(not(target_family = "wasm"), 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().connect_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().connect_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().connect_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 =
330                    ProviderBuilder::new().wallet(wallet).connect_http(reth.endpoint_url());
331
332                let tx = TransactionRequest::default()
333                    .with_from(address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"))
334                    .value(U256::from(1000))
335                    .with_to(address!("0000000000000000000000000000000000000456"));
336
337                let res = provider.send_transaction(tx).await.unwrap();
338
339                let receipt = res.get_receipt().await.unwrap();
340
341                let hash = receipt.transaction_hash;
342
343                let result = provider.trace_replay_transaction(hash).await;
344                assert!(result.is_ok());
345
346                let traces = result.unwrap();
347                similar_asserts::assert_eq!(
348                    serde_json::to_string_pretty(&traces).unwrap(),
349                    r#"{
350  "output": "0x",
351  "stateDiff": null,
352  "trace": [
353    {
354      "type": "call",
355      "action": {
356        "from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
357        "callType": "call",
358        "gas": "0x0",
359        "input": "0x",
360        "to": "0x0000000000000000000000000000000000000456",
361        "value": "0x3e8"
362      },
363      "result": {
364        "gasUsed": "0x0",
365        "output": "0x"
366      },
367      "subtraces": 0,
368      "traceAddress": []
369    }
370  ],
371  "vmTrace": null
372}"#
373                );
374            })
375            .await;
376        })
377        .await;
378    }
379
380    #[tokio::test]
381    #[cfg_attr(windows, ignore = "no reth on windows")]
382    async fn trace_raw_tx() {
383        async_ci_only(|| async move {
384            run_with_tempdir("reth-test-", |temp_dir| async move {
385                let reth = Reth::new().dev().disable_discovery().data_dir(temp_dir).spawn();
386                let pk: PrivateKeySigner =
387                    "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
388                        .parse()
389                        .unwrap();
390
391                let provider = ProviderBuilder::new().connect_http(reth.endpoint_url());
392
393                let tx = TransactionRequest::default()
394                    .with_from(address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"))
395                    .gas_limit(21000)
396                    .nonce(0)
397                    .value(U256::from(1000))
398                    .with_chain_id(provider.get_chain_id().await.unwrap())
399                    .with_to(address!("0000000000000000000000000000000000000456"))
400                    .with_max_priority_fee_per_gas(1_000_000_000)
401                    .with_max_fee_per_gas(20_000_000_000);
402
403                let wallet = EthereumWallet::new(pk);
404
405                let raw = tx.build(&wallet).await.unwrap().encoded_2718();
406
407                let result = provider.trace_raw_transaction(&raw).await;
408
409                let traces = result.unwrap();
410
411                similar_asserts::assert_eq!(
412                    serde_json::to_string_pretty(&traces).unwrap(),
413                    r#"{
414  "output": "0x",
415  "stateDiff": null,
416  "trace": [
417    {
418      "type": "call",
419      "action": {
420        "from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
421        "callType": "call",
422        "gas": "0x0",
423        "input": "0x",
424        "to": "0x0000000000000000000000000000000000000456",
425        "value": "0x3e8"
426      },
427      "result": {
428        "gasUsed": "0x0",
429        "output": "0x"
430      },
431      "subtraces": 0,
432      "traceAddress": []
433    }
434  ],
435  "vmTrace": null
436}"#
437                );
438            })
439            .await;
440        })
441        .await;
442    }
443
444    #[tokio::test]
445    #[cfg_attr(windows, ignore = "no reth on windows")]
446    async fn trace_replay_block_transactions() {
447        async_ci_only(|| async move {
448            run_with_tempdir("reth-test-", |temp_dir| async move {
449                let reth = Reth::new().dev().disable_discovery().data_dir(temp_dir).spawn();
450                let pk: PrivateKeySigner =
451                    "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
452                        .parse()
453                        .unwrap();
454
455                let wallet = EthereumWallet::new(pk);
456                let provider =
457                    ProviderBuilder::new().wallet(wallet).connect_http(reth.endpoint_url());
458
459                let tx = TransactionRequest::default()
460                    .with_from(address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"))
461                    .value(U256::from(1000))
462                    .with_to(address!("0000000000000000000000000000000000000456"));
463
464                let res = provider.send_transaction(tx).await.unwrap();
465
466                let receipt = res.get_receipt().await.unwrap();
467
468                let block_num = receipt.block_number.unwrap();
469
470                let result =
471                    provider.trace_replay_block_transactions(BlockId::number(block_num)).await;
472                assert!(result.is_ok());
473
474                let traces = result.unwrap();
475                similar_asserts::assert_eq!(
476                    serde_json::to_string_pretty(&traces).unwrap().trim(),
477                    r#"[
478  {
479    "output": "0x",
480    "stateDiff": null,
481    "trace": [
482      {
483        "type": "call",
484        "action": {
485          "from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
486          "callType": "call",
487          "gas": "0x0",
488          "input": "0x",
489          "to": "0x0000000000000000000000000000000000000456",
490          "value": "0x3e8"
491        },
492        "result": {
493          "gasUsed": "0x0",
494          "output": "0x"
495        },
496        "subtraces": 0,
497        "traceAddress": []
498      }
499    ],
500    "vmTrace": null,
501    "transactionHash": "0x744426e308ba55f122913c74009be469da45153a941932d520aa959d8547da7b"
502  }
503]"#
504                    .trim()
505                );
506            })
507            .await;
508        })
509        .await;
510    }
511}