Skip to main content

chainindex_core/
trace.rs

1//! Call trace indexing — index internal transactions (CALL, DELEGATECALL, CREATE)
2//! from `debug_traceBlockByNumber` or `trace_block` responses.
3//!
4//! # Overview
5//!
6//! Ethereum event logs only capture explicitly emitted events. Many important
7//! operations (ETH transfers via internal calls, contract creations, delegate
8//! calls) are only visible in execution traces. This module provides:
9//!
10//! - [`CallTrace`] — a structured representation of a single internal call.
11//! - [`TraceFilter`] — declarative filtering by address, selector, call type.
12//! - [`TraceHandler`] — async trait for user-provided trace processing logic.
13//! - [`TraceRegistry`] — handler dispatch with filtering.
14//! - [`parse_geth_traces`] — parse Geth `debug_traceBlockByNumber` JSON.
15//! - [`parse_parity_traces`] — parse OpenEthereum/Parity `trace_block` JSON.
16//!
17//! # Example
18//!
19//! ```rust,no_run
20//! use chainindex_core::trace::{CallTrace, TraceFilter, CallType};
21//!
22//! let filter = TraceFilter::new()
23//!     .with_address("0xdead")
24//!     .with_call_type(CallType::Call)
25//!     .exclude_reverted(true);
26//! ```
27
28use std::collections::HashSet;
29use std::sync::Arc;
30
31use async_trait::async_trait;
32use serde::{Deserialize, Serialize};
33
34use crate::error::IndexerError;
35use crate::types::IndexContext;
36
37// ─── CallType ───────────────────────────────────────────────────────────────
38
39/// Type of EVM call/operation captured in a trace.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
41pub enum CallType {
42    /// Standard `CALL` opcode.
43    Call,
44    /// `DELEGATECALL` — executes callee code in caller's storage context.
45    DelegateCall,
46    /// `STATICCALL` — read-only call (reverts on state changes).
47    StaticCall,
48    /// `CREATE` opcode — deploys a new contract.
49    Create,
50    /// `CREATE2` opcode — deploys with deterministic address.
51    Create2,
52    /// `SELFDESTRUCT` opcode — destroys the contract.
53    SelfDestruct,
54}
55
56impl CallType {
57    /// Parse a call type string from Geth trace output.
58    ///
59    /// Geth uses uppercase strings like `"CALL"`, `"DELEGATECALL"`, etc.
60    pub fn from_geth(s: &str) -> Option<Self> {
61        match s.to_uppercase().as_str() {
62            "CALL" => Some(Self::Call),
63            "DELEGATECALL" => Some(Self::DelegateCall),
64            "STATICCALL" => Some(Self::StaticCall),
65            "CREATE" => Some(Self::Create),
66            "CREATE2" => Some(Self::Create2),
67            "SELFDESTRUCT" => Some(Self::SelfDestruct),
68            _ => None,
69        }
70    }
71
72    /// Parse a call type string from Parity/OpenEthereum trace output.
73    ///
74    /// Parity uses lowercase strings and a different naming convention:
75    /// `"call"`, `"delegatecall"`, `"staticcall"`, `"create"`, `"suicide"`.
76    pub fn from_parity(s: &str) -> Option<Self> {
77        match s.to_lowercase().as_str() {
78            "call" => Some(Self::Call),
79            "delegatecall" => Some(Self::DelegateCall),
80            "staticcall" => Some(Self::StaticCall),
81            "create" => Some(Self::Create),
82            "create2" => Some(Self::Create2),
83            "suicide" | "selfdestruct" => Some(Self::SelfDestruct),
84            _ => None,
85        }
86    }
87}
88
89// ─── CallTrace ──────────────────────────────────────────────────────────────
90
91/// A single call trace (internal transaction).
92///
93/// Represents one node in the call tree of a transaction. Top-level external
94/// calls have `depth = 0`; internal calls increment the depth.
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct CallTrace {
97    /// The type of call (CALL, DELEGATECALL, CREATE, etc.).
98    pub call_type: CallType,
99    /// Sender address (`0x…`).
100    pub from: String,
101    /// Recipient/target address (`0x…`). For CREATE, this is the new contract.
102    pub to: String,
103    /// Value transferred in wei (decimal string, e.g. `"1000000000000000000"`).
104    pub value: String,
105    /// Gas consumed by this call.
106    pub gas_used: u64,
107    /// Input data (hex-encoded with `0x` prefix).
108    pub input: String,
109    /// Output/return data (hex-encoded with `0x` prefix).
110    pub output: String,
111    /// First 4 bytes of input — the function selector (e.g. `"0xa9059cbb"`).
112    /// `None` if input is too short (< 4 bytes after `0x` prefix).
113    pub function_selector: Option<String>,
114    /// Call depth: 0 for top-level, increments for nested calls.
115    pub depth: u32,
116    /// Block number containing the transaction.
117    pub block_number: u64,
118    /// Transaction hash.
119    pub tx_hash: String,
120    /// Transaction index within the block.
121    pub tx_index: u32,
122    /// Trace index within the transaction (sequential ordering).
123    pub trace_index: u32,
124    /// Error message if the call failed.
125    pub error: Option<String>,
126    /// Whether this call (or a parent call) was reverted.
127    pub reverted: bool,
128}
129
130impl CallTrace {
131    /// Extract the function selector (first 4 bytes) from hex-encoded input.
132    ///
133    /// Returns `None` if input is `"0x"` or shorter than 10 characters
134    /// (i.e., `0x` + 8 hex chars = 4 bytes).
135    pub fn extract_selector(input: &str) -> Option<String> {
136        let hex = input.strip_prefix("0x").unwrap_or(input);
137        if hex.len() >= 8 {
138            Some(format!("0x{}", &hex[..8].to_lowercase()))
139        } else {
140            None
141        }
142    }
143}
144
145// ─── TraceFilter ────────────────────────────────────────────────────────────
146
147/// Declarative filter for which traces to process.
148///
149/// All filter fields are "AND"-ed: a trace must match all non-empty criteria.
150/// Empty/default fields match everything.
151#[derive(Debug, Clone, Default, Serialize, Deserialize)]
152pub struct TraceFilter {
153    /// Only include traces involving these addresses (as `from` or `to`).
154    /// Empty = match all addresses.
155    pub addresses: HashSet<String>,
156    /// Only include traces with these function selectors.
157    /// Empty = match all selectors.
158    pub selectors: HashSet<String>,
159    /// Only include these call types. Empty = match all types.
160    pub call_types: HashSet<CallType>,
161    /// If `true`, exclude traces that reverted.
162    pub exclude_reverted: bool,
163    /// Minimum call depth to include (e.g., 1 = skip top-level calls).
164    pub min_depth: Option<u32>,
165    /// Maximum call depth to include.
166    pub max_depth: Option<u32>,
167}
168
169impl TraceFilter {
170    /// Create a new empty filter (matches everything).
171    pub fn new() -> Self {
172        Self::default()
173    }
174
175    /// Add an address to filter on (as `from` or `to`).
176    pub fn with_address(mut self, addr: impl Into<String>) -> Self {
177        self.addresses.insert(addr.into().to_lowercase());
178        self
179    }
180
181    /// Add a function selector to filter on.
182    pub fn with_selector(mut self, selector: impl Into<String>) -> Self {
183        self.selectors.insert(selector.into().to_lowercase());
184        self
185    }
186
187    /// Add a call type to filter on.
188    pub fn with_call_type(mut self, call_type: CallType) -> Self {
189        self.call_types.insert(call_type);
190        self
191    }
192
193    /// Set whether to exclude reverted traces.
194    pub fn exclude_reverted(mut self, exclude: bool) -> Self {
195        self.exclude_reverted = exclude;
196        self
197    }
198
199    /// Set the minimum call depth.
200    pub fn min_depth(mut self, depth: u32) -> Self {
201        self.min_depth = Some(depth);
202        self
203    }
204
205    /// Set the maximum call depth.
206    pub fn max_depth(mut self, depth: u32) -> Self {
207        self.max_depth = Some(depth);
208        self
209    }
210
211    /// Check whether a trace matches this filter.
212    pub fn matches(&self, trace: &CallTrace) -> bool {
213        // Check reverted.
214        if self.exclude_reverted && trace.reverted {
215            return false;
216        }
217
218        // Check call type.
219        if !self.call_types.is_empty() && !self.call_types.contains(&trace.call_type) {
220            return false;
221        }
222
223        // Check addresses (from OR to).
224        if !self.addresses.is_empty() {
225            let from_lower = trace.from.to_lowercase();
226            let to_lower = trace.to.to_lowercase();
227            if !self.addresses.contains(&from_lower) && !self.addresses.contains(&to_lower) {
228                return false;
229            }
230        }
231
232        // Check function selector.
233        if !self.selectors.is_empty() {
234            match &trace.function_selector {
235                Some(sel) => {
236                    if !self.selectors.contains(&sel.to_lowercase()) {
237                        return false;
238                    }
239                }
240                None => return false,
241            }
242        }
243
244        // Check depth bounds.
245        if let Some(min) = self.min_depth {
246            if trace.depth < min {
247                return false;
248            }
249        }
250        if let Some(max) = self.max_depth {
251            if trace.depth > max {
252                return false;
253            }
254        }
255
256        true
257    }
258}
259
260// ─── TraceHandler ───────────────────────────────────────────────────────────
261
262/// Trait for user-provided trace processing logic.
263///
264/// Implement this to react to call traces during indexing (e.g., track ETH
265/// transfers, monitor contract creations, record internal function calls).
266#[async_trait]
267pub trait TraceHandler: Send + Sync {
268    /// Called for each trace that passes the handler's filter.
269    async fn handle_trace(&self, trace: &CallTrace, ctx: &IndexContext)
270        -> Result<(), IndexerError>;
271
272    /// Human-readable name for this handler (used in error messages and logging).
273    fn name(&self) -> &str;
274}
275
276// ─── TraceRegistry ──────────────────────────────────────────────────────────
277
278/// Entry in the trace registry: a handler paired with its filter.
279struct TraceEntry {
280    handler: Arc<dyn TraceHandler>,
281    filter: TraceFilter,
282}
283
284/// Registry of trace handlers with their associated filters.
285///
286/// Register handlers with filters, then dispatch traces. Only traces that
287/// match a handler's filter will be forwarded to that handler.
288pub struct TraceRegistry {
289    entries: Vec<TraceEntry>,
290}
291
292impl TraceRegistry {
293    /// Create an empty trace registry.
294    pub fn new() -> Self {
295        Self {
296            entries: Vec::new(),
297        }
298    }
299
300    /// Register a trace handler with its filter.
301    pub fn register(&mut self, handler: Arc<dyn TraceHandler>, filter: TraceFilter) {
302        self.entries.push(TraceEntry { handler, filter });
303    }
304
305    /// Dispatch a trace to all handlers whose filter matches.
306    pub async fn dispatch(
307        &self,
308        trace: &CallTrace,
309        ctx: &IndexContext,
310    ) -> Result<(), IndexerError> {
311        for entry in &self.entries {
312            if entry.filter.matches(trace) {
313                entry.handler.handle_trace(trace, ctx).await.map_err(|e| {
314                    IndexerError::Handler {
315                        handler: entry.handler.name().to_string(),
316                        reason: e.to_string(),
317                    }
318                })?;
319            }
320        }
321        Ok(())
322    }
323
324    /// Dispatch a batch of traces to all matching handlers.
325    pub async fn dispatch_batch(
326        &self,
327        traces: &[CallTrace],
328        ctx: &IndexContext,
329    ) -> Result<(), IndexerError> {
330        for trace in traces {
331            self.dispatch(trace, ctx).await?;
332        }
333        Ok(())
334    }
335
336    /// Returns the number of registered handlers.
337    pub fn handler_count(&self) -> usize {
338        self.entries.len()
339    }
340}
341
342impl Default for TraceRegistry {
343    fn default() -> Self {
344        Self::new()
345    }
346}
347
348// ─── Geth Trace Parsing ────────────────────────────────────────────────────
349
350/// Parse a Geth `debug_traceBlockByNumber` response into [`CallTrace`] objects.
351///
352/// Geth's `callTracer` returns a tree of calls per transaction. This function
353/// flattens the tree into a linear sequence with depth tracking.
354///
355/// # Arguments
356///
357/// * `json` — The JSON response from `debug_traceBlockByNumber` with
358///   `{"tracer": "callTracer"}`. Expected format: array of `{"result": {...}}`.
359/// * `block_number` — The block number (for populating `CallTrace::block_number`).
360///
361/// # Errors
362///
363/// Returns `IndexerError::Rpc` if the JSON structure is unexpected.
364pub fn parse_geth_traces(
365    json: &serde_json::Value,
366    block_number: u64,
367) -> Result<Vec<CallTrace>, IndexerError> {
368    let results = json
369        .as_array()
370        .ok_or_else(|| IndexerError::Rpc("expected array of trace results".into()))?;
371
372    let mut traces = Vec::new();
373
374    for (tx_index, entry) in results.iter().enumerate() {
375        // Each entry has { "txHash": "0x...", "result": { ... } }
376        let tx_hash = entry
377            .get("txHash")
378            .and_then(|v| v.as_str())
379            .unwrap_or("")
380            .to_string();
381
382        let result = entry.get("result").unwrap_or(entry);
383
384        let mut trace_index: u32 = 0;
385        flatten_geth_call(
386            result,
387            block_number,
388            &tx_hash,
389            tx_index as u32,
390            0,     // depth
391            false, // parent_reverted
392            &mut trace_index,
393            &mut traces,
394        );
395    }
396
397    Ok(traces)
398}
399
400/// Recursively flatten a Geth callTracer node into the traces vector.
401#[allow(clippy::too_many_arguments)]
402fn flatten_geth_call(
403    node: &serde_json::Value,
404    block_number: u64,
405    tx_hash: &str,
406    tx_index: u32,
407    depth: u32,
408    parent_reverted: bool,
409    trace_index: &mut u32,
410    out: &mut Vec<CallTrace>,
411) {
412    let call_type_str = node.get("type").and_then(|v| v.as_str()).unwrap_or("CALL");
413
414    let call_type = CallType::from_geth(call_type_str).unwrap_or(CallType::Call);
415
416    let from = node
417        .get("from")
418        .and_then(|v| v.as_str())
419        .unwrap_or("")
420        .to_lowercase();
421
422    let to = node
423        .get("to")
424        .and_then(|v| v.as_str())
425        .unwrap_or("")
426        .to_lowercase();
427
428    let value = node
429        .get("value")
430        .and_then(|v| v.as_str())
431        .unwrap_or("0x0")
432        .to_string();
433
434    let gas_used = node
435        .get("gasUsed")
436        .and_then(|v| v.as_str())
437        .and_then(|s| u64::from_str_radix(s.strip_prefix("0x").unwrap_or(s), 16).ok())
438        .unwrap_or(0);
439
440    let input = node
441        .get("input")
442        .and_then(|v| v.as_str())
443        .unwrap_or("0x")
444        .to_string();
445
446    let output = node
447        .get("output")
448        .and_then(|v| v.as_str())
449        .unwrap_or("0x")
450        .to_string();
451
452    let error = node.get("error").and_then(|v| v.as_str()).map(String::from);
453    let reverted = parent_reverted || error.is_some();
454
455    let function_selector = CallTrace::extract_selector(&input);
456
457    let current_index = *trace_index;
458    *trace_index += 1;
459
460    out.push(CallTrace {
461        call_type,
462        from,
463        to,
464        value,
465        gas_used,
466        input,
467        output,
468        function_selector,
469        depth,
470        block_number,
471        tx_hash: tx_hash.to_string(),
472        tx_index,
473        trace_index: current_index,
474        error,
475        reverted,
476    });
477
478    // Recurse into child calls.
479    if let Some(calls) = node.get("calls").and_then(|v| v.as_array()) {
480        for child in calls {
481            flatten_geth_call(
482                child,
483                block_number,
484                tx_hash,
485                tx_index,
486                depth + 1,
487                reverted,
488                trace_index,
489                out,
490            );
491        }
492    }
493}
494
495// ─── Parity Trace Parsing ──────────────────────────────────────────────────
496
497/// Parse an OpenEthereum/Parity `trace_block` response into [`CallTrace`] objects.
498///
499/// Parity traces are flat arrays with a `traceAddress` field indicating depth.
500/// Each trace has an `action` object with call details and a `result` object
501/// with output.
502///
503/// # Arguments
504///
505/// * `json` — The JSON response from `trace_block`. Expected format: flat
506///   array of trace objects.
507/// * `block_number` — The block number (for populating `CallTrace::block_number`).
508///
509/// # Errors
510///
511/// Returns `IndexerError::Rpc` if the JSON structure is unexpected.
512pub fn parse_parity_traces(
513    json: &serde_json::Value,
514    block_number: u64,
515) -> Result<Vec<CallTrace>, IndexerError> {
516    let traces_arr = json
517        .as_array()
518        .ok_or_else(|| IndexerError::Rpc("expected array of parity traces".into()))?;
519
520    let mut traces = Vec::new();
521
522    for (i, entry) in traces_arr.iter().enumerate() {
523        let action = entry.get("action").unwrap_or(entry);
524
525        let trace_type = entry.get("type").and_then(|v| v.as_str()).unwrap_or("call");
526
527        let call_type = CallType::from_parity(trace_type).unwrap_or(CallType::Call);
528
529        let from = action
530            .get("from")
531            .and_then(|v| v.as_str())
532            .unwrap_or("")
533            .to_lowercase();
534
535        let to = action
536            .get("to")
537            .and_then(|v| v.as_str())
538            .unwrap_or("")
539            .to_lowercase();
540
541        let value = action
542            .get("value")
543            .and_then(|v| v.as_str())
544            .unwrap_or("0x0")
545            .to_string();
546
547        let gas_used = entry
548            .get("result")
549            .and_then(|r| r.get("gasUsed"))
550            .and_then(|v| v.as_str())
551            .and_then(|s| u64::from_str_radix(s.strip_prefix("0x").unwrap_or(s), 16).ok())
552            .unwrap_or(0);
553
554        let input = action
555            .get("input")
556            .and_then(|v| v.as_str())
557            .unwrap_or("0x")
558            .to_string();
559
560        let output = entry
561            .get("result")
562            .and_then(|r| r.get("output"))
563            .and_then(|v| v.as_str())
564            .unwrap_or("0x")
565            .to_string();
566
567        let tx_hash = entry
568            .get("transactionHash")
569            .and_then(|v| v.as_str())
570            .unwrap_or("")
571            .to_string();
572
573        let tx_index = entry
574            .get("transactionPosition")
575            .and_then(|v| v.as_u64())
576            .unwrap_or(0) as u32;
577
578        // Depth from traceAddress length.
579        let depth = entry
580            .get("traceAddress")
581            .and_then(|v| v.as_array())
582            .map(|a| a.len() as u32)
583            .unwrap_or(0);
584
585        let error_str = entry
586            .get("error")
587            .and_then(|v| v.as_str())
588            .map(String::from);
589        let reverted = error_str.is_some();
590
591        let function_selector = CallTrace::extract_selector(&input);
592
593        traces.push(CallTrace {
594            call_type,
595            from,
596            to,
597            value,
598            gas_used,
599            input,
600            output,
601            function_selector,
602            depth,
603            block_number,
604            tx_hash,
605            tx_index,
606            trace_index: i as u32,
607            error: error_str,
608            reverted,
609        });
610    }
611
612    Ok(traces)
613}
614
615// ─── Tests ──────────────────────────────────────────────────────────────────
616
617#[cfg(test)]
618mod tests {
619    use super::*;
620    use std::sync::atomic::{AtomicU32, Ordering};
621
622    fn dummy_ctx() -> IndexContext {
623        IndexContext {
624            block: crate::types::BlockSummary {
625                number: 1,
626                hash: "0xa".into(),
627                parent_hash: "0x0".into(),
628                timestamp: 0,
629                tx_count: 0,
630            },
631            phase: crate::types::IndexPhase::Backfill,
632            chain: "ethereum".into(),
633        }
634    }
635
636    fn make_trace(
637        call_type: CallType,
638        from: &str,
639        to: &str,
640        selector: Option<&str>,
641        depth: u32,
642        reverted: bool,
643    ) -> CallTrace {
644        let input = match selector {
645            Some(sel) => format!("{}0000000000000000", sel),
646            None => "0x".to_string(),
647        };
648        let function_selector = CallTrace::extract_selector(&input);
649        CallTrace {
650            call_type,
651            from: from.to_lowercase(),
652            to: to.to_lowercase(),
653            value: "0x0".into(),
654            gas_used: 21000,
655            input,
656            output: "0x".into(),
657            function_selector,
658            depth,
659            block_number: 100,
660            tx_hash: "0xtxhash".into(),
661            tx_index: 0,
662            trace_index: 0,
663            error: if reverted {
664                Some("execution reverted".into())
665            } else {
666                None
667            },
668            reverted,
669        }
670    }
671
672    // ── CallType parsing ────────────────────────────────────────────────
673
674    #[test]
675    fn call_type_from_geth() {
676        assert_eq!(CallType::from_geth("CALL"), Some(CallType::Call));
677        assert_eq!(
678            CallType::from_geth("DELEGATECALL"),
679            Some(CallType::DelegateCall)
680        );
681        assert_eq!(
682            CallType::from_geth("STATICCALL"),
683            Some(CallType::StaticCall)
684        );
685        assert_eq!(CallType::from_geth("CREATE"), Some(CallType::Create));
686        assert_eq!(CallType::from_geth("CREATE2"), Some(CallType::Create2));
687        assert_eq!(
688            CallType::from_geth("SELFDESTRUCT"),
689            Some(CallType::SelfDestruct)
690        );
691        assert_eq!(CallType::from_geth("UNKNOWN"), None);
692    }
693
694    #[test]
695    fn call_type_from_parity() {
696        assert_eq!(CallType::from_parity("call"), Some(CallType::Call));
697        assert_eq!(
698            CallType::from_parity("delegatecall"),
699            Some(CallType::DelegateCall)
700        );
701        assert_eq!(
702            CallType::from_parity("suicide"),
703            Some(CallType::SelfDestruct)
704        );
705        assert_eq!(
706            CallType::from_parity("selfdestruct"),
707            Some(CallType::SelfDestruct)
708        );
709        assert_eq!(CallType::from_parity("create"), Some(CallType::Create));
710    }
711
712    // ── Function selector extraction ────────────────────────────────────
713
714    #[test]
715    fn function_selector_extraction() {
716        assert_eq!(
717            CallTrace::extract_selector("0xa9059cbb0000000000000000000000001234"),
718            Some("0xa9059cbb".into())
719        );
720        assert_eq!(CallTrace::extract_selector("0x"), None);
721        assert_eq!(CallTrace::extract_selector("0xabcd"), None); // too short
722        assert_eq!(
723            CallTrace::extract_selector("0xA9059CBB"),
724            Some("0xa9059cbb".into()) // lowercased
725        );
726    }
727
728    // ── TraceFilter ─────────────────────────────────────────────────────
729
730    #[test]
731    fn filter_matches_all_by_default() {
732        let filter = TraceFilter::new();
733        let trace = make_trace(
734            CallType::Call,
735            "0xaaa",
736            "0xbbb",
737            Some("0xa9059cbb"),
738            0,
739            false,
740        );
741        assert!(filter.matches(&trace));
742    }
743
744    #[test]
745    fn filter_by_address() {
746        let filter = TraceFilter::new().with_address("0xaaa");
747
748        // Matches on `from`.
749        let t1 = make_trace(
750            CallType::Call,
751            "0xaaa",
752            "0xbbb",
753            Some("0xa9059cbb"),
754            0,
755            false,
756        );
757        assert!(filter.matches(&t1));
758
759        // Matches on `to`.
760        let t2 = make_trace(
761            CallType::Call,
762            "0xbbb",
763            "0xaaa",
764            Some("0xa9059cbb"),
765            0,
766            false,
767        );
768        assert!(filter.matches(&t2));
769
770        // No match.
771        let t3 = make_trace(
772            CallType::Call,
773            "0xbbb",
774            "0xccc",
775            Some("0xa9059cbb"),
776            0,
777            false,
778        );
779        assert!(!filter.matches(&t3));
780    }
781
782    #[test]
783    fn filter_by_selector() {
784        let filter = TraceFilter::new().with_selector("0xa9059cbb");
785
786        let t1 = make_trace(
787            CallType::Call,
788            "0xaaa",
789            "0xbbb",
790            Some("0xa9059cbb"),
791            0,
792            false,
793        );
794        assert!(filter.matches(&t1));
795
796        let t2 = make_trace(
797            CallType::Call,
798            "0xaaa",
799            "0xbbb",
800            Some("0x12345678"),
801            0,
802            false,
803        );
804        assert!(!filter.matches(&t2));
805
806        // No selector (short input).
807        let t3 = make_trace(CallType::Call, "0xaaa", "0xbbb", None, 0, false);
808        assert!(!filter.matches(&t3));
809    }
810
811    #[test]
812    fn filter_by_call_type() {
813        let filter = TraceFilter::new().with_call_type(CallType::Create);
814
815        let t1 = make_trace(CallType::Create, "0xaaa", "0xbbb", None, 0, false);
816        assert!(filter.matches(&t1));
817
818        let t2 = make_trace(CallType::Call, "0xaaa", "0xbbb", None, 0, false);
819        assert!(!filter.matches(&t2));
820    }
821
822    #[test]
823    fn filter_exclude_reverted() {
824        let filter = TraceFilter::new().exclude_reverted(true);
825
826        let t1 = make_trace(CallType::Call, "0xaaa", "0xbbb", None, 0, false);
827        assert!(filter.matches(&t1));
828
829        let t2 = make_trace(CallType::Call, "0xaaa", "0xbbb", None, 0, true);
830        assert!(!filter.matches(&t2));
831    }
832
833    #[test]
834    fn filter_by_depth() {
835        let filter = TraceFilter::new().min_depth(1).max_depth(3);
836
837        let t0 = make_trace(CallType::Call, "0xaaa", "0xbbb", None, 0, false);
838        assert!(!filter.matches(&t0)); // depth 0 < min 1
839
840        let t1 = make_trace(CallType::Call, "0xaaa", "0xbbb", None, 1, false);
841        assert!(filter.matches(&t1));
842
843        let t3 = make_trace(CallType::Call, "0xaaa", "0xbbb", None, 3, false);
844        assert!(filter.matches(&t3));
845
846        let t4 = make_trace(CallType::Call, "0xaaa", "0xbbb", None, 4, false);
847        assert!(!filter.matches(&t4)); // depth 4 > max 3
848    }
849
850    // ── TraceHandler dispatch ───────────────────────────────────────────
851
852    struct CountingHandler {
853        count: Arc<AtomicU32>,
854        handler_name: String,
855    }
856
857    #[async_trait]
858    impl TraceHandler for CountingHandler {
859        async fn handle_trace(
860            &self,
861            _trace: &CallTrace,
862            _ctx: &IndexContext,
863        ) -> Result<(), IndexerError> {
864            self.count.fetch_add(1, Ordering::Relaxed);
865            Ok(())
866        }
867        fn name(&self) -> &str {
868            &self.handler_name
869        }
870    }
871
872    #[tokio::test]
873    async fn dispatch_to_matching_handler() {
874        let count = Arc::new(AtomicU32::new(0));
875        let handler = Arc::new(CountingHandler {
876            count: count.clone(),
877            handler_name: "test_handler".into(),
878        });
879
880        let mut registry = TraceRegistry::new();
881        registry.register(handler, TraceFilter::new().with_call_type(CallType::Create));
882
883        let ctx = dummy_ctx();
884
885        // Should match (Create).
886        let t1 = make_trace(CallType::Create, "0xaaa", "0xbbb", None, 0, false);
887        registry.dispatch(&t1, &ctx).await.unwrap();
888
889        // Should not match (Call).
890        let t2 = make_trace(CallType::Call, "0xaaa", "0xbbb", None, 0, false);
891        registry.dispatch(&t2, &ctx).await.unwrap();
892
893        assert_eq!(count.load(Ordering::Relaxed), 1);
894    }
895
896    #[tokio::test]
897    async fn dispatch_batch() {
898        let count = Arc::new(AtomicU32::new(0));
899        let handler = Arc::new(CountingHandler {
900            count: count.clone(),
901            handler_name: "batch_handler".into(),
902        });
903
904        let mut registry = TraceRegistry::new();
905        registry.register(handler, TraceFilter::new());
906
907        let ctx = dummy_ctx();
908        let traces = vec![
909            make_trace(CallType::Call, "0xaaa", "0xbbb", None, 0, false),
910            make_trace(CallType::Create, "0xaaa", "0xbbb", None, 0, false),
911            make_trace(CallType::DelegateCall, "0xaaa", "0xbbb", None, 0, false),
912        ];
913
914        registry.dispatch_batch(&traces, &ctx).await.unwrap();
915        assert_eq!(count.load(Ordering::Relaxed), 3);
916    }
917
918    // ── Geth trace parsing ──────────────────────────────────────────────
919
920    #[test]
921    fn parse_geth_trace_basic() {
922        let json = serde_json::json!([
923            {
924                "txHash": "0xabc123",
925                "result": {
926                    "type": "CALL",
927                    "from": "0xSender",
928                    "to": "0xReceiver",
929                    "value": "0xde0b6b3a7640000",
930                    "gasUsed": "0x5208",
931                    "input": "0xa9059cbb0000000000000000000000001234",
932                    "output": "0x0000000000000000000000000000000000000001",
933                    "calls": [
934                        {
935                            "type": "DELEGATECALL",
936                            "from": "0xReceiver",
937                            "to": "0xImpl",
938                            "value": "0x0",
939                            "gasUsed": "0x1000",
940                            "input": "0xa9059cbb0000",
941                            "output": "0x01"
942                        }
943                    ]
944                }
945            }
946        ]);
947
948        let traces = parse_geth_traces(&json, 12345).unwrap();
949        assert_eq!(traces.len(), 2);
950
951        // Top-level call.
952        assert_eq!(traces[0].call_type, CallType::Call);
953        assert_eq!(traces[0].from, "0xsender");
954        assert_eq!(traces[0].to, "0xreceiver");
955        assert_eq!(traces[0].depth, 0);
956        assert_eq!(traces[0].block_number, 12345);
957        assert_eq!(traces[0].tx_hash, "0xabc123");
958        assert_eq!(traces[0].gas_used, 0x5208);
959        assert_eq!(traces[0].function_selector, Some("0xa9059cbb".into()));
960        assert!(!traces[0].reverted);
961        assert_eq!(traces[0].trace_index, 0);
962
963        // Nested delegate call.
964        assert_eq!(traces[1].call_type, CallType::DelegateCall);
965        assert_eq!(traces[1].depth, 1);
966        assert_eq!(traces[1].trace_index, 1);
967    }
968
969    #[test]
970    fn parse_geth_trace_with_error() {
971        let json = serde_json::json!([
972            {
973                "txHash": "0xfailed",
974                "result": {
975                    "type": "CALL",
976                    "from": "0xSender",
977                    "to": "0xReceiver",
978                    "value": "0x0",
979                    "gasUsed": "0x5208",
980                    "input": "0x",
981                    "output": "0x",
982                    "error": "execution reverted",
983                    "calls": [
984                        {
985                            "type": "CALL",
986                            "from": "0xReceiver",
987                            "to": "0xInner",
988                            "value": "0x0",
989                            "gasUsed": "0x100",
990                            "input": "0x",
991                            "output": "0x"
992                        }
993                    ]
994                }
995            }
996        ]);
997
998        let traces = parse_geth_traces(&json, 100).unwrap();
999        assert_eq!(traces.len(), 2);
1000
1001        // Parent is reverted.
1002        assert!(traces[0].reverted);
1003        assert_eq!(traces[0].error, Some("execution reverted".into()));
1004
1005        // Child inherits reverted status from parent.
1006        assert!(traces[1].reverted);
1007    }
1008
1009    // ── Parity trace parsing ────────────────────────────────────────────
1010
1011    #[test]
1012    fn parse_parity_trace_basic() {
1013        let json = serde_json::json!([
1014            {
1015                "action": {
1016                    "from": "0xSender",
1017                    "to": "0xReceiver",
1018                    "value": "0xde0b6b3a7640000",
1019                    "input": "0xa9059cbb0000000000000000000000001234"
1020                },
1021                "result": {
1022                    "gasUsed": "0x5208",
1023                    "output": "0x0001"
1024                },
1025                "transactionHash": "0xparity_tx",
1026                "transactionPosition": 0,
1027                "traceAddress": [],
1028                "type": "call"
1029            },
1030            {
1031                "action": {
1032                    "from": "0xReceiver",
1033                    "to": "0xInner",
1034                    "value": "0x0",
1035                    "input": "0x12345678aabbccdd"
1036                },
1037                "result": {
1038                    "gasUsed": "0x1000",
1039                    "output": "0x"
1040                },
1041                "transactionHash": "0xparity_tx",
1042                "transactionPosition": 0,
1043                "traceAddress": [0],
1044                "type": "call"
1045            }
1046        ]);
1047
1048        let traces = parse_parity_traces(&json, 999).unwrap();
1049        assert_eq!(traces.len(), 2);
1050
1051        // Top-level.
1052        assert_eq!(traces[0].call_type, CallType::Call);
1053        assert_eq!(traces[0].from, "0xsender");
1054        assert_eq!(traces[0].to, "0xreceiver");
1055        assert_eq!(traces[0].depth, 0);
1056        assert_eq!(traces[0].block_number, 999);
1057        assert_eq!(traces[0].tx_hash, "0xparity_tx");
1058        assert_eq!(traces[0].function_selector, Some("0xa9059cbb".into()));
1059
1060        // Nested.
1061        assert_eq!(traces[1].depth, 1);
1062        assert_eq!(traces[1].function_selector, Some("0x12345678".into()));
1063    }
1064
1065    #[test]
1066    fn parse_parity_trace_create() {
1067        let json = serde_json::json!([
1068            {
1069                "action": {
1070                    "from": "0xDeployer",
1071                    "value": "0x0",
1072                    "init": "0x6080604052"
1073                },
1074                "result": {
1075                    "address": "0xNewContract",
1076                    "gasUsed": "0x30000",
1077                    "code": "0x6080"
1078                },
1079                "transactionHash": "0xcreate_tx",
1080                "transactionPosition": 1,
1081                "traceAddress": [],
1082                "type": "create"
1083            }
1084        ]);
1085
1086        let traces = parse_parity_traces(&json, 500).unwrap();
1087        assert_eq!(traces.len(), 1);
1088        assert_eq!(traces[0].call_type, CallType::Create);
1089        assert_eq!(traces[0].from, "0xdeployer");
1090    }
1091
1092    #[test]
1093    fn parse_parity_trace_with_error() {
1094        let json = serde_json::json!([
1095            {
1096                "action": {
1097                    "from": "0xSender",
1098                    "to": "0xReceiver",
1099                    "value": "0x0",
1100                    "input": "0x"
1101                },
1102                "transactionHash": "0xfail_tx",
1103                "transactionPosition": 0,
1104                "traceAddress": [],
1105                "type": "call",
1106                "error": "out of gas"
1107            }
1108        ]);
1109
1110        let traces = parse_parity_traces(&json, 200).unwrap();
1111        assert_eq!(traces.len(), 1);
1112        assert!(traces[0].reverted);
1113        assert_eq!(traces[0].error, Some("out of gas".into()));
1114    }
1115
1116    // ── Trace depth tracking ────────────────────────────────────────────
1117
1118    #[test]
1119    fn geth_trace_depth_tracking() {
1120        let json = serde_json::json!([
1121            {
1122                "txHash": "0xdeep",
1123                "result": {
1124                    "type": "CALL",
1125                    "from": "0xa",
1126                    "to": "0xb",
1127                    "value": "0x0",
1128                    "gasUsed": "0x100",
1129                    "input": "0x",
1130                    "output": "0x",
1131                    "calls": [
1132                        {
1133                            "type": "CALL",
1134                            "from": "0xb",
1135                            "to": "0xc",
1136                            "value": "0x0",
1137                            "gasUsed": "0x50",
1138                            "input": "0x",
1139                            "output": "0x",
1140                            "calls": [
1141                                {
1142                                    "type": "STATICCALL",
1143                                    "from": "0xc",
1144                                    "to": "0xd",
1145                                    "value": "0x0",
1146                                    "gasUsed": "0x20",
1147                                    "input": "0x",
1148                                    "output": "0x"
1149                                }
1150                            ]
1151                        }
1152                    ]
1153                }
1154            }
1155        ]);
1156
1157        let traces = parse_geth_traces(&json, 1).unwrap();
1158        assert_eq!(traces.len(), 3);
1159        assert_eq!(traces[0].depth, 0);
1160        assert_eq!(traces[1].depth, 1);
1161        assert_eq!(traces[2].depth, 2);
1162        assert_eq!(traces[2].call_type, CallType::StaticCall);
1163    }
1164
1165    // ── Combined filter ─────────────────────────────────────────────────
1166
1167    #[test]
1168    fn combined_filter_all_criteria() {
1169        let filter = TraceFilter::new()
1170            .with_address("0xaaa")
1171            .with_call_type(CallType::Call)
1172            .with_selector("0xa9059cbb")
1173            .exclude_reverted(true);
1174
1175        // Matches all criteria.
1176        let t1 = make_trace(
1177            CallType::Call,
1178            "0xaaa",
1179            "0xbbb",
1180            Some("0xa9059cbb"),
1181            0,
1182            false,
1183        );
1184        assert!(filter.matches(&t1));
1185
1186        // Wrong call type.
1187        let t2 = make_trace(
1188            CallType::Create,
1189            "0xaaa",
1190            "0xbbb",
1191            Some("0xa9059cbb"),
1192            0,
1193            false,
1194        );
1195        assert!(!filter.matches(&t2));
1196
1197        // Wrong address.
1198        let t3 = make_trace(
1199            CallType::Call,
1200            "0xzzz",
1201            "0xbbb",
1202            Some("0xa9059cbb"),
1203            0,
1204            false,
1205        );
1206        assert!(!filter.matches(&t3));
1207
1208        // Reverted.
1209        let t4 = make_trace(
1210            CallType::Call,
1211            "0xaaa",
1212            "0xbbb",
1213            Some("0xa9059cbb"),
1214            0,
1215            true,
1216        );
1217        assert!(!filter.matches(&t4));
1218    }
1219}