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, AccountToContractChanges},
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/// Storage changes grouped by transaction.
1039#[derive(Debug, PartialEq, Default, Clone, DeepSizeOf)]
1040pub struct TxWithContractChanges {
1041    pub tx: Transaction,
1042    pub contract_changes: AccountToContractChanges,
1043}
1044
1045#[derive(Debug, PartialEq, Default, Clone, DeepSizeOf)]
1046pub struct BlockChanges {
1047    pub extractor: String,
1048    pub chain: Chain,
1049    pub block: Block,
1050    pub finalized_block_height: u64,
1051    pub revert: bool,
1052    pub new_tokens: HashMap<Address, Token>,
1053    /// Vec of updates at this block, aggregated by tx and sorted by tx index in ascending order
1054    pub txs_with_update: Vec<TxWithChanges>,
1055    // Raw block contract changes. This is intended as DCI input and is to be omitted from the
1056    // reorg buffer and aggregation into the `BlockAggregatedChanges` object.
1057    pub block_contract_changes: Vec<TxWithContractChanges>,
1058    /// Required here so that it is part of the reorg buffer and thus inserted into storage once
1059    /// finalized.
1060    /// Populated by the `DynamicContractIndexer`
1061    pub trace_results: Vec<TracedEntryPoint>,
1062    /// The index of the partial block. None if it's a full block.
1063    pub partial_block_index: Option<u32>,
1064}
1065
1066impl BlockChanges {
1067    pub fn new(
1068        extractor: String,
1069        chain: Chain,
1070        block: Block,
1071        finalized_block_height: u64,
1072        revert: bool,
1073        txs_with_update: Vec<TxWithChanges>,
1074        block_contract_changes: Vec<TxWithContractChanges>,
1075    ) -> Self {
1076        BlockChanges {
1077            extractor,
1078            chain,
1079            block,
1080            finalized_block_height,
1081            revert,
1082            new_tokens: HashMap::new(),
1083            txs_with_update,
1084            block_contract_changes,
1085            trace_results: Vec::new(),
1086            partial_block_index: None,
1087        }
1088    }
1089
1090    /// Aggregates component and account updates.
1091    ///
1092    /// This function aggregates all protocol updates into a [`BlockAggregatedChanges`] object. This
1093    /// new object should have all individual changes merged into only one final/compacted change
1094    /// per component and account. This means there is only one state delta and component balance
1095    /// per component, and one account delta and account balance per account. DCI trace results are
1096    /// also aggregated into a result per entry point.
1097    ///
1098    /// Note - all non-protocol specific data in the BlockChanges object are lost during
1099    /// aggregation. This means block_storage_changes is dropped.
1100    ///
1101    /// # Errors
1102    ///
1103    /// This returns a `MergeError` if there was a problem during merge.
1104    pub fn into_aggregated(
1105        self,
1106        db_committed_block_height: Option<u64>,
1107    ) -> Result<BlockAggregatedChanges, MergeError> {
1108        if db_committed_block_height.is_some_and(|h| h > self.finalized_block_height) {
1109            return Err(MergeError::InvalidState(format!(
1110                "Database committed block height {:?} is greater than finalized_block_height {}",
1111                db_committed_block_height, self.finalized_block_height
1112            )));
1113        }
1114
1115        let mut iter = self.txs_with_update.into_iter();
1116
1117        let first_state = iter.next().unwrap_or_default();
1118
1119        let aggregated_changes = iter.try_fold(first_state, |mut acc_state, new_state| {
1120            acc_state.merge(new_state.clone())?;
1121            Ok::<_, MergeError>(acc_state.clone())
1122        })?;
1123
1124        // Aggregate trace_results
1125        let mut aggregated_trace_results = HashMap::new();
1126        for result in self.trace_results {
1127            let external_id = result.entry_point_id();
1128            aggregated_trace_results
1129                .entry(external_id)
1130                .and_modify(|existing: &mut TracingResult| {
1131                    existing.merge(result.tracing_result.clone())
1132                })
1133                .or_insert(result.tracing_result);
1134        }
1135
1136        Ok(BlockAggregatedChanges {
1137            extractor: self.extractor,
1138            chain: self.chain,
1139            block: self.block,
1140            db_committed_block_height,
1141            finalized_block_height: self.finalized_block_height,
1142            revert: self.revert,
1143            new_protocol_components: aggregated_changes.protocol_components,
1144            new_tokens: self.new_tokens,
1145            deleted_protocol_components: HashMap::new(),
1146            state_deltas: aggregated_changes.state_updates,
1147            account_deltas: aggregated_changes.account_deltas,
1148            component_balances: aggregated_changes.balance_changes,
1149            account_balances: aggregated_changes.account_balance_changes,
1150            component_tvl: HashMap::new(),
1151            dci_update: DCIUpdate {
1152                new_entrypoints: aggregated_changes.entrypoints,
1153                new_entrypoint_params: aggregated_changes.entrypoint_params,
1154                trace_results: aggregated_trace_results,
1155            },
1156            partial_block_index: self.partial_block_index,
1157        })
1158    }
1159
1160    pub fn protocol_components(&self) -> Vec<ProtocolComponent> {
1161        self.txs_with_update
1162            .iter()
1163            .flat_map(|tx_u| {
1164                tx_u.protocol_components
1165                    .values()
1166                    .cloned()
1167            })
1168            .collect()
1169    }
1170
1171    /// Returns true if the block is a partial block.
1172    pub fn is_partial_block(&self) -> bool {
1173        self.partial_block_index.is_some()
1174    }
1175
1176    /// Sets the partial block index.
1177    pub fn set_partial_block_index(&mut self, index: Option<u32>) {
1178        self.partial_block_index = index;
1179    }
1180
1181    /// Sets every transaction's `block_hash` in `txs_with_update` and `block_contract_changes`
1182    /// to this block's hash. Used after merging partials so all txs refer to the same block
1183    /// (e.g. the final block hash).
1184    pub fn normalize_block_hash(&mut self) {
1185        let h = self.block.hash.clone();
1186        for tx_with_changes in self.txs_with_update.iter_mut() {
1187            tx_with_changes.tx.block_hash = h.clone();
1188        }
1189        for tx_with_contract in self.block_contract_changes.iter_mut() {
1190            tx_with_contract.tx.block_hash = h.clone();
1191        }
1192    }
1193
1194    /// Merges another partial block into this one, preserving later changes.
1195    ///
1196    /// The partial block with the higher index represents later changes and takes precedence.
1197    /// Merges `new_tokens`, `txs_with_update` (sorted by index), `block_contract_changes`,
1198    /// and `trace_results`. When both blocks have the same token address, the token from the
1199    /// block with the higher partial index is kept.
1200    ///
1201    /// Works regardless of merge order: `partial_0.merge_partial(partial_1)` and
1202    /// `partial_1.merge_partial(partial_0)` produce equivalent results.
1203    ///
1204    /// # Errors
1205    /// - Non-partial block: Either block is not marked as partial
1206    /// - Extractor mismatch: Blocks from different extractors
1207    /// - Chain mismatch: Blocks from different chains
1208    /// - Block mismatch: Different block numbers (hash may differ for temp vs final partial)
1209    /// - Revert mismatch: Different revert status
1210    pub fn merge_partial(self, other: Self) -> Result<Self, MergeError> {
1211        let Some(self_index) = self.partial_block_index else {
1212            return Err(MergeError::InvalidState("self is not a partial block".to_string()));
1213        };
1214
1215        let Some(other_index) = other.partial_block_index else {
1216            return Err(MergeError::InvalidState("other is not a partial block".to_string()));
1217        };
1218
1219        if self.extractor != other.extractor {
1220            return Err(MergeError::IdMismatch(
1221                "partial blocks (extractor)".to_string(),
1222                self.extractor.clone(),
1223                other.extractor.clone(),
1224            ));
1225        }
1226
1227        if self.chain != other.chain {
1228            return Err(MergeError::IdMismatch(
1229                "partial blocks (chain)".to_string(),
1230                format!("{:?}", self.chain),
1231                format!("{:?}", other.chain),
1232            ));
1233        }
1234
1235        if self.block.number != other.block.number {
1236            return Err(MergeError::BlockMismatch(
1237                "partial blocks".to_string(),
1238                self.block.hash.clone(),
1239                other.block.hash.clone(),
1240            ));
1241        }
1242
1243        if self.revert != other.revert {
1244            return Err(MergeError::InvalidState(format!(
1245                "different revert status: {} vs {}",
1246                self.revert, other.revert
1247            )));
1248        }
1249
1250        let (mut current, previous) = if self_index > other_index {
1251            (self, other)
1252        } else if self_index < other_index {
1253            (other, self)
1254        } else {
1255            return Err(MergeError::InvalidState(format!("same partial block index: {self_index}")));
1256        };
1257
1258        for (addr, token) in previous.new_tokens {
1259            current
1260                .new_tokens
1261                .entry(addr)
1262                .or_insert(token);
1263        }
1264
1265        current
1266            .txs_with_update
1267            .extend(previous.txs_with_update);
1268        current
1269            .txs_with_update
1270            .sort_by_key(|tx| tx.tx.index);
1271
1272        current
1273            .block_contract_changes
1274            .extend(previous.block_contract_changes);
1275
1276        current.normalize_block_hash();
1277
1278        current
1279            .trace_results
1280            .extend(previous.trace_results);
1281
1282        Ok(current)
1283    }
1284}
1285
1286impl BlockScoped for BlockChanges {
1287    fn block(&self) -> Block {
1288        self.block.clone()
1289    }
1290}
1291
1292#[cfg(test)]
1293pub mod fixtures {
1294    use std::str::FromStr;
1295
1296    use rstest::rstest;
1297
1298    use super::*;
1299    use crate::models::ChangeType;
1300
1301    // PERF: duplicated in crate::extractor::models::fixtures — consider a `test-utils`
1302    // feature flag to share test fixtures cross-crate.
1303    pub fn create_transaction(hash: &str, block: &str, index: u64) -> Transaction {
1304        Transaction::new(
1305            hash.parse().unwrap(),
1306            block.parse().unwrap(),
1307            Bytes::zero(20),
1308            Some(Bytes::zero(20)),
1309            index,
1310        )
1311    }
1312
1313    /// Returns a pre-built `TxWithChanges` for testing.
1314    ///
1315    /// Both indices share the same keys (component `"pool_0"`, token `0xaa..`, contract `0xbb..`)
1316    /// but with different values, so "later wins" precedence can be verified across all fields:
1317    ///
1318    /// - Index 0: tx_index=1, component_balance {token=800, token2=300}, account_balance
1319    ///   {token=500, token2=150}, slots {1=>100, 2=>200}, state {"reserve"=>1000, "fee"=>50}, 1
1320    ///   entrypoint, ChangeType::Creation
1321    /// - Index 1: tx_index=2, component_balance {token=1000}, account_balance {token=700}, slots
1322    ///   {1=>300} (overlaps slot 1), state {"reserve"=>2000} (overlaps), 2 entrypoints (superset),
1323    ///   ChangeType::Update
1324    // PERF: duplicated in crate::extractor::models::fixtures — consider a `test-utils`
1325    // feature flag to share test fixtures cross-crate.
1326    pub fn tx_with_changes(index: u8) -> TxWithChanges {
1327        let token = Bytes::from(vec![0xaa; 20]);
1328        let token2 = Bytes::from(vec![0xcc; 20]);
1329        let contract = Bytes::from(vec![0xbb; 20]);
1330        let c_id = "pool_0".to_string();
1331
1332        match index {
1333            0 => {
1334                let tx = create_transaction("0x01", "0x00", 1);
1335                TxWithChanges {
1336                    tx: tx.clone(),
1337                    protocol_components: HashMap::from([(
1338                        c_id.clone(),
1339                        ProtocolComponent { id: c_id.clone(), ..Default::default() },
1340                    )]),
1341                    account_deltas: HashMap::from([(
1342                        contract.clone(),
1343                        AccountDelta::new(
1344                            Chain::Ethereum,
1345                            contract.clone(),
1346                            HashMap::from([
1347                                (
1348                                    Bytes::from(1u64).lpad(32, 0),
1349                                    Some(Bytes::from(100u64).lpad(32, 0)),
1350                                ),
1351                                (
1352                                    Bytes::from(2u64).lpad(32, 0),
1353                                    Some(Bytes::from(200u64).lpad(32, 0)),
1354                                ),
1355                            ]),
1356                            None,
1357                            Some(Bytes::from(vec![0; 4])),
1358                            ChangeType::Creation,
1359                        ),
1360                    )]),
1361                    state_updates: HashMap::from([(
1362                        c_id.clone(),
1363                        ProtocolComponentStateDelta::new(
1364                            &c_id,
1365                            HashMap::from([
1366                                ("reserve".into(), Bytes::from(1000u64).lpad(32, 0)),
1367                                ("fee".into(), Bytes::from(50u64).lpad(32, 0)),
1368                            ]),
1369                            HashSet::new(),
1370                        ),
1371                    )]),
1372                    balance_changes: HashMap::from([(
1373                        c_id.clone(),
1374                        HashMap::from([
1375                            (
1376                                token.clone(),
1377                                ComponentBalance {
1378                                    token: token.clone(),
1379                                    balance: Bytes::from(800_u64).lpad(32, 0),
1380                                    balance_float: 800.0,
1381                                    component_id: c_id.clone(),
1382                                    modify_tx: tx.hash.clone(),
1383                                },
1384                            ),
1385                            (
1386                                token2.clone(),
1387                                ComponentBalance {
1388                                    token: token2.clone(),
1389                                    balance: Bytes::from(300_u64).lpad(32, 0),
1390                                    balance_float: 300.0,
1391                                    component_id: c_id.clone(),
1392                                    modify_tx: tx.hash.clone(),
1393                                },
1394                            ),
1395                        ]),
1396                    )]),
1397                    account_balance_changes: HashMap::from([(
1398                        contract.clone(),
1399                        HashMap::from([
1400                            (
1401                                token.clone(),
1402                                AccountBalance {
1403                                    token: token.clone(),
1404                                    balance: Bytes::from(500_u64).lpad(32, 0),
1405                                    modify_tx: tx.hash.clone(),
1406                                    account: contract.clone(),
1407                                },
1408                            ),
1409                            (
1410                                token2,
1411                                AccountBalance {
1412                                    token: Bytes::from(vec![0xcc; 20]),
1413                                    balance: Bytes::from(150_u64).lpad(32, 0),
1414                                    modify_tx: tx.hash,
1415                                    account: contract,
1416                                },
1417                            ),
1418                        ]),
1419                    )]),
1420                    entrypoints: HashMap::from([(
1421                        c_id.clone(),
1422                        HashSet::from([EntryPoint::new(
1423                            "ep_0".into(),
1424                            Bytes::zero(20),
1425                            "fn_a()".into(),
1426                        )]),
1427                    )]),
1428                    entrypoint_params: HashMap::from([(
1429                        "ep_0".into(),
1430                        HashSet::from([(
1431                            TracingParams::RPCTracer(RPCTracerParams::new(
1432                                None,
1433                                Bytes::from(vec![1]),
1434                            )),
1435                            c_id,
1436                        )]),
1437                    )]),
1438                }
1439            }
1440            1 => {
1441                let tx = create_transaction("0x02", "0x00", 2);
1442                TxWithChanges {
1443                    tx: tx.clone(),
1444                    protocol_components: HashMap::from([(
1445                        c_id.clone(),
1446                        ProtocolComponent { id: c_id.clone(), ..Default::default() },
1447                    )]),
1448                    account_deltas: HashMap::from([(
1449                        contract.clone(),
1450                        AccountDelta::new(
1451                            Chain::Ethereum,
1452                            contract.clone(),
1453                            HashMap::from([(
1454                                Bytes::from(1u64).lpad(32, 0),
1455                                Some(Bytes::from(300u64).lpad(32, 0)),
1456                            )]),
1457                            None,
1458                            None,
1459                            ChangeType::Update,
1460                        ),
1461                    )]),
1462                    state_updates: HashMap::from([(
1463                        c_id.clone(),
1464                        ProtocolComponentStateDelta::new(
1465                            &c_id,
1466                            HashMap::from([("reserve".into(), Bytes::from(2000u64).lpad(32, 0))]),
1467                            HashSet::new(),
1468                        ),
1469                    )]),
1470                    balance_changes: HashMap::from([(
1471                        c_id.clone(),
1472                        HashMap::from([(
1473                            token.clone(),
1474                            ComponentBalance {
1475                                token: token.clone(),
1476                                balance: Bytes::from(1000_u64).lpad(32, 0),
1477                                balance_float: 1000.0,
1478                                component_id: c_id.clone(),
1479                                modify_tx: tx.hash.clone(),
1480                            },
1481                        )]),
1482                    )]),
1483                    account_balance_changes: HashMap::from([(
1484                        contract.clone(),
1485                        HashMap::from([(
1486                            token.clone(),
1487                            AccountBalance {
1488                                token: token.clone(),
1489                                balance: Bytes::from(700_u64).lpad(32, 0),
1490                                modify_tx: tx.hash,
1491                                account: contract,
1492                            },
1493                        )]),
1494                    )]),
1495                    entrypoints: HashMap::from([(
1496                        c_id.clone(),
1497                        HashSet::from([
1498                            EntryPoint::new("ep_0".into(), Bytes::zero(20), "fn_a()".into()),
1499                            EntryPoint::new("ep_1".into(), Bytes::zero(20), "fn_b()".into()),
1500                        ]),
1501                    )]),
1502                    entrypoint_params: HashMap::from([(
1503                        "ep_1".into(),
1504                        HashSet::from([(
1505                            TracingParams::RPCTracer(RPCTracerParams::new(
1506                                None,
1507                                Bytes::from(vec![2]),
1508                            )),
1509                            c_id,
1510                        )]),
1511                    )]),
1512                }
1513            }
1514            _ => panic!("tx_with_changes: index must be 0 or 1, got {index}"),
1515        }
1516    }
1517
1518    #[test]
1519    fn test_merge_tx_with_changes() {
1520        let mut changes1 = tx_with_changes(0);
1521        let changes2 = tx_with_changes(1);
1522
1523        let token = Bytes::from(vec![0xaa; 20]);
1524        let contract = Bytes::from(vec![0xbb; 20]);
1525        let c_id = "pool_0".to_string();
1526
1527        assert!(changes1.merge(changes2).is_ok());
1528
1529        // After merge, balances should reflect changes2 ("later wins")
1530        assert_eq!(
1531            changes1.balance_changes[&c_id][&token].balance,
1532            Bytes::from(1000_u64).lpad(32, 0),
1533        );
1534        assert_eq!(
1535            changes1.account_balance_changes[&contract][&token].balance,
1536            Bytes::from(700_u64).lpad(32, 0),
1537        );
1538        // tx should be updated to changes2's tx
1539        assert_eq!(changes1.tx.hash, Bytes::from(vec![2]));
1540        // Entrypoints should be merged (union of both)
1541        assert_eq!(changes1.entrypoints[&c_id].len(), 2);
1542        let mut sigs: Vec<_> = changes1.entrypoints[&c_id]
1543            .iter()
1544            .map(|ep| ep.signature.clone())
1545            .collect();
1546        sigs.sort();
1547        assert_eq!(sigs, vec!["fn_a()", "fn_b()"]);
1548    }
1549
1550    #[rstest]
1551    #[case::mismatched_blocks(
1552        fixtures::create_transaction("0x01", "0x0abc", 1),
1553        fixtures::create_transaction("0x02", "0x0def", 2)
1554    )]
1555    #[case::older_transaction(
1556        fixtures::create_transaction("0x02", "0x0abc", 2),
1557        fixtures::create_transaction("0x01", "0x0abc", 1)
1558    )]
1559    fn test_merge_errors(#[case] tx1: Transaction, #[case] tx2: Transaction) {
1560        let mut changes1 = TxWithChanges { tx: tx1, ..Default::default() };
1561
1562        let changes2 = TxWithChanges { tx: tx2, ..Default::default() };
1563
1564        assert!(changes1.merge(changes2).is_err());
1565    }
1566
1567    #[test]
1568    fn test_rpc_tracer_entry_point_serialization_order() {
1569        use std::str::FromStr;
1570
1571        use serde_json;
1572
1573        let entry_point = RPCTracerParams::new(
1574            Some(Address::from_str("0x1234567890123456789012345678901234567890").unwrap()),
1575            Bytes::from_str("0xabcdef").unwrap(),
1576        );
1577
1578        let serialized = serde_json::to_string(&entry_point).unwrap();
1579
1580        // Verify that "caller" comes before "calldata" in the serialized output
1581        assert!(serialized.find("\"caller\"").unwrap() < serialized.find("\"calldata\"").unwrap());
1582
1583        // Verify we can deserialize it back
1584        let deserialized: RPCTracerParams = serde_json::from_str(&serialized).unwrap();
1585        assert_eq!(entry_point, deserialized);
1586    }
1587
1588    #[test]
1589    fn test_tracing_result_merge() {
1590        let address1 = Address::from_str("0x1234567890123456789012345678901234567890").unwrap();
1591        let address2 = Address::from_str("0x2345678901234567890123456789012345678901").unwrap();
1592        let address3 = Address::from_str("0x3456789012345678901234567890123456789012").unwrap();
1593
1594        let store_key1 = StoreKey::from(vec![1, 2, 3, 4]);
1595        let store_key2 = StoreKey::from(vec![5, 6, 7, 8]);
1596
1597        let mut result1 = TracingResult::new(
1598            HashSet::from([(
1599                address1.clone(),
1600                AddressStorageLocation::new(store_key1.clone(), 12),
1601            )]),
1602            HashMap::from([
1603                (address2.clone(), HashSet::from([store_key1.clone()])),
1604                (address3.clone(), HashSet::from([store_key2.clone()])),
1605            ]),
1606        );
1607
1608        let result2 = TracingResult::new(
1609            HashSet::from([(
1610                address3.clone(),
1611                AddressStorageLocation::new(store_key2.clone(), 12),
1612            )]),
1613            HashMap::from([
1614                (address1.clone(), HashSet::from([store_key1.clone()])),
1615                (address2.clone(), HashSet::from([store_key2.clone()])),
1616            ]),
1617        );
1618
1619        result1.merge(result2);
1620
1621        // Verify retriggers were merged
1622        assert_eq!(result1.retriggers.len(), 2);
1623        assert!(result1
1624            .retriggers
1625            .contains(&(address1.clone(), AddressStorageLocation::new(store_key1.clone(), 12))));
1626        assert!(result1
1627            .retriggers
1628            .contains(&(address3.clone(), AddressStorageLocation::new(store_key2.clone(), 12))));
1629
1630        // Verify accessed slots were merged
1631        assert_eq!(result1.accessed_slots.len(), 3);
1632        assert!(result1
1633            .accessed_slots
1634            .contains_key(&address1));
1635        assert!(result1
1636            .accessed_slots
1637            .contains_key(&address2));
1638        assert!(result1
1639            .accessed_slots
1640            .contains_key(&address3));
1641
1642        assert_eq!(
1643            result1
1644                .accessed_slots
1645                .get(&address2)
1646                .unwrap(),
1647            &HashSet::from([store_key1.clone(), store_key2.clone()])
1648        );
1649    }
1650
1651    #[test]
1652    fn test_entry_point_with_tracing_params_display() {
1653        use std::str::FromStr;
1654
1655        let entry_point = EntryPoint::new(
1656            "uniswap_v3_pool_swap".to_string(),
1657            Address::from_str("0x1234567890123456789012345678901234567890").unwrap(),
1658            "swapExactETHForTokens(uint256,address[],address,uint256)".to_string(),
1659        );
1660
1661        let tracing_params = TracingParams::RPCTracer(RPCTracerParams::new(
1662            Some(Address::from_str("0x9876543210987654321098765432109876543210").unwrap()),
1663            Bytes::from_str("0xabcdef").unwrap(),
1664        ));
1665
1666        let entry_point_with_params = EntryPointWithTracingParams::new(entry_point, tracing_params);
1667
1668        let display_output = entry_point_with_params.to_string();
1669        assert_eq!(display_output, "uniswap_v3_pool_swap [RPC]");
1670    }
1671
1672    #[test]
1673    fn test_traced_entry_point_display() {
1674        use std::str::FromStr;
1675
1676        let entry_point = EntryPoint::new(
1677            "uniswap_v3_pool_swap".to_string(),
1678            Address::from_str("0x1234567890123456789012345678901234567890").unwrap(),
1679            "swapExactETHForTokens(uint256,address[],address,uint256)".to_string(),
1680        );
1681
1682        let tracing_params = TracingParams::RPCTracer(RPCTracerParams::new(
1683            Some(Address::from_str("0x9876543210987654321098765432109876543210").unwrap()),
1684            Bytes::from_str("0xabcdef").unwrap(),
1685        ));
1686
1687        let entry_point_with_params = EntryPointWithTracingParams::new(entry_point, tracing_params);
1688
1689        // Create tracing result with 2 retriggers and 3 accessed addresses
1690        let address1 = Address::from_str("0x1111111111111111111111111111111111111111").unwrap();
1691        let address2 = Address::from_str("0x2222222222222222222222222222222222222222").unwrap();
1692        let address3 = Address::from_str("0x3333333333333333333333333333333333333333").unwrap();
1693
1694        let store_key1 = StoreKey::from(vec![1, 2, 3, 4]);
1695        let store_key2 = StoreKey::from(vec![5, 6, 7, 8]);
1696
1697        let tracing_result = TracingResult::new(
1698            HashSet::from([
1699                (address1.clone(), AddressStorageLocation::new(store_key1.clone(), 0)),
1700                (address2.clone(), AddressStorageLocation::new(store_key2.clone(), 12)),
1701            ]),
1702            HashMap::from([
1703                (address1.clone(), HashSet::from([store_key1.clone()])),
1704                (address2.clone(), HashSet::from([store_key2.clone()])),
1705                (address3.clone(), HashSet::from([store_key1.clone()])),
1706            ]),
1707        );
1708
1709        let traced_entry_point = TracedEntryPoint::new(
1710            entry_point_with_params,
1711            Bytes::from_str("0xabcdef1234567890").unwrap(),
1712            tracing_result,
1713        );
1714
1715        let display_output = traced_entry_point.to_string();
1716        assert_eq!(display_output, "[uniswap_v3_pool_swap: 2 retriggers, 3 accessed addresses]");
1717    }
1718}