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