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
153/// A block under construction with ordered transactions and post-execution account state.
154#[derive(Debug, Clone, Default)]
155pub struct PendingBlock {
156    block: Block,
157    txs: Vec<TxInput>,
158    accounts: HashMap<Address, AccountDelta>,
159}
160
161impl PendingBlock {
162    pub fn new(block: Block, txs: Vec<TxInput>, accounts: HashMap<Address, AccountDelta>) -> Self {
163        Self { block, txs, accounts }
164    }
165
166    /// The block being assembled; its number and timestamp are the transaction execution clock.
167    pub fn block(&self) -> &Block {
168        &self.block
169    }
170
171    /// Transactions in execution order.
172    pub fn txs(&self) -> &[TxInput] {
173        &self.txs
174    }
175
176    /// Post-execution state for accounts touched by the transactions.
177    /// May be empty when no consuming indexer needs account state.
178    pub fn accounts(&self) -> &HashMap<Address, AccountDelta> {
179        &self.accounts
180    }
181}
182
183pub struct BlockTransactionDeltas<T> {
184    pub extractor: String,
185    pub chain: Chain,
186    pub block: Block,
187    pub revert: bool,
188    pub deltas: Vec<TransactionDeltaGroup<T>>,
189}
190
191#[allow(dead_code)]
192pub struct TransactionDeltaGroup<T> {
193    changes: T,
194    protocol_component: HashMap<String, ProtocolComponent>,
195    component_balances: HashMap<String, ComponentBalance>,
196    component_tvl: HashMap<String, f64>,
197    tx: Transaction,
198}
199
200#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, DeepSizeOf)]
201pub struct BlockAggregatedChanges {
202    pub extractor: String,
203    pub chain: Chain,
204    pub block: Block,
205    pub finalized_block_height: u64,
206    pub db_committed_block_height: Option<u64>,
207    pub revert: bool,
208    pub state_deltas: HashMap<String, ProtocolComponentStateDelta>,
209    pub account_deltas: HashMap<Bytes, AccountDelta>,
210    pub new_tokens: HashMap<Address, Token>,
211    pub new_protocol_components: HashMap<String, ProtocolComponent>,
212    pub deleted_protocol_components: HashMap<String, ProtocolComponent>,
213    pub component_balances: HashMap<ComponentId, HashMap<Bytes, ComponentBalance>>,
214    pub account_balances: HashMap<Address, HashMap<Address, AccountBalance>>,
215    pub component_tvl: HashMap<String, f64>,
216    pub dci_update: DCIUpdate,
217    /// The index of the partial block. None if it's a full block.
218    #[serde(default, skip_serializing_if = "Option::is_none")]
219    pub partial_block_index: Option<u32>,
220}
221
222impl BlockAggregatedChanges {
223    #[allow(clippy::too_many_arguments)]
224    pub fn new(
225        extractor: &str,
226        chain: Chain,
227        block: Block,
228        db_committed_block_height: Option<u64>,
229        finalized_block_height: u64,
230        revert: bool,
231        state_deltas: HashMap<String, ProtocolComponentStateDelta>,
232        account_deltas: HashMap<Bytes, AccountDelta>,
233        new_tokens: HashMap<Address, Token>,
234        new_components: HashMap<String, ProtocolComponent>,
235        deleted_components: HashMap<String, ProtocolComponent>,
236        component_balances: HashMap<ComponentId, HashMap<Bytes, ComponentBalance>>,
237        account_balances: HashMap<Address, HashMap<Address, AccountBalance>>,
238        component_tvl: HashMap<String, f64>,
239        dci_update: DCIUpdate,
240    ) -> Self {
241        Self {
242            extractor: extractor.to_string(),
243            chain,
244            block,
245            db_committed_block_height,
246            finalized_block_height,
247            revert,
248            state_deltas,
249            account_deltas,
250            new_tokens,
251            new_protocol_components: new_components,
252            deleted_protocol_components: deleted_components,
253            component_balances,
254            account_balances,
255            component_tvl,
256            dci_update,
257            partial_block_index: None,
258        }
259    }
260
261    pub fn drop_state(&self) -> Self {
262        Self {
263            extractor: self.extractor.clone(),
264            chain: self.chain,
265            block: self.block.clone(),
266            db_committed_block_height: self.db_committed_block_height,
267            finalized_block_height: self.finalized_block_height,
268            revert: self.revert,
269            account_deltas: HashMap::new(),
270            state_deltas: HashMap::new(),
271            new_tokens: self.new_tokens.clone(),
272            new_protocol_components: self.new_protocol_components.clone(),
273            deleted_protocol_components: self.deleted_protocol_components.clone(),
274            component_balances: self.component_balances.clone(),
275            account_balances: self.account_balances.clone(),
276            component_tvl: self.component_tvl.clone(),
277            dci_update: self.dci_update.clone(),
278            partial_block_index: self.partial_block_index,
279        }
280    }
281
282    pub fn is_partial(&self) -> bool {
283        self.partial_block_index.is_some()
284    }
285
286    pub fn get_block(&self) -> &Block {
287        &self.block
288    }
289
290    pub fn n_changes(&self) -> usize {
291        self.account_deltas.len() + self.state_deltas.len()
292    }
293
294    pub fn filter_by_component<F: Fn(&str) -> bool>(&mut self, keep: F) {
295        self.state_deltas.retain(|k, _| keep(k));
296        self.component_balances
297            .retain(|k, _| keep(k));
298        self.component_tvl
299            .retain(|k, _| keep(k));
300    }
301
302    pub fn filter_by_contract<F: Fn(&Bytes) -> bool>(&mut self, keep: F) {
303        self.account_deltas
304            .retain(|k, _| keep(k));
305        self.account_balances
306            .retain(|k, _| keep(k));
307    }
308
309    /// Merges this update with another one, consuming both.
310    ///
311    /// `other` is assumed to be a more recent update than `self`.
312    pub fn merge(mut self, other: Self) -> Self {
313        for (k, v) in other.account_deltas {
314            match self.account_deltas.entry(k) {
315                Entry::Occupied(mut e) => {
316                    // best-effort: ignore merge errors (address mismatch is a bug)
317                    let _ = e.get_mut().merge(v);
318                }
319                Entry::Vacant(e) => {
320                    e.insert(v);
321                }
322            }
323        }
324
325        for (k, v) in other.state_deltas {
326            match self.state_deltas.entry(k) {
327                Entry::Occupied(mut e) => {
328                    let _ = e.get_mut().merge(v);
329                }
330                Entry::Vacant(e) => {
331                    e.insert(v);
332                }
333            }
334        }
335
336        for (component_id, balances) in other.component_balances {
337            self.component_balances
338                .entry(component_id)
339                .or_default()
340                .extend(balances);
341        }
342
343        for (account, balances) in other.account_balances {
344            self.account_balances
345                .entry(account)
346                .or_default()
347                .extend(balances);
348        }
349
350        self.component_tvl
351            .extend(other.component_tvl);
352        self.new_protocol_components
353            .extend(other.new_protocol_components);
354        self.deleted_protocol_components
355            .extend(other.deleted_protocol_components);
356        self.revert = other.revert;
357        self.block = other.block;
358        self
359    }
360}
361
362impl std::fmt::Display for BlockAggregatedChanges {
363    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
364        write!(f, "block_number: {}, extractor: {}", self.block.number, self.extractor)
365    }
366}
367
368pub trait BlockScoped {
369    fn block(&self) -> Block;
370}
371
372impl<T: BlockScoped> BlockScoped for std::sync::Arc<T> {
373    fn block(&self) -> Block {
374        (**self).block()
375    }
376}
377
378impl BlockScoped for BlockAggregatedChanges {
379    fn block(&self) -> Block {
380        self.block.clone()
381    }
382}
383
384impl From<dto::Block> for Block {
385    fn from(value: dto::Block) -> Self {
386        Self {
387            number: value.number,
388            chain: value.chain.into(),
389            hash: value.hash,
390            parent_hash: value.parent_hash,
391            ts: value.ts,
392        }
393    }
394}
395
396impl From<dto::AddressStorageLocation> for AddressStorageLocation {
397    fn from(value: dto::AddressStorageLocation) -> Self {
398        Self { key: value.key, offset: value.offset }
399    }
400}
401
402impl From<dto::TracingResult> for TracingResult {
403    fn from(value: dto::TracingResult) -> Self {
404        Self {
405            retriggers: value
406                .retriggers
407                .into_iter()
408                .map(|(addr, loc)| (addr, loc.into()))
409                .collect(),
410            accessed_slots: value.accessed_slots,
411        }
412    }
413}
414
415impl From<dto::DCIUpdate> for DCIUpdate {
416    fn from(value: dto::DCIUpdate) -> Self {
417        Self {
418            new_entrypoints: value
419                .new_entrypoints
420                .into_iter()
421                .map(|(k, v)| {
422                    (
423                        k,
424                        v.into_iter()
425                            .map(EntryPoint::from)
426                            .collect(),
427                    )
428                })
429                .collect(),
430            new_entrypoint_params: value
431                .new_entrypoint_params
432                .into_iter()
433                .map(|(k, v)| {
434                    (
435                        k,
436                        v.into_iter()
437                            .map(|(p, c)| (TracingParams::from(p), c))
438                            .collect(),
439                    )
440                })
441                .collect(),
442            trace_results: value
443                .trace_results
444                .into_iter()
445                .map(|(k, v)| (k, TracingResult::from(v)))
446                .collect(),
447        }
448    }
449}
450
451impl From<dto::BlockAggregatedChanges> for BlockAggregatedChanges {
452    fn from(value: dto::BlockAggregatedChanges) -> Self {
453        use crate::models::{
454            contract::{AccountBalance, AccountDelta},
455            protocol::{ComponentBalance, ProtocolComponent, ProtocolComponentStateDelta},
456            token::Token,
457        };
458        Self {
459            extractor: value.extractor,
460            chain: value.chain.into(),
461            block: value.block.into(),
462            finalized_block_height: value.finalized_block_height,
463            db_committed_block_height: None,
464            revert: value.revert,
465            state_deltas: value
466                .state_updates
467                .into_iter()
468                .map(|(k, v)| (k, ProtocolComponentStateDelta::from(v)))
469                .collect(),
470            account_deltas: value
471                .account_updates
472                .into_iter()
473                .map(|(k, v)| (k, AccountDelta::from(v)))
474                .collect(),
475            new_tokens: value
476                .new_tokens
477                .into_iter()
478                .map(|(k, v)| (k, Token::from(v)))
479                .collect(),
480            new_protocol_components: value
481                .new_protocol_components
482                .into_iter()
483                .map(|(k, v)| (k, ProtocolComponent::from(v)))
484                .collect(),
485            deleted_protocol_components: value
486                .deleted_protocol_components
487                .into_iter()
488                .map(|(k, v)| (k, ProtocolComponent::from(v)))
489                .collect(),
490            component_balances: value
491                .component_balances
492                .into_iter()
493                .map(|(component_id, token_balances)| {
494                    (
495                        component_id,
496                        token_balances
497                            .0
498                            .into_iter()
499                            .map(|(k, v)| (k, ComponentBalance::from(v)))
500                            .collect(),
501                    )
502                })
503                .collect(),
504            account_balances: value
505                .account_balances
506                .into_iter()
507                .map(|(account, balances)| {
508                    (
509                        account,
510                        balances
511                            .into_iter()
512                            .map(|(k, v)| (k, AccountBalance::from(v)))
513                            .collect(),
514                    )
515                })
516                .collect(),
517            component_tvl: value.component_tvl,
518            dci_update: DCIUpdate::from(value.dci_update),
519            partial_block_index: value.partial_block_index,
520        }
521    }
522}
523
524#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, DeepSizeOf)]
525pub struct DCIUpdate {
526    pub new_entrypoints: HashMap<ComponentId, HashSet<EntryPoint>>,
527    pub new_entrypoint_params: HashMap<EntryPointId, HashSet<(TracingParams, ComponentId)>>,
528    pub trace_results: HashMap<EntryPointId, TracingResult>,
529}
530
531/// Traced entry points for a set of components, as returned by the Tycho RPC.
532///
533/// Maps each component ID to the list of `(EntryPointWithTracingParams, TracingResult)` pairs
534/// produced by the tracer for that component.
535pub type TracedEntryPoints =
536    HashMap<ComponentId, Vec<(EntryPointWithTracingParams, TracingResult)>>;
537
538impl From<TracedEntryPoints> for DCIUpdate {
539    fn from(traced_entry_points: TracedEntryPoints) -> Self {
540        let mut new_entrypoints: HashMap<ComponentId, HashSet<EntryPoint>> = HashMap::new();
541        let mut new_entrypoint_params: HashMap<
542            EntryPointId,
543            HashSet<(TracingParams, ComponentId)>,
544        > = HashMap::new();
545        let mut trace_results: HashMap<EntryPointId, TracingResult> = HashMap::new();
546
547        for (component_id, traces) in traced_entry_points {
548            let mut entrypoints = HashSet::new();
549
550            for (ep_with_params, trace) in traces {
551                let ep_id = ep_with_params
552                    .entry_point
553                    .external_id
554                    .clone();
555
556                entrypoints.insert(ep_with_params.entry_point.clone());
557
558                new_entrypoint_params
559                    .entry(ep_id.clone())
560                    .or_default()
561                    .insert((ep_with_params.params, component_id.clone()));
562
563                trace_results
564                    .entry(ep_id)
565                    .and_modify(|existing: &mut TracingResult| {
566                        existing
567                            .retriggers
568                            .extend(trace.retriggers.clone());
569                        for (address, slots) in trace.accessed_slots.clone() {
570                            existing
571                                .accessed_slots
572                                .entry(address)
573                                .or_default()
574                                .extend(slots);
575                        }
576                    })
577                    .or_insert(trace);
578            }
579
580            if !entrypoints.is_empty() {
581                new_entrypoints.insert(component_id, entrypoints);
582            }
583        }
584
585        DCIUpdate { new_entrypoints, new_entrypoint_params, trace_results }
586    }
587}
588
589/// Changes grouped by their respective transaction.
590#[derive(Debug, Clone, PartialEq, Default, DeepSizeOf)]
591pub struct TxWithChanges {
592    pub tx: Transaction,
593    pub protocol_components: HashMap<ComponentId, ProtocolComponent>,
594    pub account_deltas: HashMap<Address, AccountDelta>,
595    pub state_updates: HashMap<ComponentId, ProtocolComponentStateDelta>,
596    pub balance_changes: HashMap<ComponentId, HashMap<Address, ComponentBalance>>,
597    pub account_balance_changes: HashMap<Address, HashMap<Address, AccountBalance>>,
598    pub entrypoints: HashMap<ComponentId, HashSet<EntryPoint>>,
599    pub entrypoint_params: HashMap<EntryPointId, HashSet<(TracingParams, ComponentId)>>,
600}
601
602impl TxWithChanges {
603    #[allow(clippy::too_many_arguments)]
604    pub fn new(
605        tx: Transaction,
606        protocol_components: HashMap<ComponentId, ProtocolComponent>,
607        account_deltas: HashMap<Address, AccountDelta>,
608        protocol_states: HashMap<ComponentId, ProtocolComponentStateDelta>,
609        balance_changes: HashMap<ComponentId, HashMap<Address, ComponentBalance>>,
610        account_balance_changes: HashMap<Address, HashMap<Address, AccountBalance>>,
611        entrypoints: HashMap<ComponentId, HashSet<EntryPoint>>,
612        entrypoint_params: HashMap<EntryPointId, HashSet<(TracingParams, ComponentId)>>,
613    ) -> Self {
614        Self {
615            tx,
616            account_deltas,
617            protocol_components,
618            state_updates: protocol_states,
619            balance_changes,
620            account_balance_changes,
621            entrypoints,
622            entrypoint_params,
623        }
624    }
625
626    /// Merges this update with another one.
627    ///
628    /// The method combines two [`TxWithChanges`] instances if they are on the same block.
629    ///
630    /// NB: It is expected that `other` is a more recent update than `self` is and the two are
631    /// combined accordingly.
632    ///
633    /// # Errors
634    /// Returns a `MergeError` if any of the above conditions are violated.
635    pub fn merge(&mut self, other: TxWithChanges) -> Result<(), MergeError> {
636        if self.tx.block_hash != other.tx.block_hash {
637            return Err(MergeError::BlockMismatch(
638                "TxWithChanges".to_string(),
639                self.tx.block_hash.clone(),
640                other.tx.block_hash,
641            ));
642        }
643        if self.tx.index > other.tx.index {
644            return Err(MergeError::TransactionOrderError(
645                "TxWithChanges".to_string(),
646                self.tx.index,
647                other.tx.index,
648            ));
649        }
650
651        self.tx = other.tx;
652
653        // Merge new protocol components
654        // Log a warning if a new protocol component for the same id already exists, because this
655        // should never happen.
656        for (key, value) in other.protocol_components {
657            match self.protocol_components.entry(key) {
658                Entry::Occupied(mut entry) => {
659                    warn!(
660                        "Overwriting new protocol component for id {} with a new one. This should never happen! Please check logic",
661                        entry.get().id
662                    );
663                    entry.insert(value);
664                }
665                Entry::Vacant(entry) => {
666                    entry.insert(value);
667                }
668            }
669        }
670
671        // Merge account deltas
672        for (address, update) in other.account_deltas.clone().into_iter() {
673            match self.account_deltas.entry(address) {
674                Entry::Occupied(mut e) => {
675                    e.get_mut().merge(update)?;
676                }
677                Entry::Vacant(e) => {
678                    e.insert(update);
679                }
680            }
681        }
682
683        // Merge protocol state updates
684        for (key, value) in other.state_updates {
685            match self.state_updates.entry(key) {
686                Entry::Occupied(mut entry) => {
687                    entry.get_mut().merge(value)?;
688                }
689                Entry::Vacant(entry) => {
690                    entry.insert(value);
691                }
692            }
693        }
694
695        // Merge component balance changes
696        for (component_id, balance_changes) in other.balance_changes {
697            let token_balances = self
698                .balance_changes
699                .entry(component_id)
700                .or_default();
701            for (token, balance) in balance_changes {
702                token_balances.insert(token, balance);
703            }
704        }
705
706        // Merge account balance changes
707        for (account_addr, balance_changes) in other.account_balance_changes {
708            let token_balances = self
709                .account_balance_changes
710                .entry(account_addr)
711                .or_default();
712            for (token, balance) in balance_changes {
713                token_balances.insert(token, balance);
714            }
715        }
716
717        // Merge new entrypoints
718        for (component_id, entrypoints) in other.entrypoints {
719            self.entrypoints
720                .entry(component_id)
721                .or_default()
722                .extend(entrypoints);
723        }
724
725        // Merge new entrypoint params
726        for (entrypoint_id, params) in other.entrypoint_params {
727            self.entrypoint_params
728                .entry(entrypoint_id)
729                .or_default()
730                .extend(params);
731        }
732
733        Ok(())
734    }
735}
736
737#[derive(Copy, Clone, Debug, PartialEq)]
738pub enum BlockTag {
739    /// Finalized block
740    Finalized,
741    /// Safe block
742    Safe,
743    /// Latest block
744    Latest,
745    /// Earliest block (genesis)
746    Earliest,
747    /// Pending block (not yet part of the blockchain)
748    Pending,
749    /// Block by number
750    Number(u64),
751}
752#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, DeepSizeOf)]
753pub struct EntryPoint {
754    /// Entry point id
755    pub external_id: String,
756    /// The address of the contract to trace.
757    pub target: Address,
758    /// The signature of the function to trace.
759    pub signature: String,
760}
761
762impl EntryPoint {
763    pub fn new(external_id: String, target: Address, signature: String) -> Self {
764        Self { external_id, target, signature }
765    }
766}
767
768impl From<dto::EntryPoint> for EntryPoint {
769    fn from(value: dto::EntryPoint) -> Self {
770        Self { external_id: value.external_id, target: value.target, signature: value.signature }
771    }
772}
773
774/// A struct that combines an entry point with its associated tracing params.
775#[derive(Debug, Clone, PartialEq, Eq, Hash, DeepSizeOf)]
776pub struct EntryPointWithTracingParams {
777    /// The entry point to trace, containing the target contract address and function signature
778    pub entry_point: EntryPoint,
779    /// The tracing parameters for this entry point
780    pub params: TracingParams,
781}
782
783impl From<dto::EntryPointWithTracingParams> for EntryPointWithTracingParams {
784    fn from(value: dto::EntryPointWithTracingParams) -> Self {
785        match value.params {
786            dto::TracingParams::RPCTracer(ref tracer_params) => Self {
787                entry_point: EntryPoint {
788                    external_id: value.entry_point.external_id,
789                    target: value.entry_point.target,
790                    signature: value.entry_point.signature,
791                },
792                params: TracingParams::RPCTracer(RPCTracerParams {
793                    caller: tracer_params.caller.clone(),
794                    calldata: tracer_params.calldata.clone(),
795                    state_overrides: tracer_params
796                        .state_overrides
797                        .clone()
798                        .map(|s| {
799                            s.into_iter()
800                                .map(|(k, v)| (k, v.into()))
801                                .collect()
802                        }),
803                    prune_addresses: tracer_params.prune_addresses.clone(),
804                }),
805            },
806        }
807    }
808}
809
810impl EntryPointWithTracingParams {
811    pub fn new(entry_point: EntryPoint, params: TracingParams) -> Self {
812        Self { entry_point, params }
813    }
814}
815
816impl std::fmt::Display for EntryPointWithTracingParams {
817    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
818        let tracer_type = match &self.params {
819            TracingParams::RPCTracer(_) => "RPC",
820        };
821        write!(f, "{} [{}]", self.entry_point.external_id, tracer_type)
822    }
823}
824
825#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Eq, Hash, DeepSizeOf)]
826/// An entry point to trace. Different types of entry points tracing will be supported in the
827/// future. Like RPC debug tracing, symbolic execution, etc.
828pub enum TracingParams {
829    /// Uses RPC calls to retrieve the called addresses and retriggers
830    RPCTracer(RPCTracerParams),
831}
832
833impl std::fmt::Display for TracingParams {
834    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
835        match self {
836            TracingParams::RPCTracer(params) => write!(f, "RPC: {params}"),
837        }
838    }
839}
840
841impl From<dto::TracingParams> for TracingParams {
842    fn from(value: dto::TracingParams) -> Self {
843        match value {
844            dto::TracingParams::RPCTracer(tracer_params) => {
845                TracingParams::RPCTracer(tracer_params.into())
846            }
847        }
848    }
849}
850
851#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Eq, Hash, DeepSizeOf)]
852pub enum StorageOverride {
853    Diff(BTreeMap<StoreKey, StoreVal>),
854    Replace(BTreeMap<StoreKey, StoreVal>),
855}
856
857impl From<dto::StorageOverride> for StorageOverride {
858    fn from(value: dto::StorageOverride) -> Self {
859        match value {
860            dto::StorageOverride::Diff(diff) => StorageOverride::Diff(diff),
861            dto::StorageOverride::Replace(replace) => StorageOverride::Replace(replace),
862        }
863    }
864}
865
866#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Eq, Hash, DeepSizeOf)]
867pub struct AccountOverrides {
868    pub slots: Option<StorageOverride>,
869    pub native_balance: Option<Balance>,
870    pub code: Option<Code>,
871}
872
873impl From<dto::AccountOverrides> for AccountOverrides {
874    fn from(value: dto::AccountOverrides) -> Self {
875        Self {
876            slots: value.slots.map(|s| s.into()),
877            native_balance: value.native_balance,
878            code: value.code,
879        }
880    }
881}
882
883#[derive(Debug, Clone, PartialEq, Deserialize, Eq, Hash, DeepSizeOf)]
884pub struct RPCTracerParams {
885    /// The caller address of the transaction, if not provided tracing will use the default value
886    /// for an address defined by the VM.
887    pub caller: Option<Address>,
888    /// The call data used for the tracing call, this needs to include the function selector
889    pub calldata: Bytes,
890    /// Optionally allow for state overrides so that the call works as expected
891    pub state_overrides: Option<BTreeMap<Address, AccountOverrides>>,
892    /// Addresses to prune from trace results. Useful for hooks that use mock
893    /// accounts/routers that shouldn't be tracked in the final DCI results.
894    pub prune_addresses: Option<Vec<Address>>,
895}
896
897impl From<dto::RPCTracerParams> for RPCTracerParams {
898    fn from(value: dto::RPCTracerParams) -> Self {
899        Self {
900            caller: value.caller,
901            calldata: value.calldata,
902            state_overrides: value.state_overrides.map(|overrides| {
903                overrides
904                    .into_iter()
905                    .map(|(address, account_overrides)| (address, account_overrides.into()))
906                    .collect()
907            }),
908            prune_addresses: value.prune_addresses,
909        }
910    }
911}
912
913impl RPCTracerParams {
914    pub fn new(caller: Option<Address>, calldata: Bytes) -> Self {
915        Self { caller, calldata, state_overrides: None, prune_addresses: None }
916    }
917
918    pub fn with_state_overrides(mut self, state: BTreeMap<Address, AccountOverrides>) -> Self {
919        self.state_overrides = Some(state);
920        self
921    }
922
923    pub fn with_prune_addresses(mut self, addresses: Vec<Address>) -> Self {
924        self.prune_addresses = Some(addresses);
925        self
926    }
927}
928
929impl std::fmt::Display for RPCTracerParams {
930    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
931        let caller_str = match &self.caller {
932            Some(addr) => format!("caller={addr}"),
933            None => String::new(),
934        };
935
936        let calldata_str = if self.calldata.len() >= 8 {
937            format!(
938                "calldata=0x{}..({} bytes)",
939                hex::encode(&self.calldata[..8]),
940                self.calldata.len()
941            )
942        } else {
943            format!("calldata={}", self.calldata)
944        };
945
946        let overrides_str = match &self.state_overrides {
947            Some(overrides) if !overrides.is_empty() => {
948                format!(", {} state override(s)", overrides.len())
949            }
950            _ => String::new(),
951        };
952
953        write!(f, "{caller_str}, {calldata_str}{overrides_str}")
954    }
955}
956
957// Ensure serialization order, required by the storage layer
958impl Serialize for RPCTracerParams {
959    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
960    where
961        S: Serializer,
962    {
963        // Count fields: always serialize caller and calldata, plus optional fields
964        let mut field_count = 2;
965        if self.state_overrides.is_some() {
966            field_count += 1;
967        }
968        if self.prune_addresses.is_some() {
969            field_count += 1;
970        }
971
972        let mut state = serializer.serialize_struct("RPCTracerEntryPoint", field_count)?;
973        state.serialize_field("caller", &self.caller)?;
974        state.serialize_field("calldata", &self.calldata)?;
975
976        // Only serialize optional fields if they are present
977        if let Some(ref overrides) = self.state_overrides {
978            state.serialize_field("state_overrides", overrides)?;
979        }
980        if let Some(ref prune_addrs) = self.prune_addresses {
981            state.serialize_field("prune_addresses", prune_addrs)?;
982        }
983
984        state.end()
985    }
986}
987
988#[derive(
989    Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, DeepSizeOf,
990)]
991pub struct AddressStorageLocation {
992    pub key: StoreKey,
993    pub offset: u8,
994}
995
996impl AddressStorageLocation {
997    pub fn new(key: StoreKey, offset: u8) -> Self {
998        Self { key, offset }
999    }
1000}
1001
1002#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, DeepSizeOf)]
1003pub struct TracingResult {
1004    /// A set of (address, storage slot) pairs representing state that contain a called address.
1005    /// If any of these storage slots change, the execution path might change.
1006    pub retriggers: HashSet<(Address, AddressStorageLocation)>,
1007    /// A map of all addresses that were called during the trace with a list of storage slots that
1008    /// were accessed.
1009    pub accessed_slots: HashMap<Address, HashSet<StoreKey>>,
1010}
1011
1012impl TracingResult {
1013    pub fn new(
1014        retriggers: HashSet<(Address, AddressStorageLocation)>,
1015        accessed_slots: HashMap<Address, HashSet<StoreKey>>,
1016    ) -> Self {
1017        Self { retriggers, accessed_slots }
1018    }
1019
1020    /// Merges this tracing result with another one.
1021    ///
1022    /// The method combines two [`TracingResult`] instances.
1023    pub fn merge(&mut self, other: TracingResult) {
1024        self.retriggers.extend(other.retriggers);
1025        for (address, slots) in other.accessed_slots {
1026            self.accessed_slots
1027                .entry(address)
1028                .or_default()
1029                .extend(slots);
1030        }
1031    }
1032}
1033
1034#[derive(Debug, Clone, PartialEq, DeepSizeOf)]
1035/// Represents a traced entry point and the results of the tracing operation.
1036pub struct TracedEntryPoint {
1037    /// The combined entry point and tracing params that was traced
1038    pub entry_point_with_params: EntryPointWithTracingParams,
1039    /// The block hash of the block that the entry point was traced on.
1040    pub detection_block_hash: BlockHash,
1041    /// The results of the tracing operation
1042    pub tracing_result: TracingResult,
1043}
1044
1045impl TracedEntryPoint {
1046    pub fn new(
1047        entry_point_with_params: EntryPointWithTracingParams,
1048        detection_block_hash: BlockHash,
1049        result: TracingResult,
1050    ) -> Self {
1051        Self { entry_point_with_params, detection_block_hash, tracing_result: result }
1052    }
1053
1054    pub fn entry_point_id(&self) -> String {
1055        self.entry_point_with_params
1056            .entry_point
1057            .external_id
1058            .clone()
1059    }
1060}
1061
1062impl std::fmt::Display for TracedEntryPoint {
1063    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1064        write!(
1065            f,
1066            "[{}: {} retriggers, {} accessed addresses]",
1067            self.entry_point_id(),
1068            self.tracing_result.retriggers.len(),
1069            self.tracing_result.accessed_slots.len()
1070        )
1071    }
1072}
1073
1074/// Storage changes grouped by transaction.
1075#[derive(Debug, PartialEq, Default, Clone, DeepSizeOf)]
1076pub struct TxWithContractChanges {
1077    pub tx: Transaction,
1078    pub contract_changes: AccountToContractChanges,
1079}
1080
1081#[derive(Debug, PartialEq, Default, Clone, DeepSizeOf)]
1082pub struct BlockChanges {
1083    pub extractor: String,
1084    pub chain: Chain,
1085    pub block: Block,
1086    pub finalized_block_height: u64,
1087    pub revert: bool,
1088    pub new_tokens: HashMap<Address, Token>,
1089    /// Vec of updates at this block, aggregated by tx and sorted by tx index in ascending order
1090    pub txs_with_update: Vec<TxWithChanges>,
1091    // Raw block contract changes. This is intended as DCI input and is to be omitted from the
1092    // reorg buffer and aggregation into the `BlockAggregatedChanges` object.
1093    pub block_contract_changes: Vec<TxWithContractChanges>,
1094    /// Required here so that it is part of the reorg buffer and thus inserted into storage once
1095    /// finalized.
1096    /// Populated by the `DynamicContractIndexer`
1097    pub trace_results: Vec<TracedEntryPoint>,
1098    /// The index of the partial block. None if it's a full block.
1099    pub partial_block_index: Option<u32>,
1100}
1101
1102impl BlockChanges {
1103    pub fn new(
1104        extractor: String,
1105        chain: Chain,
1106        block: Block,
1107        finalized_block_height: u64,
1108        revert: bool,
1109        txs_with_update: Vec<TxWithChanges>,
1110        block_contract_changes: Vec<TxWithContractChanges>,
1111    ) -> Self {
1112        BlockChanges {
1113            extractor,
1114            chain,
1115            block,
1116            finalized_block_height,
1117            revert,
1118            new_tokens: HashMap::new(),
1119            txs_with_update,
1120            block_contract_changes,
1121            trace_results: Vec::new(),
1122            partial_block_index: None,
1123        }
1124    }
1125
1126    /// Aggregates component and account updates.
1127    ///
1128    /// This function aggregates all protocol updates into a [`BlockAggregatedChanges`] object. This
1129    /// new object should have all individual changes merged into only one final/compacted change
1130    /// per component and account. This means there is only one state delta and component balance
1131    /// per component, and one account delta and account balance per account. DCI trace results are
1132    /// also aggregated into a result per entry point.
1133    ///
1134    /// Note - all non-protocol specific data in the BlockChanges object are lost during
1135    /// aggregation. This means block_storage_changes is dropped.
1136    ///
1137    /// # Errors
1138    ///
1139    /// This returns a `MergeError` if there was a problem during merge.
1140    pub fn into_aggregated(
1141        self,
1142        db_committed_block_height: Option<u64>,
1143    ) -> Result<BlockAggregatedChanges, MergeError> {
1144        if db_committed_block_height.is_some_and(|h| h > self.finalized_block_height) {
1145            return Err(MergeError::InvalidState(format!(
1146                "Database committed block height {:?} is greater than finalized_block_height {}",
1147                db_committed_block_height, self.finalized_block_height
1148            )));
1149        }
1150
1151        let mut iter = self.txs_with_update.into_iter();
1152
1153        let first_state = iter.next().unwrap_or_default();
1154
1155        let aggregated_changes = iter.try_fold(first_state, |mut acc_state, new_state| {
1156            acc_state.merge(new_state.clone())?;
1157            Ok::<_, MergeError>(acc_state.clone())
1158        })?;
1159
1160        // Aggregate trace_results
1161        let mut aggregated_trace_results = HashMap::new();
1162        for result in self.trace_results {
1163            let external_id = result.entry_point_id();
1164            aggregated_trace_results
1165                .entry(external_id)
1166                .and_modify(|existing: &mut TracingResult| {
1167                    existing.merge(result.tracing_result.clone())
1168                })
1169                .or_insert(result.tracing_result);
1170        }
1171
1172        Ok(BlockAggregatedChanges {
1173            extractor: self.extractor,
1174            chain: self.chain,
1175            block: self.block,
1176            db_committed_block_height,
1177            finalized_block_height: self.finalized_block_height,
1178            revert: self.revert,
1179            new_protocol_components: aggregated_changes.protocol_components,
1180            new_tokens: self.new_tokens,
1181            deleted_protocol_components: HashMap::new(),
1182            state_deltas: aggregated_changes.state_updates,
1183            account_deltas: aggregated_changes.account_deltas,
1184            component_balances: aggregated_changes.balance_changes,
1185            account_balances: aggregated_changes.account_balance_changes,
1186            component_tvl: HashMap::new(),
1187            dci_update: DCIUpdate {
1188                new_entrypoints: aggregated_changes.entrypoints,
1189                new_entrypoint_params: aggregated_changes.entrypoint_params,
1190                trace_results: aggregated_trace_results,
1191            },
1192            partial_block_index: self.partial_block_index,
1193        })
1194    }
1195
1196    pub fn protocol_components(&self) -> Vec<ProtocolComponent> {
1197        self.txs_with_update
1198            .iter()
1199            .flat_map(|tx_u| {
1200                tx_u.protocol_components
1201                    .values()
1202                    .cloned()
1203            })
1204            .collect()
1205    }
1206
1207    /// Returns true if the block is a partial block.
1208    pub fn is_partial_block(&self) -> bool {
1209        self.partial_block_index.is_some()
1210    }
1211
1212    /// Sets the partial block index.
1213    pub fn set_partial_block_index(&mut self, index: Option<u32>) {
1214        self.partial_block_index = index;
1215    }
1216
1217    /// Sets every transaction's `block_hash` in `txs_with_update` and `block_contract_changes`
1218    /// to this block's hash. Used after merging partials so all txs refer to the same block
1219    /// (e.g. the final block hash).
1220    pub fn normalize_block_hash(&mut self) {
1221        let h = self.block.hash.clone();
1222        for tx_with_changes in self.txs_with_update.iter_mut() {
1223            tx_with_changes.tx.block_hash = h.clone();
1224        }
1225        for tx_with_contract in self.block_contract_changes.iter_mut() {
1226            tx_with_contract.tx.block_hash = h.clone();
1227        }
1228    }
1229
1230    /// Merges another partial block into this one, preserving later changes.
1231    ///
1232    /// The partial block with the higher index represents later changes and takes precedence.
1233    /// Merges `new_tokens`, `txs_with_update` (sorted by index), `block_contract_changes`,
1234    /// and `trace_results`. When both blocks have the same token address, the token from the
1235    /// block with the higher partial index is kept.
1236    ///
1237    /// Works regardless of merge order: `partial_0.merge_partial(partial_1)` and
1238    /// `partial_1.merge_partial(partial_0)` produce equivalent results.
1239    ///
1240    /// # Errors
1241    /// - Non-partial block: Either block is not marked as partial
1242    /// - Extractor mismatch: Blocks from different extractors
1243    /// - Chain mismatch: Blocks from different chains
1244    /// - Block mismatch: Different block numbers (hash may differ for temp vs final partial)
1245    /// - Revert mismatch: Different revert status
1246    pub fn merge_partial(self, other: Self) -> Result<Self, MergeError> {
1247        let Some(self_index) = self.partial_block_index else {
1248            return Err(MergeError::InvalidState("self is not a partial block".to_string()));
1249        };
1250
1251        let Some(other_index) = other.partial_block_index else {
1252            return Err(MergeError::InvalidState("other is not a partial block".to_string()));
1253        };
1254
1255        if self.extractor != other.extractor {
1256            return Err(MergeError::IdMismatch(
1257                "partial blocks (extractor)".to_string(),
1258                self.extractor.clone(),
1259                other.extractor.clone(),
1260            ));
1261        }
1262
1263        if self.chain != other.chain {
1264            return Err(MergeError::IdMismatch(
1265                "partial blocks (chain)".to_string(),
1266                format!("{:?}", self.chain),
1267                format!("{:?}", other.chain),
1268            ));
1269        }
1270
1271        if self.block.number != other.block.number {
1272            return Err(MergeError::BlockMismatch(
1273                "partial blocks".to_string(),
1274                self.block.hash.clone(),
1275                other.block.hash.clone(),
1276            ));
1277        }
1278
1279        if self.revert != other.revert {
1280            return Err(MergeError::InvalidState(format!(
1281                "different revert status: {} vs {}",
1282                self.revert, other.revert
1283            )));
1284        }
1285
1286        let (mut current, previous) = if self_index > other_index {
1287            (self, other)
1288        } else if self_index < other_index {
1289            (other, self)
1290        } else {
1291            return Err(MergeError::InvalidState(format!("same partial block index: {self_index}")));
1292        };
1293
1294        for (addr, token) in previous.new_tokens {
1295            current
1296                .new_tokens
1297                .entry(addr)
1298                .or_insert(token);
1299        }
1300
1301        current
1302            .txs_with_update
1303            .extend(previous.txs_with_update);
1304        current
1305            .txs_with_update
1306            .sort_by_key(|tx| tx.tx.index);
1307
1308        current
1309            .block_contract_changes
1310            .extend(previous.block_contract_changes);
1311
1312        current.normalize_block_hash();
1313
1314        current
1315            .trace_results
1316            .extend(previous.trace_results);
1317
1318        Ok(current)
1319    }
1320}
1321
1322impl BlockScoped for BlockChanges {
1323    fn block(&self) -> Block {
1324        self.block.clone()
1325    }
1326}
1327
1328#[cfg(test)]
1329pub mod fixtures {
1330    use std::str::FromStr;
1331
1332    use rstest::rstest;
1333
1334    use super::*;
1335    use crate::models::ChangeType;
1336
1337    // PERF: duplicated in crate::extractor::models::fixtures — consider a `test-utils`
1338    // feature flag to share test fixtures cross-crate.
1339    pub fn create_transaction(hash: &str, block: &str, index: u64) -> Transaction {
1340        Transaction::new(
1341            hash.parse().unwrap(),
1342            block.parse().unwrap(),
1343            Bytes::zero(20),
1344            Some(Bytes::zero(20)),
1345            index,
1346        )
1347    }
1348
1349    /// Returns a pre-built `TxWithChanges` for testing.
1350    ///
1351    /// Both indices share the same keys (component `"pool_0"`, token `0xaa..`, contract `0xbb..`)
1352    /// but with different values, so "later wins" precedence can be verified across all fields:
1353    ///
1354    /// - Index 0: tx_index=1, component_balance {token=800, token2=300}, account_balance
1355    ///   {token=500, token2=150}, slots {1=>100, 2=>200}, state {"reserve"=>1000, "fee"=>50}, 1
1356    ///   entrypoint, ChangeType::Creation
1357    /// - Index 1: tx_index=2, component_balance {token=1000}, account_balance {token=700}, slots
1358    ///   {1=>300} (overlaps slot 1), state {"reserve"=>2000} (overlaps), 2 entrypoints (superset),
1359    ///   ChangeType::Update
1360    // PERF: duplicated in crate::extractor::models::fixtures — consider a `test-utils`
1361    // feature flag to share test fixtures cross-crate.
1362    pub fn tx_with_changes(index: u8) -> TxWithChanges {
1363        let token = Bytes::from(vec![0xaa; 20]);
1364        let token2 = Bytes::from(vec![0xcc; 20]);
1365        let contract = Bytes::from(vec![0xbb; 20]);
1366        let c_id = "pool_0".to_string();
1367
1368        match index {
1369            0 => {
1370                let tx = create_transaction("0x01", "0x00", 1);
1371                TxWithChanges {
1372                    tx: tx.clone(),
1373                    protocol_components: HashMap::from([(
1374                        c_id.clone(),
1375                        ProtocolComponent { id: c_id.clone(), ..Default::default() },
1376                    )]),
1377                    account_deltas: HashMap::from([(
1378                        contract.clone(),
1379                        AccountDelta::new(
1380                            Chain::Ethereum,
1381                            contract.clone(),
1382                            HashMap::from([
1383                                (
1384                                    Bytes::from(1u64).lpad(32, 0),
1385                                    Some(Bytes::from(100u64).lpad(32, 0)),
1386                                ),
1387                                (
1388                                    Bytes::from(2u64).lpad(32, 0),
1389                                    Some(Bytes::from(200u64).lpad(32, 0)),
1390                                ),
1391                            ]),
1392                            None,
1393                            Some(Bytes::from(vec![0; 4])),
1394                            ChangeType::Creation,
1395                        ),
1396                    )]),
1397                    state_updates: HashMap::from([(
1398                        c_id.clone(),
1399                        ProtocolComponentStateDelta::new(
1400                            &c_id,
1401                            HashMap::from([
1402                                ("reserve".into(), Bytes::from(1000u64).lpad(32, 0)),
1403                                ("fee".into(), Bytes::from(50u64).lpad(32, 0)),
1404                            ]),
1405                            HashSet::new(),
1406                        ),
1407                    )]),
1408                    balance_changes: HashMap::from([(
1409                        c_id.clone(),
1410                        HashMap::from([
1411                            (
1412                                token.clone(),
1413                                ComponentBalance {
1414                                    token: token.clone(),
1415                                    balance: Bytes::from(800_u64).lpad(32, 0),
1416                                    balance_float: 800.0,
1417                                    component_id: c_id.clone(),
1418                                    modify_tx: tx.hash.clone(),
1419                                },
1420                            ),
1421                            (
1422                                token2.clone(),
1423                                ComponentBalance {
1424                                    token: token2.clone(),
1425                                    balance: Bytes::from(300_u64).lpad(32, 0),
1426                                    balance_float: 300.0,
1427                                    component_id: c_id.clone(),
1428                                    modify_tx: tx.hash.clone(),
1429                                },
1430                            ),
1431                        ]),
1432                    )]),
1433                    account_balance_changes: HashMap::from([(
1434                        contract.clone(),
1435                        HashMap::from([
1436                            (
1437                                token.clone(),
1438                                AccountBalance {
1439                                    token: token.clone(),
1440                                    balance: Bytes::from(500_u64).lpad(32, 0),
1441                                    modify_tx: tx.hash.clone(),
1442                                    account: contract.clone(),
1443                                },
1444                            ),
1445                            (
1446                                token2,
1447                                AccountBalance {
1448                                    token: Bytes::from(vec![0xcc; 20]),
1449                                    balance: Bytes::from(150_u64).lpad(32, 0),
1450                                    modify_tx: tx.hash,
1451                                    account: contract,
1452                                },
1453                            ),
1454                        ]),
1455                    )]),
1456                    entrypoints: HashMap::from([(
1457                        c_id.clone(),
1458                        HashSet::from([EntryPoint::new(
1459                            "ep_0".into(),
1460                            Bytes::zero(20),
1461                            "fn_a()".into(),
1462                        )]),
1463                    )]),
1464                    entrypoint_params: HashMap::from([(
1465                        "ep_0".into(),
1466                        HashSet::from([(
1467                            TracingParams::RPCTracer(RPCTracerParams::new(
1468                                None,
1469                                Bytes::from(vec![1]),
1470                            )),
1471                            c_id,
1472                        )]),
1473                    )]),
1474                }
1475            }
1476            1 => {
1477                let tx = create_transaction("0x02", "0x00", 2);
1478                TxWithChanges {
1479                    tx: tx.clone(),
1480                    protocol_components: HashMap::from([(
1481                        c_id.clone(),
1482                        ProtocolComponent { id: c_id.clone(), ..Default::default() },
1483                    )]),
1484                    account_deltas: HashMap::from([(
1485                        contract.clone(),
1486                        AccountDelta::new(
1487                            Chain::Ethereum,
1488                            contract.clone(),
1489                            HashMap::from([(
1490                                Bytes::from(1u64).lpad(32, 0),
1491                                Some(Bytes::from(300u64).lpad(32, 0)),
1492                            )]),
1493                            None,
1494                            None,
1495                            ChangeType::Update,
1496                        ),
1497                    )]),
1498                    state_updates: HashMap::from([(
1499                        c_id.clone(),
1500                        ProtocolComponentStateDelta::new(
1501                            &c_id,
1502                            HashMap::from([("reserve".into(), Bytes::from(2000u64).lpad(32, 0))]),
1503                            HashSet::new(),
1504                        ),
1505                    )]),
1506                    balance_changes: HashMap::from([(
1507                        c_id.clone(),
1508                        HashMap::from([(
1509                            token.clone(),
1510                            ComponentBalance {
1511                                token: token.clone(),
1512                                balance: Bytes::from(1000_u64).lpad(32, 0),
1513                                balance_float: 1000.0,
1514                                component_id: c_id.clone(),
1515                                modify_tx: tx.hash.clone(),
1516                            },
1517                        )]),
1518                    )]),
1519                    account_balance_changes: HashMap::from([(
1520                        contract.clone(),
1521                        HashMap::from([(
1522                            token.clone(),
1523                            AccountBalance {
1524                                token: token.clone(),
1525                                balance: Bytes::from(700_u64).lpad(32, 0),
1526                                modify_tx: tx.hash,
1527                                account: contract,
1528                            },
1529                        )]),
1530                    )]),
1531                    entrypoints: HashMap::from([(
1532                        c_id.clone(),
1533                        HashSet::from([
1534                            EntryPoint::new("ep_0".into(), Bytes::zero(20), "fn_a()".into()),
1535                            EntryPoint::new("ep_1".into(), Bytes::zero(20), "fn_b()".into()),
1536                        ]),
1537                    )]),
1538                    entrypoint_params: HashMap::from([(
1539                        "ep_1".into(),
1540                        HashSet::from([(
1541                            TracingParams::RPCTracer(RPCTracerParams::new(
1542                                None,
1543                                Bytes::from(vec![2]),
1544                            )),
1545                            c_id,
1546                        )]),
1547                    )]),
1548                }
1549            }
1550            _ => panic!("tx_with_changes: index must be 0 or 1, got {index}"),
1551        }
1552    }
1553
1554    #[test]
1555    fn test_merge_tx_with_changes() {
1556        let mut changes1 = tx_with_changes(0);
1557        let changes2 = tx_with_changes(1);
1558
1559        let token = Bytes::from(vec![0xaa; 20]);
1560        let contract = Bytes::from(vec![0xbb; 20]);
1561        let c_id = "pool_0".to_string();
1562
1563        assert!(changes1.merge(changes2).is_ok());
1564
1565        // After merge, balances should reflect changes2 ("later wins")
1566        assert_eq!(
1567            changes1.balance_changes[&c_id][&token].balance,
1568            Bytes::from(1000_u64).lpad(32, 0),
1569        );
1570        assert_eq!(
1571            changes1.account_balance_changes[&contract][&token].balance,
1572            Bytes::from(700_u64).lpad(32, 0),
1573        );
1574        // tx should be updated to changes2's tx
1575        assert_eq!(changes1.tx.hash, Bytes::from(vec![2]));
1576        // Entrypoints should be merged (union of both)
1577        assert_eq!(changes1.entrypoints[&c_id].len(), 2);
1578        let mut sigs: Vec<_> = changes1.entrypoints[&c_id]
1579            .iter()
1580            .map(|ep| ep.signature.clone())
1581            .collect();
1582        sigs.sort();
1583        assert_eq!(sigs, vec!["fn_a()", "fn_b()"]);
1584    }
1585
1586    #[rstest]
1587    #[case::mismatched_blocks(
1588        fixtures::create_transaction("0x01", "0x0abc", 1),
1589        fixtures::create_transaction("0x02", "0x0def", 2)
1590    )]
1591    #[case::older_transaction(
1592        fixtures::create_transaction("0x02", "0x0abc", 2),
1593        fixtures::create_transaction("0x01", "0x0abc", 1)
1594    )]
1595    fn test_merge_errors(#[case] tx1: Transaction, #[case] tx2: Transaction) {
1596        let mut changes1 = TxWithChanges { tx: tx1, ..Default::default() };
1597
1598        let changes2 = TxWithChanges { tx: tx2, ..Default::default() };
1599
1600        assert!(changes1.merge(changes2).is_err());
1601    }
1602
1603    #[test]
1604    fn test_rpc_tracer_entry_point_serialization_order() {
1605        use std::str::FromStr;
1606
1607        use serde_json;
1608
1609        let entry_point = RPCTracerParams::new(
1610            Some(Address::from_str("0x1234567890123456789012345678901234567890").unwrap()),
1611            Bytes::from_str("0xabcdef").unwrap(),
1612        );
1613
1614        let serialized = serde_json::to_string(&entry_point).unwrap();
1615
1616        // Verify that "caller" comes before "calldata" in the serialized output
1617        assert!(serialized.find("\"caller\"").unwrap() < serialized.find("\"calldata\"").unwrap());
1618
1619        // Verify we can deserialize it back
1620        let deserialized: RPCTracerParams = serde_json::from_str(&serialized).unwrap();
1621        assert_eq!(entry_point, deserialized);
1622    }
1623
1624    #[test]
1625    fn test_tracing_result_merge() {
1626        let address1 = Address::from_str("0x1234567890123456789012345678901234567890").unwrap();
1627        let address2 = Address::from_str("0x2345678901234567890123456789012345678901").unwrap();
1628        let address3 = Address::from_str("0x3456789012345678901234567890123456789012").unwrap();
1629
1630        let store_key1 = StoreKey::from(vec![1, 2, 3, 4]);
1631        let store_key2 = StoreKey::from(vec![5, 6, 7, 8]);
1632
1633        let mut result1 = TracingResult::new(
1634            HashSet::from([(
1635                address1.clone(),
1636                AddressStorageLocation::new(store_key1.clone(), 12),
1637            )]),
1638            HashMap::from([
1639                (address2.clone(), HashSet::from([store_key1.clone()])),
1640                (address3.clone(), HashSet::from([store_key2.clone()])),
1641            ]),
1642        );
1643
1644        let result2 = TracingResult::new(
1645            HashSet::from([(
1646                address3.clone(),
1647                AddressStorageLocation::new(store_key2.clone(), 12),
1648            )]),
1649            HashMap::from([
1650                (address1.clone(), HashSet::from([store_key1.clone()])),
1651                (address2.clone(), HashSet::from([store_key2.clone()])),
1652            ]),
1653        );
1654
1655        result1.merge(result2);
1656
1657        // Verify retriggers were merged
1658        assert_eq!(result1.retriggers.len(), 2);
1659        assert!(result1
1660            .retriggers
1661            .contains(&(address1.clone(), AddressStorageLocation::new(store_key1.clone(), 12))));
1662        assert!(result1
1663            .retriggers
1664            .contains(&(address3.clone(), AddressStorageLocation::new(store_key2.clone(), 12))));
1665
1666        // Verify accessed slots were merged
1667        assert_eq!(result1.accessed_slots.len(), 3);
1668        assert!(result1
1669            .accessed_slots
1670            .contains_key(&address1));
1671        assert!(result1
1672            .accessed_slots
1673            .contains_key(&address2));
1674        assert!(result1
1675            .accessed_slots
1676            .contains_key(&address3));
1677
1678        assert_eq!(
1679            result1
1680                .accessed_slots
1681                .get(&address2)
1682                .unwrap(),
1683            &HashSet::from([store_key1.clone(), store_key2.clone()])
1684        );
1685    }
1686
1687    #[test]
1688    fn test_entry_point_with_tracing_params_display() {
1689        use std::str::FromStr;
1690
1691        let entry_point = EntryPoint::new(
1692            "uniswap_v3_pool_swap".to_string(),
1693            Address::from_str("0x1234567890123456789012345678901234567890").unwrap(),
1694            "swapExactETHForTokens(uint256,address[],address,uint256)".to_string(),
1695        );
1696
1697        let tracing_params = TracingParams::RPCTracer(RPCTracerParams::new(
1698            Some(Address::from_str("0x9876543210987654321098765432109876543210").unwrap()),
1699            Bytes::from_str("0xabcdef").unwrap(),
1700        ));
1701
1702        let entry_point_with_params = EntryPointWithTracingParams::new(entry_point, tracing_params);
1703
1704        let display_output = entry_point_with_params.to_string();
1705        assert_eq!(display_output, "uniswap_v3_pool_swap [RPC]");
1706    }
1707
1708    #[test]
1709    fn test_traced_entry_point_display() {
1710        use std::str::FromStr;
1711
1712        let entry_point = EntryPoint::new(
1713            "uniswap_v3_pool_swap".to_string(),
1714            Address::from_str("0x1234567890123456789012345678901234567890").unwrap(),
1715            "swapExactETHForTokens(uint256,address[],address,uint256)".to_string(),
1716        );
1717
1718        let tracing_params = TracingParams::RPCTracer(RPCTracerParams::new(
1719            Some(Address::from_str("0x9876543210987654321098765432109876543210").unwrap()),
1720            Bytes::from_str("0xabcdef").unwrap(),
1721        ));
1722
1723        let entry_point_with_params = EntryPointWithTracingParams::new(entry_point, tracing_params);
1724
1725        // Create tracing result with 2 retriggers and 3 accessed addresses
1726        let address1 = Address::from_str("0x1111111111111111111111111111111111111111").unwrap();
1727        let address2 = Address::from_str("0x2222222222222222222222222222222222222222").unwrap();
1728        let address3 = Address::from_str("0x3333333333333333333333333333333333333333").unwrap();
1729
1730        let store_key1 = StoreKey::from(vec![1, 2, 3, 4]);
1731        let store_key2 = StoreKey::from(vec![5, 6, 7, 8]);
1732
1733        let tracing_result = TracingResult::new(
1734            HashSet::from([
1735                (address1.clone(), AddressStorageLocation::new(store_key1.clone(), 0)),
1736                (address2.clone(), AddressStorageLocation::new(store_key2.clone(), 12)),
1737            ]),
1738            HashMap::from([
1739                (address1.clone(), HashSet::from([store_key1.clone()])),
1740                (address2.clone(), HashSet::from([store_key2.clone()])),
1741                (address3.clone(), HashSet::from([store_key1.clone()])),
1742            ]),
1743        );
1744
1745        let traced_entry_point = TracedEntryPoint::new(
1746            entry_point_with_params,
1747            Bytes::from_str("0xabcdef1234567890").unwrap(),
1748            tracing_result,
1749        );
1750
1751        let display_output = traced_entry_point.to_string();
1752        assert_eq!(display_output, "[uniswap_v3_pool_swap: 2 retriggers, 3 accessed addresses]");
1753    }
1754}