Skip to main content

tycho_common/models/
blockchain.rs

1use std::collections::{hash_map::Entry, BTreeMap, HashMap, HashSet};
2
3use chrono::NaiveDateTime;
4use deepsize::DeepSizeOf;
5use serde::{ser::SerializeStruct, Deserialize, Serialize, Serializer};
6use tracing::warn;
7
8use crate::{
9    dto,
10    models::{
11        contract::{AccountBalance, AccountDelta},
12        protocol::{ComponentBalance, ProtocolComponent, ProtocolComponentStateDelta},
13        token::Token,
14        Address, Balance, BlockHash, Chain, Code, ComponentId, EntryPointId, MergeError, StoreKey,
15        StoreVal,
16    },
17    Bytes,
18};
19
20#[derive(Clone, Default, PartialEq, Serialize, Deserialize, Debug)]
21pub struct Block {
22    pub number: u64,
23    pub chain: Chain,
24    pub hash: Bytes,
25    pub parent_hash: Bytes,
26    pub ts: NaiveDateTime,
27}
28
29impl Block {
30    pub fn new(
31        number: u64,
32        chain: Chain,
33        hash: Bytes,
34        parent_hash: Bytes,
35        ts: NaiveDateTime,
36    ) -> Self {
37        Block { hash, parent_hash, number, chain, ts }
38    }
39}
40
41// Manual impl as `NaiveDateTime` structure referenced in `ts` does not implement DeepSizeOf
42impl DeepSizeOf for Block {
43    fn deep_size_of_children(&self, context: &mut deepsize::Context) -> usize {
44        self.chain
45            .deep_size_of_children(context) +
46            self.hash.deep_size_of_children(context) +
47            self.parent_hash
48                .deep_size_of_children(context)
49    }
50}
51
52#[derive(Clone, Default, PartialEq, Debug, Eq, Hash, DeepSizeOf)]
53pub struct Transaction {
54    pub hash: Bytes,
55    pub block_hash: Bytes,
56    pub from: Bytes,
57    pub to: Option<Bytes>,
58    pub index: u64,
59}
60
61impl Transaction {
62    pub fn new(hash: Bytes, block_hash: Bytes, from: Bytes, to: Option<Bytes>, index: u64) -> Self {
63        Transaction { hash, block_hash, from, to, index }
64    }
65}
66
67/// Raw EVM log emitted by a contract during transaction execution.
68///
69/// Used as the primary input to [`TxDeltaIndexer`] implementations.
70///
71/// [`TxDeltaIndexer`]: crate::traits::TxDeltaIndexer
72#[derive(Debug, Clone)]
73pub struct LogInput {
74    address: Bytes,
75    topics: Vec<Bytes>,
76    data: Bytes,
77    log_index: u32,
78}
79
80impl LogInput {
81    pub fn new(address: Bytes, topics: Vec<Bytes>, data: Bytes, log_index: u32) -> Self {
82        Self { address, topics, data, log_index }
83    }
84
85    pub fn address(&self) -> &Bytes {
86        &self.address
87    }
88
89    pub fn topics(&self) -> &[Bytes] {
90        &self.topics
91    }
92
93    pub fn data(&self) -> &Bytes {
94        &self.data
95    }
96
97    pub fn log_index(&self) -> u32 {
98        self.log_index
99    }
100}
101
102/// Raw EVM transaction with its associated logs.
103///
104/// The `succeeded` flag allows callers to pass all transactions in a block and
105/// have the processor skip reverted ones, avoiding a separate pre-filter.
106#[derive(Debug, Clone)]
107pub struct TxInput {
108    hash: Bytes,
109    from: Bytes,
110    to: Bytes,
111    index: u64,
112    logs: Vec<LogInput>,
113    succeeded: bool,
114}
115
116impl TxInput {
117    pub fn new(
118        hash: Bytes,
119        from: Bytes,
120        to: Bytes,
121        index: u64,
122        logs: Vec<LogInput>,
123        succeeded: bool,
124    ) -> Self {
125        Self { hash, from, to, index, logs, succeeded }
126    }
127
128    pub fn hash(&self) -> &Bytes {
129        &self.hash
130    }
131
132    pub fn from(&self) -> &Bytes {
133        &self.from
134    }
135
136    pub fn to(&self) -> &Bytes {
137        &self.to
138    }
139
140    pub fn index(&self) -> u64 {
141        self.index
142    }
143
144    pub fn logs(&self) -> &[LogInput] {
145        &self.logs
146    }
147
148    pub fn succeeded(&self) -> bool {
149        self.succeeded
150    }
151}
152
153pub struct BlockTransactionDeltas<T> {
154    pub extractor: String,
155    pub chain: Chain,
156    pub block: Block,
157    pub revert: bool,
158    pub deltas: Vec<TransactionDeltaGroup<T>>,
159}
160
161#[allow(dead_code)]
162pub struct TransactionDeltaGroup<T> {
163    changes: T,
164    protocol_component: HashMap<String, ProtocolComponent>,
165    component_balances: HashMap<String, ComponentBalance>,
166    component_tvl: HashMap<String, f64>,
167    tx: Transaction,
168}
169
170#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, DeepSizeOf)]
171pub struct BlockAggregatedChanges {
172    pub extractor: String,
173    pub chain: Chain,
174    pub block: Block,
175    pub finalized_block_height: u64,
176    pub db_committed_block_height: Option<u64>,
177    pub revert: bool,
178    pub state_deltas: HashMap<String, ProtocolComponentStateDelta>,
179    pub account_deltas: HashMap<Bytes, AccountDelta>,
180    pub new_tokens: HashMap<Address, Token>,
181    pub new_protocol_components: HashMap<String, ProtocolComponent>,
182    pub deleted_protocol_components: HashMap<String, ProtocolComponent>,
183    pub component_balances: HashMap<ComponentId, HashMap<Bytes, ComponentBalance>>,
184    pub account_balances: HashMap<Address, HashMap<Address, AccountBalance>>,
185    pub component_tvl: HashMap<String, f64>,
186    pub dci_update: DCIUpdate,
187    /// The index of the partial block. None if it's a full block.
188    #[serde(default, skip_serializing_if = "Option::is_none")]
189    pub partial_block_index: Option<u32>,
190}
191
192impl BlockAggregatedChanges {
193    #[allow(clippy::too_many_arguments)]
194    pub fn new(
195        extractor: &str,
196        chain: Chain,
197        block: Block,
198        db_committed_block_height: Option<u64>,
199        finalized_block_height: u64,
200        revert: bool,
201        state_deltas: HashMap<String, ProtocolComponentStateDelta>,
202        account_deltas: HashMap<Bytes, AccountDelta>,
203        new_tokens: HashMap<Address, Token>,
204        new_components: HashMap<String, ProtocolComponent>,
205        deleted_components: HashMap<String, ProtocolComponent>,
206        component_balances: HashMap<ComponentId, HashMap<Bytes, ComponentBalance>>,
207        account_balances: HashMap<Address, HashMap<Address, AccountBalance>>,
208        component_tvl: HashMap<String, f64>,
209        dci_update: DCIUpdate,
210    ) -> Self {
211        Self {
212            extractor: extractor.to_string(),
213            chain,
214            block,
215            db_committed_block_height,
216            finalized_block_height,
217            revert,
218            state_deltas,
219            account_deltas,
220            new_tokens,
221            new_protocol_components: new_components,
222            deleted_protocol_components: deleted_components,
223            component_balances,
224            account_balances,
225            component_tvl,
226            dci_update,
227            partial_block_index: None,
228        }
229    }
230
231    pub fn drop_state(&self) -> Self {
232        Self {
233            extractor: self.extractor.clone(),
234            chain: self.chain,
235            block: self.block.clone(),
236            db_committed_block_height: self.db_committed_block_height,
237            finalized_block_height: self.finalized_block_height,
238            revert: self.revert,
239            account_deltas: HashMap::new(),
240            state_deltas: HashMap::new(),
241            new_tokens: self.new_tokens.clone(),
242            new_protocol_components: self.new_protocol_components.clone(),
243            deleted_protocol_components: self.deleted_protocol_components.clone(),
244            component_balances: self.component_balances.clone(),
245            account_balances: self.account_balances.clone(),
246            component_tvl: self.component_tvl.clone(),
247            dci_update: self.dci_update.clone(),
248            partial_block_index: self.partial_block_index,
249        }
250    }
251
252    pub fn is_partial(&self) -> bool {
253        self.partial_block_index.is_some()
254    }
255
256    pub fn get_block(&self) -> &Block {
257        &self.block
258    }
259
260    pub fn n_changes(&self) -> usize {
261        self.account_deltas.len() + self.state_deltas.len()
262    }
263
264    pub fn filter_by_component<F: Fn(&str) -> bool>(&mut self, keep: F) {
265        self.state_deltas.retain(|k, _| keep(k));
266        self.component_balances
267            .retain(|k, _| keep(k));
268        self.component_tvl
269            .retain(|k, _| keep(k));
270    }
271
272    pub fn filter_by_contract<F: Fn(&Bytes) -> bool>(&mut self, keep: F) {
273        self.account_deltas
274            .retain(|k, _| keep(k));
275        self.account_balances
276            .retain(|k, _| keep(k));
277    }
278
279    /// Merges this update with another one, consuming both.
280    ///
281    /// `other` is assumed to be a more recent update than `self`.
282    pub fn merge(mut self, other: Self) -> Self {
283        for (k, v) in other.account_deltas {
284            match self.account_deltas.entry(k) {
285                Entry::Occupied(mut e) => {
286                    // best-effort: ignore merge errors (address mismatch is a bug)
287                    let _ = e.get_mut().merge(v);
288                }
289                Entry::Vacant(e) => {
290                    e.insert(v);
291                }
292            }
293        }
294
295        for (k, v) in other.state_deltas {
296            match self.state_deltas.entry(k) {
297                Entry::Occupied(mut e) => {
298                    let _ = e.get_mut().merge(v);
299                }
300                Entry::Vacant(e) => {
301                    e.insert(v);
302                }
303            }
304        }
305
306        for (component_id, balances) in other.component_balances {
307            self.component_balances
308                .entry(component_id)
309                .or_default()
310                .extend(balances);
311        }
312
313        for (account, balances) in other.account_balances {
314            self.account_balances
315                .entry(account)
316                .or_default()
317                .extend(balances);
318        }
319
320        self.component_tvl
321            .extend(other.component_tvl);
322        self.new_protocol_components
323            .extend(other.new_protocol_components);
324        self.deleted_protocol_components
325            .extend(other.deleted_protocol_components);
326        self.revert = other.revert;
327        self.block = other.block;
328        self
329    }
330}
331
332impl std::fmt::Display for BlockAggregatedChanges {
333    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
334        write!(f, "block_number: {}, extractor: {}", self.block.number, self.extractor)
335    }
336}
337
338pub trait BlockScoped {
339    fn block(&self) -> Block;
340}
341
342impl BlockScoped for BlockAggregatedChanges {
343    fn block(&self) -> Block {
344        self.block.clone()
345    }
346}
347
348impl From<dto::Block> for Block {
349    fn from(value: dto::Block) -> Self {
350        Self {
351            number: value.number,
352            chain: value.chain.into(),
353            hash: value.hash,
354            parent_hash: value.parent_hash,
355            ts: value.ts,
356        }
357    }
358}
359
360impl From<dto::AddressStorageLocation> for AddressStorageLocation {
361    fn from(value: dto::AddressStorageLocation) -> Self {
362        Self { key: value.key, offset: value.offset }
363    }
364}
365
366impl From<dto::TracingResult> for TracingResult {
367    fn from(value: dto::TracingResult) -> Self {
368        Self {
369            retriggers: value
370                .retriggers
371                .into_iter()
372                .map(|(addr, loc)| (addr, loc.into()))
373                .collect(),
374            accessed_slots: value.accessed_slots,
375        }
376    }
377}
378
379impl From<dto::DCIUpdate> for DCIUpdate {
380    fn from(value: dto::DCIUpdate) -> Self {
381        Self {
382            new_entrypoints: value
383                .new_entrypoints
384                .into_iter()
385                .map(|(k, v)| {
386                    (
387                        k,
388                        v.into_iter()
389                            .map(EntryPoint::from)
390                            .collect(),
391                    )
392                })
393                .collect(),
394            new_entrypoint_params: value
395                .new_entrypoint_params
396                .into_iter()
397                .map(|(k, v)| {
398                    (
399                        k,
400                        v.into_iter()
401                            .map(|(p, c)| (TracingParams::from(p), c))
402                            .collect(),
403                    )
404                })
405                .collect(),
406            trace_results: value
407                .trace_results
408                .into_iter()
409                .map(|(k, v)| (k, TracingResult::from(v)))
410                .collect(),
411        }
412    }
413}
414
415impl From<dto::BlockAggregatedChanges> for BlockAggregatedChanges {
416    fn from(value: dto::BlockAggregatedChanges) -> Self {
417        use crate::models::{
418            contract::{AccountBalance, AccountDelta},
419            protocol::{ComponentBalance, ProtocolComponent, ProtocolComponentStateDelta},
420            token::Token,
421        };
422        Self {
423            extractor: value.extractor,
424            chain: value.chain.into(),
425            block: value.block.into(),
426            finalized_block_height: value.finalized_block_height,
427            db_committed_block_height: None,
428            revert: value.revert,
429            state_deltas: value
430                .state_updates
431                .into_iter()
432                .map(|(k, v)| (k, ProtocolComponentStateDelta::from(v)))
433                .collect(),
434            account_deltas: value
435                .account_updates
436                .into_iter()
437                .map(|(k, v)| (k, AccountDelta::from(v)))
438                .collect(),
439            new_tokens: value
440                .new_tokens
441                .into_iter()
442                .map(|(k, v)| (k, Token::from(v)))
443                .collect(),
444            new_protocol_components: value
445                .new_protocol_components
446                .into_iter()
447                .map(|(k, v)| (k, ProtocolComponent::from(v)))
448                .collect(),
449            deleted_protocol_components: value
450                .deleted_protocol_components
451                .into_iter()
452                .map(|(k, v)| (k, ProtocolComponent::from(v)))
453                .collect(),
454            component_balances: value
455                .component_balances
456                .into_iter()
457                .map(|(component_id, token_balances)| {
458                    (
459                        component_id,
460                        token_balances
461                            .0
462                            .into_iter()
463                            .map(|(k, v)| (k, ComponentBalance::from(v)))
464                            .collect(),
465                    )
466                })
467                .collect(),
468            account_balances: value
469                .account_balances
470                .into_iter()
471                .map(|(account, balances)| {
472                    (
473                        account,
474                        balances
475                            .into_iter()
476                            .map(|(k, v)| (k, AccountBalance::from(v)))
477                            .collect(),
478                    )
479                })
480                .collect(),
481            component_tvl: value.component_tvl,
482            dci_update: DCIUpdate::from(value.dci_update),
483            partial_block_index: value.partial_block_index,
484        }
485    }
486}
487
488#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, DeepSizeOf)]
489pub struct DCIUpdate {
490    pub new_entrypoints: HashMap<ComponentId, HashSet<EntryPoint>>,
491    pub new_entrypoint_params: HashMap<EntryPointId, HashSet<(TracingParams, ComponentId)>>,
492    pub trace_results: HashMap<EntryPointId, TracingResult>,
493}
494
495/// Traced entry points for a set of components, as returned by the Tycho RPC.
496///
497/// Maps each component ID to the list of `(EntryPointWithTracingParams, TracingResult)` pairs
498/// produced by the tracer for that component.
499pub type TracedEntryPoints =
500    HashMap<ComponentId, Vec<(EntryPointWithTracingParams, TracingResult)>>;
501
502impl From<TracedEntryPoints> for DCIUpdate {
503    fn from(traced_entry_points: TracedEntryPoints) -> Self {
504        let mut new_entrypoints: HashMap<ComponentId, HashSet<EntryPoint>> = HashMap::new();
505        let mut new_entrypoint_params: HashMap<
506            EntryPointId,
507            HashSet<(TracingParams, ComponentId)>,
508        > = HashMap::new();
509        let mut trace_results: HashMap<EntryPointId, TracingResult> = HashMap::new();
510
511        for (component_id, traces) in traced_entry_points {
512            let mut entrypoints = HashSet::new();
513
514            for (ep_with_params, trace) in traces {
515                let ep_id = ep_with_params
516                    .entry_point
517                    .external_id
518                    .clone();
519
520                entrypoints.insert(ep_with_params.entry_point.clone());
521
522                new_entrypoint_params
523                    .entry(ep_id.clone())
524                    .or_default()
525                    .insert((ep_with_params.params, component_id.clone()));
526
527                trace_results
528                    .entry(ep_id)
529                    .and_modify(|existing: &mut TracingResult| {
530                        existing
531                            .retriggers
532                            .extend(trace.retriggers.clone());
533                        for (address, slots) in trace.accessed_slots.clone() {
534                            existing
535                                .accessed_slots
536                                .entry(address)
537                                .or_default()
538                                .extend(slots);
539                        }
540                    })
541                    .or_insert(trace);
542            }
543
544            if !entrypoints.is_empty() {
545                new_entrypoints.insert(component_id, entrypoints);
546            }
547        }
548
549        DCIUpdate { new_entrypoints, new_entrypoint_params, trace_results }
550    }
551}
552
553/// Changes grouped by their respective transaction.
554#[derive(Debug, Clone, PartialEq, Default, DeepSizeOf)]
555pub struct TxWithChanges {
556    pub tx: Transaction,
557    pub protocol_components: HashMap<ComponentId, ProtocolComponent>,
558    pub account_deltas: HashMap<Address, AccountDelta>,
559    pub state_updates: HashMap<ComponentId, ProtocolComponentStateDelta>,
560    pub balance_changes: HashMap<ComponentId, HashMap<Address, ComponentBalance>>,
561    pub account_balance_changes: HashMap<Address, HashMap<Address, AccountBalance>>,
562    pub entrypoints: HashMap<ComponentId, HashSet<EntryPoint>>,
563    pub entrypoint_params: HashMap<EntryPointId, HashSet<(TracingParams, ComponentId)>>,
564}
565
566impl TxWithChanges {
567    #[allow(clippy::too_many_arguments)]
568    pub fn new(
569        tx: Transaction,
570        protocol_components: HashMap<ComponentId, ProtocolComponent>,
571        account_deltas: HashMap<Address, AccountDelta>,
572        protocol_states: HashMap<ComponentId, ProtocolComponentStateDelta>,
573        balance_changes: HashMap<ComponentId, HashMap<Address, ComponentBalance>>,
574        account_balance_changes: HashMap<Address, HashMap<Address, AccountBalance>>,
575        entrypoints: HashMap<ComponentId, HashSet<EntryPoint>>,
576        entrypoint_params: HashMap<EntryPointId, HashSet<(TracingParams, ComponentId)>>,
577    ) -> Self {
578        Self {
579            tx,
580            account_deltas,
581            protocol_components,
582            state_updates: protocol_states,
583            balance_changes,
584            account_balance_changes,
585            entrypoints,
586            entrypoint_params,
587        }
588    }
589
590    /// Merges this update with another one.
591    ///
592    /// The method combines two [`TxWithChanges`] instances if they are on the same block.
593    ///
594    /// NB: It is expected that `other` is a more recent update than `self` is and the two are
595    /// combined accordingly.
596    ///
597    /// # Errors
598    /// Returns a `MergeError` if any of the above conditions are violated.
599    pub fn merge(&mut self, other: TxWithChanges) -> Result<(), MergeError> {
600        if self.tx.block_hash != other.tx.block_hash {
601            return Err(MergeError::BlockMismatch(
602                "TxWithChanges".to_string(),
603                self.tx.block_hash.clone(),
604                other.tx.block_hash,
605            ));
606        }
607        if self.tx.index > other.tx.index {
608            return Err(MergeError::TransactionOrderError(
609                "TxWithChanges".to_string(),
610                self.tx.index,
611                other.tx.index,
612            ));
613        }
614
615        self.tx = other.tx;
616
617        // Merge new protocol components
618        // Log a warning if a new protocol component for the same id already exists, because this
619        // should never happen.
620        for (key, value) in other.protocol_components {
621            match self.protocol_components.entry(key) {
622                Entry::Occupied(mut entry) => {
623                    warn!(
624                        "Overwriting new protocol component for id {} with a new one. This should never happen! Please check logic",
625                        entry.get().id
626                    );
627                    entry.insert(value);
628                }
629                Entry::Vacant(entry) => {
630                    entry.insert(value);
631                }
632            }
633        }
634
635        // Merge account deltas
636        for (address, update) in other.account_deltas.clone().into_iter() {
637            match self.account_deltas.entry(address) {
638                Entry::Occupied(mut e) => {
639                    e.get_mut().merge(update)?;
640                }
641                Entry::Vacant(e) => {
642                    e.insert(update);
643                }
644            }
645        }
646
647        // Merge protocol state updates
648        for (key, value) in other.state_updates {
649            match self.state_updates.entry(key) {
650                Entry::Occupied(mut entry) => {
651                    entry.get_mut().merge(value)?;
652                }
653                Entry::Vacant(entry) => {
654                    entry.insert(value);
655                }
656            }
657        }
658
659        // Merge component balance changes
660        for (component_id, balance_changes) in other.balance_changes {
661            let token_balances = self
662                .balance_changes
663                .entry(component_id)
664                .or_default();
665            for (token, balance) in balance_changes {
666                token_balances.insert(token, balance);
667            }
668        }
669
670        // Merge account balance changes
671        for (account_addr, balance_changes) in other.account_balance_changes {
672            let token_balances = self
673                .account_balance_changes
674                .entry(account_addr)
675                .or_default();
676            for (token, balance) in balance_changes {
677                token_balances.insert(token, balance);
678            }
679        }
680
681        // Merge new entrypoints
682        for (component_id, entrypoints) in other.entrypoints {
683            self.entrypoints
684                .entry(component_id)
685                .or_default()
686                .extend(entrypoints);
687        }
688
689        // Merge new entrypoint params
690        for (entrypoint_id, params) in other.entrypoint_params {
691            self.entrypoint_params
692                .entry(entrypoint_id)
693                .or_default()
694                .extend(params);
695        }
696
697        Ok(())
698    }
699}
700
701#[derive(Copy, Clone, Debug, PartialEq)]
702pub enum BlockTag {
703    /// Finalized block
704    Finalized,
705    /// Safe block
706    Safe,
707    /// Latest block
708    Latest,
709    /// Earliest block (genesis)
710    Earliest,
711    /// Pending block (not yet part of the blockchain)
712    Pending,
713    /// Block by number
714    Number(u64),
715}
716#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, DeepSizeOf)]
717pub struct EntryPoint {
718    /// Entry point id
719    pub external_id: String,
720    /// The address of the contract to trace.
721    pub target: Address,
722    /// The signature of the function to trace.
723    pub signature: String,
724}
725
726impl EntryPoint {
727    pub fn new(external_id: String, target: Address, signature: String) -> Self {
728        Self { external_id, target, signature }
729    }
730}
731
732impl From<dto::EntryPoint> for EntryPoint {
733    fn from(value: dto::EntryPoint) -> Self {
734        Self { external_id: value.external_id, target: value.target, signature: value.signature }
735    }
736}
737
738/// A struct that combines an entry point with its associated tracing params.
739#[derive(Debug, Clone, PartialEq, Eq, Hash, DeepSizeOf)]
740pub struct EntryPointWithTracingParams {
741    /// The entry point to trace, containing the target contract address and function signature
742    pub entry_point: EntryPoint,
743    /// The tracing parameters for this entry point
744    pub params: TracingParams,
745}
746
747impl From<dto::EntryPointWithTracingParams> for EntryPointWithTracingParams {
748    fn from(value: dto::EntryPointWithTracingParams) -> Self {
749        match value.params {
750            dto::TracingParams::RPCTracer(ref tracer_params) => Self {
751                entry_point: EntryPoint {
752                    external_id: value.entry_point.external_id,
753                    target: value.entry_point.target,
754                    signature: value.entry_point.signature,
755                },
756                params: TracingParams::RPCTracer(RPCTracerParams {
757                    caller: tracer_params.caller.clone(),
758                    calldata: tracer_params.calldata.clone(),
759                    state_overrides: tracer_params
760                        .state_overrides
761                        .clone()
762                        .map(|s| {
763                            s.into_iter()
764                                .map(|(k, v)| (k, v.into()))
765                                .collect()
766                        }),
767                    prune_addresses: tracer_params.prune_addresses.clone(),
768                }),
769            },
770        }
771    }
772}
773
774impl EntryPointWithTracingParams {
775    pub fn new(entry_point: EntryPoint, params: TracingParams) -> Self {
776        Self { entry_point, params }
777    }
778}
779
780impl std::fmt::Display for EntryPointWithTracingParams {
781    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
782        let tracer_type = match &self.params {
783            TracingParams::RPCTracer(_) => "RPC",
784        };
785        write!(f, "{} [{}]", self.entry_point.external_id, tracer_type)
786    }
787}
788
789#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Eq, Hash, DeepSizeOf)]
790/// An entry point to trace. Different types of entry points tracing will be supported in the
791/// future. Like RPC debug tracing, symbolic execution, etc.
792pub enum TracingParams {
793    /// Uses RPC calls to retrieve the called addresses and retriggers
794    RPCTracer(RPCTracerParams),
795}
796
797impl std::fmt::Display for TracingParams {
798    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
799        match self {
800            TracingParams::RPCTracer(params) => write!(f, "RPC: {params}"),
801        }
802    }
803}
804
805impl From<dto::TracingParams> for TracingParams {
806    fn from(value: dto::TracingParams) -> Self {
807        match value {
808            dto::TracingParams::RPCTracer(tracer_params) => {
809                TracingParams::RPCTracer(tracer_params.into())
810            }
811        }
812    }
813}
814
815#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Eq, Hash, DeepSizeOf)]
816pub enum StorageOverride {
817    Diff(BTreeMap<StoreKey, StoreVal>),
818    Replace(BTreeMap<StoreKey, StoreVal>),
819}
820
821impl From<dto::StorageOverride> for StorageOverride {
822    fn from(value: dto::StorageOverride) -> Self {
823        match value {
824            dto::StorageOverride::Diff(diff) => StorageOverride::Diff(diff),
825            dto::StorageOverride::Replace(replace) => StorageOverride::Replace(replace),
826        }
827    }
828}
829
830#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Eq, Hash, DeepSizeOf)]
831pub struct AccountOverrides {
832    pub slots: Option<StorageOverride>,
833    pub native_balance: Option<Balance>,
834    pub code: Option<Code>,
835}
836
837impl From<dto::AccountOverrides> for AccountOverrides {
838    fn from(value: dto::AccountOverrides) -> Self {
839        Self {
840            slots: value.slots.map(|s| s.into()),
841            native_balance: value.native_balance,
842            code: value.code,
843        }
844    }
845}
846
847#[derive(Debug, Clone, PartialEq, Deserialize, Eq, Hash, DeepSizeOf)]
848pub struct RPCTracerParams {
849    /// The caller address of the transaction, if not provided tracing will use the default value
850    /// for an address defined by the VM.
851    pub caller: Option<Address>,
852    /// The call data used for the tracing call, this needs to include the function selector
853    pub calldata: Bytes,
854    /// Optionally allow for state overrides so that the call works as expected
855    pub state_overrides: Option<BTreeMap<Address, AccountOverrides>>,
856    /// Addresses to prune from trace results. Useful for hooks that use mock
857    /// accounts/routers that shouldn't be tracked in the final DCI results.
858    pub prune_addresses: Option<Vec<Address>>,
859}
860
861impl From<dto::RPCTracerParams> for RPCTracerParams {
862    fn from(value: dto::RPCTracerParams) -> Self {
863        Self {
864            caller: value.caller,
865            calldata: value.calldata,
866            state_overrides: value.state_overrides.map(|overrides| {
867                overrides
868                    .into_iter()
869                    .map(|(address, account_overrides)| (address, account_overrides.into()))
870                    .collect()
871            }),
872            prune_addresses: value.prune_addresses,
873        }
874    }
875}
876
877impl RPCTracerParams {
878    pub fn new(caller: Option<Address>, calldata: Bytes) -> Self {
879        Self { caller, calldata, state_overrides: None, prune_addresses: None }
880    }
881
882    pub fn with_state_overrides(mut self, state: BTreeMap<Address, AccountOverrides>) -> Self {
883        self.state_overrides = Some(state);
884        self
885    }
886
887    pub fn with_prune_addresses(mut self, addresses: Vec<Address>) -> Self {
888        self.prune_addresses = Some(addresses);
889        self
890    }
891}
892
893impl std::fmt::Display for RPCTracerParams {
894    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
895        let caller_str = match &self.caller {
896            Some(addr) => format!("caller={addr}"),
897            None => String::new(),
898        };
899
900        let calldata_str = if self.calldata.len() >= 8 {
901            format!(
902                "calldata=0x{}..({} bytes)",
903                hex::encode(&self.calldata[..8]),
904                self.calldata.len()
905            )
906        } else {
907            format!("calldata={}", self.calldata)
908        };
909
910        let overrides_str = match &self.state_overrides {
911            Some(overrides) if !overrides.is_empty() => {
912                format!(", {} state override(s)", overrides.len())
913            }
914            _ => String::new(),
915        };
916
917        write!(f, "{caller_str}, {calldata_str}{overrides_str}")
918    }
919}
920
921// Ensure serialization order, required by the storage layer
922impl Serialize for RPCTracerParams {
923    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
924    where
925        S: Serializer,
926    {
927        // Count fields: always serialize caller and calldata, plus optional fields
928        let mut field_count = 2;
929        if self.state_overrides.is_some() {
930            field_count += 1;
931        }
932        if self.prune_addresses.is_some() {
933            field_count += 1;
934        }
935
936        let mut state = serializer.serialize_struct("RPCTracerEntryPoint", field_count)?;
937        state.serialize_field("caller", &self.caller)?;
938        state.serialize_field("calldata", &self.calldata)?;
939
940        // Only serialize optional fields if they are present
941        if let Some(ref overrides) = self.state_overrides {
942            state.serialize_field("state_overrides", overrides)?;
943        }
944        if let Some(ref prune_addrs) = self.prune_addresses {
945            state.serialize_field("prune_addresses", prune_addrs)?;
946        }
947
948        state.end()
949    }
950}
951
952#[derive(
953    Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, DeepSizeOf,
954)]
955pub struct AddressStorageLocation {
956    pub key: StoreKey,
957    pub offset: u8,
958}
959
960impl AddressStorageLocation {
961    pub fn new(key: StoreKey, offset: u8) -> Self {
962        Self { key, offset }
963    }
964}
965
966#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, DeepSizeOf)]
967pub struct TracingResult {
968    /// A set of (address, storage slot) pairs representing state that contain a called address.
969    /// If any of these storage slots change, the execution path might change.
970    pub retriggers: HashSet<(Address, AddressStorageLocation)>,
971    /// A map of all addresses that were called during the trace with a list of storage slots that
972    /// were accessed.
973    pub accessed_slots: HashMap<Address, HashSet<StoreKey>>,
974}
975
976impl TracingResult {
977    pub fn new(
978        retriggers: HashSet<(Address, AddressStorageLocation)>,
979        accessed_slots: HashMap<Address, HashSet<StoreKey>>,
980    ) -> Self {
981        Self { retriggers, accessed_slots }
982    }
983
984    /// Merges this tracing result with another one.
985    ///
986    /// The method combines two [`TracingResult`] instances.
987    pub fn merge(&mut self, other: TracingResult) {
988        self.retriggers.extend(other.retriggers);
989        for (address, slots) in other.accessed_slots {
990            self.accessed_slots
991                .entry(address)
992                .or_default()
993                .extend(slots);
994        }
995    }
996}
997
998#[derive(Debug, Clone, PartialEq, DeepSizeOf)]
999/// Represents a traced entry point and the results of the tracing operation.
1000pub struct TracedEntryPoint {
1001    /// The combined entry point and tracing params that was traced
1002    pub entry_point_with_params: EntryPointWithTracingParams,
1003    /// The block hash of the block that the entry point was traced on.
1004    pub detection_block_hash: BlockHash,
1005    /// The results of the tracing operation
1006    pub tracing_result: TracingResult,
1007}
1008
1009impl TracedEntryPoint {
1010    pub fn new(
1011        entry_point_with_params: EntryPointWithTracingParams,
1012        detection_block_hash: BlockHash,
1013        result: TracingResult,
1014    ) -> Self {
1015        Self { entry_point_with_params, detection_block_hash, tracing_result: result }
1016    }
1017
1018    pub fn entry_point_id(&self) -> String {
1019        self.entry_point_with_params
1020            .entry_point
1021            .external_id
1022            .clone()
1023    }
1024}
1025
1026impl std::fmt::Display for TracedEntryPoint {
1027    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1028        write!(
1029            f,
1030            "[{}: {} retriggers, {} accessed addresses]",
1031            self.entry_point_id(),
1032            self.tracing_result.retriggers.len(),
1033            self.tracing_result.accessed_slots.len()
1034        )
1035    }
1036}
1037
1038#[cfg(test)]
1039pub mod fixtures {
1040    use std::str::FromStr;
1041
1042    use rstest::rstest;
1043
1044    use super::*;
1045    use crate::models::ChangeType;
1046
1047    // PERF: duplicated in crate::extractor::models::fixtures — consider a `test-utils`
1048    // feature flag to share test fixtures cross-crate.
1049    pub fn create_transaction(hash: &str, block: &str, index: u64) -> Transaction {
1050        Transaction::new(
1051            hash.parse().unwrap(),
1052            block.parse().unwrap(),
1053            Bytes::zero(20),
1054            Some(Bytes::zero(20)),
1055            index,
1056        )
1057    }
1058
1059    /// Returns a pre-built `TxWithChanges` for testing.
1060    ///
1061    /// Both indices share the same keys (component `"pool_0"`, token `0xaa..`, contract `0xbb..`)
1062    /// but with different values, so "later wins" precedence can be verified across all fields:
1063    ///
1064    /// - Index 0: tx_index=1, component_balance {token=800, token2=300}, account_balance
1065    ///   {token=500, token2=150}, slots {1=>100, 2=>200}, state {"reserve"=>1000, "fee"=>50}, 1
1066    ///   entrypoint, ChangeType::Creation
1067    /// - Index 1: tx_index=2, component_balance {token=1000}, account_balance {token=700}, slots
1068    ///   {1=>300} (overlaps slot 1), state {"reserve"=>2000} (overlaps), 2 entrypoints (superset),
1069    ///   ChangeType::Update
1070    // PERF: duplicated in crate::extractor::models::fixtures — consider a `test-utils`
1071    // feature flag to share test fixtures cross-crate.
1072    pub fn tx_with_changes(index: u8) -> TxWithChanges {
1073        let token = Bytes::from(vec![0xaa; 20]);
1074        let token2 = Bytes::from(vec![0xcc; 20]);
1075        let contract = Bytes::from(vec![0xbb; 20]);
1076        let c_id = "pool_0".to_string();
1077
1078        match index {
1079            0 => {
1080                let tx = create_transaction("0x01", "0x00", 1);
1081                TxWithChanges {
1082                    tx: tx.clone(),
1083                    protocol_components: HashMap::from([(
1084                        c_id.clone(),
1085                        ProtocolComponent { id: c_id.clone(), ..Default::default() },
1086                    )]),
1087                    account_deltas: HashMap::from([(
1088                        contract.clone(),
1089                        AccountDelta::new(
1090                            Chain::Ethereum,
1091                            contract.clone(),
1092                            HashMap::from([
1093                                (
1094                                    Bytes::from(1u64).lpad(32, 0),
1095                                    Some(Bytes::from(100u64).lpad(32, 0)),
1096                                ),
1097                                (
1098                                    Bytes::from(2u64).lpad(32, 0),
1099                                    Some(Bytes::from(200u64).lpad(32, 0)),
1100                                ),
1101                            ]),
1102                            None,
1103                            Some(Bytes::from(vec![0; 4])),
1104                            ChangeType::Creation,
1105                        ),
1106                    )]),
1107                    state_updates: HashMap::from([(
1108                        c_id.clone(),
1109                        ProtocolComponentStateDelta::new(
1110                            &c_id,
1111                            HashMap::from([
1112                                ("reserve".into(), Bytes::from(1000u64).lpad(32, 0)),
1113                                ("fee".into(), Bytes::from(50u64).lpad(32, 0)),
1114                            ]),
1115                            HashSet::new(),
1116                        ),
1117                    )]),
1118                    balance_changes: HashMap::from([(
1119                        c_id.clone(),
1120                        HashMap::from([
1121                            (
1122                                token.clone(),
1123                                ComponentBalance {
1124                                    token: token.clone(),
1125                                    balance: Bytes::from(800_u64).lpad(32, 0),
1126                                    balance_float: 800.0,
1127                                    component_id: c_id.clone(),
1128                                    modify_tx: tx.hash.clone(),
1129                                },
1130                            ),
1131                            (
1132                                token2.clone(),
1133                                ComponentBalance {
1134                                    token: token2.clone(),
1135                                    balance: Bytes::from(300_u64).lpad(32, 0),
1136                                    balance_float: 300.0,
1137                                    component_id: c_id.clone(),
1138                                    modify_tx: tx.hash.clone(),
1139                                },
1140                            ),
1141                        ]),
1142                    )]),
1143                    account_balance_changes: HashMap::from([(
1144                        contract.clone(),
1145                        HashMap::from([
1146                            (
1147                                token.clone(),
1148                                AccountBalance {
1149                                    token: token.clone(),
1150                                    balance: Bytes::from(500_u64).lpad(32, 0),
1151                                    modify_tx: tx.hash.clone(),
1152                                    account: contract.clone(),
1153                                },
1154                            ),
1155                            (
1156                                token2,
1157                                AccountBalance {
1158                                    token: Bytes::from(vec![0xcc; 20]),
1159                                    balance: Bytes::from(150_u64).lpad(32, 0),
1160                                    modify_tx: tx.hash,
1161                                    account: contract,
1162                                },
1163                            ),
1164                        ]),
1165                    )]),
1166                    entrypoints: HashMap::from([(
1167                        c_id.clone(),
1168                        HashSet::from([EntryPoint::new(
1169                            "ep_0".into(),
1170                            Bytes::zero(20),
1171                            "fn_a()".into(),
1172                        )]),
1173                    )]),
1174                    entrypoint_params: HashMap::from([(
1175                        "ep_0".into(),
1176                        HashSet::from([(
1177                            TracingParams::RPCTracer(RPCTracerParams::new(
1178                                None,
1179                                Bytes::from(vec![1]),
1180                            )),
1181                            c_id,
1182                        )]),
1183                    )]),
1184                }
1185            }
1186            1 => {
1187                let tx = create_transaction("0x02", "0x00", 2);
1188                TxWithChanges {
1189                    tx: tx.clone(),
1190                    protocol_components: HashMap::from([(
1191                        c_id.clone(),
1192                        ProtocolComponent { id: c_id.clone(), ..Default::default() },
1193                    )]),
1194                    account_deltas: HashMap::from([(
1195                        contract.clone(),
1196                        AccountDelta::new(
1197                            Chain::Ethereum,
1198                            contract.clone(),
1199                            HashMap::from([(
1200                                Bytes::from(1u64).lpad(32, 0),
1201                                Some(Bytes::from(300u64).lpad(32, 0)),
1202                            )]),
1203                            None,
1204                            None,
1205                            ChangeType::Update,
1206                        ),
1207                    )]),
1208                    state_updates: HashMap::from([(
1209                        c_id.clone(),
1210                        ProtocolComponentStateDelta::new(
1211                            &c_id,
1212                            HashMap::from([("reserve".into(), Bytes::from(2000u64).lpad(32, 0))]),
1213                            HashSet::new(),
1214                        ),
1215                    )]),
1216                    balance_changes: HashMap::from([(
1217                        c_id.clone(),
1218                        HashMap::from([(
1219                            token.clone(),
1220                            ComponentBalance {
1221                                token: token.clone(),
1222                                balance: Bytes::from(1000_u64).lpad(32, 0),
1223                                balance_float: 1000.0,
1224                                component_id: c_id.clone(),
1225                                modify_tx: tx.hash.clone(),
1226                            },
1227                        )]),
1228                    )]),
1229                    account_balance_changes: HashMap::from([(
1230                        contract.clone(),
1231                        HashMap::from([(
1232                            token.clone(),
1233                            AccountBalance {
1234                                token: token.clone(),
1235                                balance: Bytes::from(700_u64).lpad(32, 0),
1236                                modify_tx: tx.hash,
1237                                account: contract,
1238                            },
1239                        )]),
1240                    )]),
1241                    entrypoints: HashMap::from([(
1242                        c_id.clone(),
1243                        HashSet::from([
1244                            EntryPoint::new("ep_0".into(), Bytes::zero(20), "fn_a()".into()),
1245                            EntryPoint::new("ep_1".into(), Bytes::zero(20), "fn_b()".into()),
1246                        ]),
1247                    )]),
1248                    entrypoint_params: HashMap::from([(
1249                        "ep_1".into(),
1250                        HashSet::from([(
1251                            TracingParams::RPCTracer(RPCTracerParams::new(
1252                                None,
1253                                Bytes::from(vec![2]),
1254                            )),
1255                            c_id,
1256                        )]),
1257                    )]),
1258                }
1259            }
1260            _ => panic!("tx_with_changes: index must be 0 or 1, got {index}"),
1261        }
1262    }
1263
1264    #[test]
1265    fn test_merge_tx_with_changes() {
1266        let mut changes1 = tx_with_changes(0);
1267        let changes2 = tx_with_changes(1);
1268
1269        let token = Bytes::from(vec![0xaa; 20]);
1270        let contract = Bytes::from(vec![0xbb; 20]);
1271        let c_id = "pool_0".to_string();
1272
1273        assert!(changes1.merge(changes2).is_ok());
1274
1275        // After merge, balances should reflect changes2 ("later wins")
1276        assert_eq!(
1277            changes1.balance_changes[&c_id][&token].balance,
1278            Bytes::from(1000_u64).lpad(32, 0),
1279        );
1280        assert_eq!(
1281            changes1.account_balance_changes[&contract][&token].balance,
1282            Bytes::from(700_u64).lpad(32, 0),
1283        );
1284        // tx should be updated to changes2's tx
1285        assert_eq!(changes1.tx.hash, Bytes::from(vec![2]));
1286        // Entrypoints should be merged (union of both)
1287        assert_eq!(changes1.entrypoints[&c_id].len(), 2);
1288        let mut sigs: Vec<_> = changes1.entrypoints[&c_id]
1289            .iter()
1290            .map(|ep| ep.signature.clone())
1291            .collect();
1292        sigs.sort();
1293        assert_eq!(sigs, vec!["fn_a()", "fn_b()"]);
1294    }
1295
1296    #[rstest]
1297    #[case::mismatched_blocks(
1298        fixtures::create_transaction("0x01", "0x0abc", 1),
1299        fixtures::create_transaction("0x02", "0x0def", 2)
1300    )]
1301    #[case::older_transaction(
1302        fixtures::create_transaction("0x02", "0x0abc", 2),
1303        fixtures::create_transaction("0x01", "0x0abc", 1)
1304    )]
1305    fn test_merge_errors(#[case] tx1: Transaction, #[case] tx2: Transaction) {
1306        let mut changes1 = TxWithChanges { tx: tx1, ..Default::default() };
1307
1308        let changes2 = TxWithChanges { tx: tx2, ..Default::default() };
1309
1310        assert!(changes1.merge(changes2).is_err());
1311    }
1312
1313    #[test]
1314    fn test_rpc_tracer_entry_point_serialization_order() {
1315        use std::str::FromStr;
1316
1317        use serde_json;
1318
1319        let entry_point = RPCTracerParams::new(
1320            Some(Address::from_str("0x1234567890123456789012345678901234567890").unwrap()),
1321            Bytes::from_str("0xabcdef").unwrap(),
1322        );
1323
1324        let serialized = serde_json::to_string(&entry_point).unwrap();
1325
1326        // Verify that "caller" comes before "calldata" in the serialized output
1327        assert!(serialized.find("\"caller\"").unwrap() < serialized.find("\"calldata\"").unwrap());
1328
1329        // Verify we can deserialize it back
1330        let deserialized: RPCTracerParams = serde_json::from_str(&serialized).unwrap();
1331        assert_eq!(entry_point, deserialized);
1332    }
1333
1334    #[test]
1335    fn test_tracing_result_merge() {
1336        let address1 = Address::from_str("0x1234567890123456789012345678901234567890").unwrap();
1337        let address2 = Address::from_str("0x2345678901234567890123456789012345678901").unwrap();
1338        let address3 = Address::from_str("0x3456789012345678901234567890123456789012").unwrap();
1339
1340        let store_key1 = StoreKey::from(vec![1, 2, 3, 4]);
1341        let store_key2 = StoreKey::from(vec![5, 6, 7, 8]);
1342
1343        let mut result1 = TracingResult::new(
1344            HashSet::from([(
1345                address1.clone(),
1346                AddressStorageLocation::new(store_key1.clone(), 12),
1347            )]),
1348            HashMap::from([
1349                (address2.clone(), HashSet::from([store_key1.clone()])),
1350                (address3.clone(), HashSet::from([store_key2.clone()])),
1351            ]),
1352        );
1353
1354        let result2 = TracingResult::new(
1355            HashSet::from([(
1356                address3.clone(),
1357                AddressStorageLocation::new(store_key2.clone(), 12),
1358            )]),
1359            HashMap::from([
1360                (address1.clone(), HashSet::from([store_key1.clone()])),
1361                (address2.clone(), HashSet::from([store_key2.clone()])),
1362            ]),
1363        );
1364
1365        result1.merge(result2);
1366
1367        // Verify retriggers were merged
1368        assert_eq!(result1.retriggers.len(), 2);
1369        assert!(result1
1370            .retriggers
1371            .contains(&(address1.clone(), AddressStorageLocation::new(store_key1.clone(), 12))));
1372        assert!(result1
1373            .retriggers
1374            .contains(&(address3.clone(), AddressStorageLocation::new(store_key2.clone(), 12))));
1375
1376        // Verify accessed slots were merged
1377        assert_eq!(result1.accessed_slots.len(), 3);
1378        assert!(result1
1379            .accessed_slots
1380            .contains_key(&address1));
1381        assert!(result1
1382            .accessed_slots
1383            .contains_key(&address2));
1384        assert!(result1
1385            .accessed_slots
1386            .contains_key(&address3));
1387
1388        assert_eq!(
1389            result1
1390                .accessed_slots
1391                .get(&address2)
1392                .unwrap(),
1393            &HashSet::from([store_key1.clone(), store_key2.clone()])
1394        );
1395    }
1396
1397    #[test]
1398    fn test_entry_point_with_tracing_params_display() {
1399        use std::str::FromStr;
1400
1401        let entry_point = EntryPoint::new(
1402            "uniswap_v3_pool_swap".to_string(),
1403            Address::from_str("0x1234567890123456789012345678901234567890").unwrap(),
1404            "swapExactETHForTokens(uint256,address[],address,uint256)".to_string(),
1405        );
1406
1407        let tracing_params = TracingParams::RPCTracer(RPCTracerParams::new(
1408            Some(Address::from_str("0x9876543210987654321098765432109876543210").unwrap()),
1409            Bytes::from_str("0xabcdef").unwrap(),
1410        ));
1411
1412        let entry_point_with_params = EntryPointWithTracingParams::new(entry_point, tracing_params);
1413
1414        let display_output = entry_point_with_params.to_string();
1415        assert_eq!(display_output, "uniswap_v3_pool_swap [RPC]");
1416    }
1417
1418    #[test]
1419    fn test_traced_entry_point_display() {
1420        use std::str::FromStr;
1421
1422        let entry_point = EntryPoint::new(
1423            "uniswap_v3_pool_swap".to_string(),
1424            Address::from_str("0x1234567890123456789012345678901234567890").unwrap(),
1425            "swapExactETHForTokens(uint256,address[],address,uint256)".to_string(),
1426        );
1427
1428        let tracing_params = TracingParams::RPCTracer(RPCTracerParams::new(
1429            Some(Address::from_str("0x9876543210987654321098765432109876543210").unwrap()),
1430            Bytes::from_str("0xabcdef").unwrap(),
1431        ));
1432
1433        let entry_point_with_params = EntryPointWithTracingParams::new(entry_point, tracing_params);
1434
1435        // Create tracing result with 2 retriggers and 3 accessed addresses
1436        let address1 = Address::from_str("0x1111111111111111111111111111111111111111").unwrap();
1437        let address2 = Address::from_str("0x2222222222222222222222222222222222222222").unwrap();
1438        let address3 = Address::from_str("0x3333333333333333333333333333333333333333").unwrap();
1439
1440        let store_key1 = StoreKey::from(vec![1, 2, 3, 4]);
1441        let store_key2 = StoreKey::from(vec![5, 6, 7, 8]);
1442
1443        let tracing_result = TracingResult::new(
1444            HashSet::from([
1445                (address1.clone(), AddressStorageLocation::new(store_key1.clone(), 0)),
1446                (address2.clone(), AddressStorageLocation::new(store_key2.clone(), 12)),
1447            ]),
1448            HashMap::from([
1449                (address1.clone(), HashSet::from([store_key1.clone()])),
1450                (address2.clone(), HashSet::from([store_key2.clone()])),
1451                (address3.clone(), HashSet::from([store_key1.clone()])),
1452            ]),
1453        );
1454
1455        let traced_entry_point = TracedEntryPoint::new(
1456            entry_point_with_params,
1457            Bytes::from_str("0xabcdef1234567890").unwrap(),
1458            tracing_result,
1459        );
1460
1461        let display_output = traced_entry_point.to_string();
1462        assert_eq!(display_output, "[uniswap_v3_pool_swap: 2 retriggers, 3 accessed addresses]");
1463    }
1464}