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