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, AccountChangesWithTx, AccountDelta},
12        protocol::{
13            ComponentBalance, ProtocolChangesWithTx, ProtocolComponent, ProtocolComponentStateDelta,
14        },
15        token::Token,
16        Address, Balance, BlockHash, Chain, Code, ComponentId, EntryPointId, MergeError, StoreKey,
17        StoreVal,
18    },
19    Bytes,
20};
21
22#[derive(Clone, Default, PartialEq, Serialize, Deserialize, Debug)]
23pub struct Block {
24    pub number: u64,
25    pub chain: Chain,
26    pub hash: Bytes,
27    pub parent_hash: Bytes,
28    pub ts: NaiveDateTime,
29}
30
31impl Block {
32    pub fn new(
33        number: u64,
34        chain: Chain,
35        hash: Bytes,
36        parent_hash: Bytes,
37        ts: NaiveDateTime,
38    ) -> Self {
39        Block { hash, parent_hash, number, chain, ts }
40    }
41}
42
43// Manual impl as `NaiveDateTime` structure referenced in `ts` does not implement DeepSizeOf
44impl DeepSizeOf for Block {
45    fn deep_size_of_children(&self, context: &mut deepsize::Context) -> usize {
46        size_of::<u64>() +
47            self.chain
48                .deep_size_of_children(context) +
49            self.hash.deep_size_of_children(context) +
50            self.parent_hash
51                .deep_size_of_children(context)
52    }
53}
54
55#[derive(Clone, Default, PartialEq, Debug, Eq, Hash, DeepSizeOf)]
56pub struct Transaction {
57    pub hash: Bytes,
58    pub block_hash: Bytes,
59    pub from: Bytes,
60    pub to: Option<Bytes>,
61    pub index: u64,
62}
63
64impl Transaction {
65    pub fn new(hash: Bytes, block_hash: Bytes, from: Bytes, to: Option<Bytes>, index: u64) -> Self {
66        Transaction { hash, block_hash, from, to, index }
67    }
68}
69
70pub struct BlockTransactionDeltas<T> {
71    pub extractor: String,
72    pub chain: Chain,
73    pub block: Block,
74    pub revert: bool,
75    pub deltas: Vec<TransactionDeltaGroup<T>>,
76}
77
78#[allow(dead_code)]
79pub struct TransactionDeltaGroup<T> {
80    changes: T,
81    protocol_component: HashMap<String, ProtocolComponent>,
82    component_balances: HashMap<String, ComponentBalance>,
83    component_tvl: HashMap<String, f64>,
84    tx: Transaction,
85}
86
87#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, DeepSizeOf)]
88pub struct BlockAggregatedChanges {
89    pub extractor: String,
90    pub chain: Chain,
91    pub block: Block,
92    pub finalized_block_height: u64,
93    pub db_committed_block_height: Option<u64>,
94    pub revert: bool,
95    pub state_deltas: HashMap<String, ProtocolComponentStateDelta>,
96    pub account_deltas: HashMap<Bytes, AccountDelta>,
97    pub new_tokens: HashMap<Address, Token>,
98    pub new_protocol_components: HashMap<String, ProtocolComponent>,
99    pub deleted_protocol_components: HashMap<String, ProtocolComponent>,
100    pub component_balances: HashMap<ComponentId, HashMap<Bytes, ComponentBalance>>,
101    pub account_balances: HashMap<Address, HashMap<Address, AccountBalance>>,
102    pub component_tvl: HashMap<String, f64>,
103    pub dci_update: DCIUpdate,
104}
105
106impl BlockAggregatedChanges {
107    #[allow(clippy::too_many_arguments)]
108    pub fn new(
109        extractor: &str,
110        chain: Chain,
111        block: Block,
112        db_committed_block_height: Option<u64>,
113        finalized_block_height: u64,
114        revert: bool,
115        state_deltas: HashMap<String, ProtocolComponentStateDelta>,
116        account_deltas: HashMap<Bytes, AccountDelta>,
117        new_tokens: HashMap<Address, Token>,
118        new_components: HashMap<String, ProtocolComponent>,
119        deleted_components: HashMap<String, ProtocolComponent>,
120        component_balances: HashMap<ComponentId, HashMap<Bytes, ComponentBalance>>,
121        account_balances: HashMap<Address, HashMap<Address, AccountBalance>>,
122        component_tvl: HashMap<String, f64>,
123        dci_update: DCIUpdate,
124    ) -> Self {
125        Self {
126            extractor: extractor.to_string(),
127            chain,
128            block,
129            db_committed_block_height,
130            finalized_block_height,
131            revert,
132            state_deltas,
133            account_deltas,
134            new_tokens,
135            new_protocol_components: new_components,
136            deleted_protocol_components: deleted_components,
137            component_balances,
138            account_balances,
139            component_tvl,
140            dci_update,
141        }
142    }
143}
144
145impl std::fmt::Display for BlockAggregatedChanges {
146    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147        write!(f, "block_number: {}, extractor: {}", self.block.number, self.extractor)
148    }
149}
150
151impl BlockAggregatedChanges {
152    pub fn drop_state(&self) -> Self {
153        Self {
154            extractor: self.extractor.clone(),
155            chain: self.chain,
156            block: self.block.clone(),
157            db_committed_block_height: self.db_committed_block_height,
158            finalized_block_height: self.finalized_block_height,
159            revert: self.revert,
160            account_deltas: HashMap::new(),
161            state_deltas: HashMap::new(),
162            new_tokens: self.new_tokens.clone(),
163            new_protocol_components: self.new_protocol_components.clone(),
164            deleted_protocol_components: self.deleted_protocol_components.clone(),
165            component_balances: self.component_balances.clone(),
166            account_balances: self.account_balances.clone(),
167            component_tvl: self.component_tvl.clone(),
168            dci_update: self.dci_update.clone(),
169        }
170    }
171}
172
173pub trait BlockScoped {
174    fn block(&self) -> Block;
175}
176
177impl BlockScoped for BlockAggregatedChanges {
178    fn block(&self) -> Block {
179        self.block.clone()
180    }
181}
182
183impl From<dto::Block> for Block {
184    fn from(value: dto::Block) -> Self {
185        Self {
186            number: value.number,
187            chain: value.chain.into(),
188            hash: value.hash,
189            parent_hash: value.parent_hash,
190            ts: value.ts,
191        }
192    }
193}
194
195#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, DeepSizeOf)]
196pub struct DCIUpdate {
197    pub new_entrypoints: HashMap<ComponentId, HashSet<EntryPoint>>,
198    pub new_entrypoint_params: HashMap<EntryPointId, HashSet<(TracingParams, Option<ComponentId>)>>,
199    pub trace_results: HashMap<EntryPointId, TracingResult>,
200}
201
202/// Changes grouped by their respective transaction.
203#[derive(Debug, Clone, PartialEq, Default, DeepSizeOf)]
204pub struct TxWithChanges {
205    pub tx: Transaction,
206    pub protocol_components: HashMap<ComponentId, ProtocolComponent>,
207    pub account_deltas: HashMap<Address, AccountDelta>,
208    pub state_updates: HashMap<ComponentId, ProtocolComponentStateDelta>,
209    pub balance_changes: HashMap<ComponentId, HashMap<Address, ComponentBalance>>,
210    pub account_balance_changes: HashMap<Address, HashMap<Address, AccountBalance>>,
211    pub entrypoints: HashMap<ComponentId, HashSet<EntryPoint>>,
212    pub entrypoint_params: HashMap<EntryPointId, HashSet<(TracingParams, Option<ComponentId>)>>,
213}
214
215impl TxWithChanges {
216    #[allow(clippy::too_many_arguments)]
217    pub fn new(
218        tx: Transaction,
219        protocol_components: HashMap<ComponentId, ProtocolComponent>,
220        account_deltas: HashMap<Address, AccountDelta>,
221        protocol_states: HashMap<ComponentId, ProtocolComponentStateDelta>,
222        balance_changes: HashMap<ComponentId, HashMap<Address, ComponentBalance>>,
223        account_balance_changes: HashMap<Address, HashMap<Address, AccountBalance>>,
224        entrypoints: HashMap<ComponentId, HashSet<EntryPoint>>,
225        entrypoint_params: HashMap<EntryPointId, HashSet<(TracingParams, Option<ComponentId>)>>,
226    ) -> Self {
227        Self {
228            tx,
229            account_deltas,
230            protocol_components,
231            state_updates: protocol_states,
232            balance_changes,
233            account_balance_changes,
234            entrypoints,
235            entrypoint_params,
236        }
237    }
238
239    /// Merges this update with another one.
240    ///
241    /// The method combines two [`TxWithChanges`] instances if they are on the same block.
242    ///
243    /// NB: It is expected that `other` is a more recent update than `self` is and the two are
244    /// combined accordingly.
245    ///
246    /// # Errors
247    /// Returns a `MergeError` if any of the above conditions are violated.
248    pub fn merge(&mut self, other: TxWithChanges) -> Result<(), MergeError> {
249        if self.tx.block_hash != other.tx.block_hash {
250            return Err(MergeError::BlockMismatch(
251                "TxWithChanges".to_string(),
252                self.tx.block_hash.clone(),
253                other.tx.block_hash,
254            ));
255        }
256        if self.tx.index > other.tx.index {
257            return Err(MergeError::TransactionOrderError(
258                "TxWithChanges".to_string(),
259                self.tx.index,
260                other.tx.index,
261            ));
262        }
263
264        self.tx = other.tx;
265
266        // Merge new protocol components
267        // Log a warning if a new protocol component for the same id already exists, because this
268        // should never happen.
269        for (key, value) in other.protocol_components {
270            match self.protocol_components.entry(key) {
271                Entry::Occupied(mut entry) => {
272                    warn!(
273                        "Overwriting new protocol component for id {} with a new one. This should never happen! Please check logic",
274                        entry.get().id
275                    );
276                    entry.insert(value);
277                }
278                Entry::Vacant(entry) => {
279                    entry.insert(value);
280                }
281            }
282        }
283
284        // Merge account deltas
285        for (address, update) in other.account_deltas.clone().into_iter() {
286            match self.account_deltas.entry(address) {
287                Entry::Occupied(mut e) => {
288                    e.get_mut().merge(update)?;
289                }
290                Entry::Vacant(e) => {
291                    e.insert(update);
292                }
293            }
294        }
295
296        // Merge protocol state updates
297        for (key, value) in other.state_updates {
298            match self.state_updates.entry(key) {
299                Entry::Occupied(mut entry) => {
300                    entry.get_mut().merge(value)?;
301                }
302                Entry::Vacant(entry) => {
303                    entry.insert(value);
304                }
305            }
306        }
307
308        // Merge component balance changes
309        for (component_id, balance_changes) in other.balance_changes {
310            let token_balances = self
311                .balance_changes
312                .entry(component_id)
313                .or_default();
314            for (token, balance) in balance_changes {
315                token_balances.insert(token, balance);
316            }
317        }
318
319        // Merge account balance changes
320        for (account_addr, balance_changes) in other.account_balance_changes {
321            let token_balances = self
322                .account_balance_changes
323                .entry(account_addr)
324                .or_default();
325            for (token, balance) in balance_changes {
326                token_balances.insert(token, balance);
327            }
328        }
329
330        // Merge new entrypoints
331        for (component_id, entrypoints) in other.entrypoints {
332            self.entrypoints
333                .entry(component_id)
334                .or_default()
335                .extend(entrypoints);
336        }
337
338        // Merge new entrypoint params
339        for (entrypoint_id, params) in other.entrypoint_params {
340            self.entrypoint_params
341                .entry(entrypoint_id)
342                .or_default()
343                .extend(params);
344        }
345
346        Ok(())
347    }
348}
349
350impl From<AccountChangesWithTx> for TxWithChanges {
351    fn from(value: AccountChangesWithTx) -> Self {
352        Self {
353            tx: value.tx,
354            protocol_components: value.protocol_components,
355            account_deltas: value.account_deltas,
356            balance_changes: value.component_balances,
357            account_balance_changes: value.account_balances,
358            ..Default::default()
359        }
360    }
361}
362
363impl From<ProtocolChangesWithTx> for TxWithChanges {
364    fn from(value: ProtocolChangesWithTx) -> Self {
365        Self {
366            tx: value.tx,
367            protocol_components: value.new_protocol_components,
368            state_updates: value.protocol_states,
369            balance_changes: value.balance_changes,
370            ..Default::default()
371        }
372    }
373}
374
375#[derive(Copy, Clone, Debug, PartialEq)]
376pub enum BlockTag {
377    /// Finalized block
378    Finalized,
379    /// Safe block
380    Safe,
381    /// Latest block
382    Latest,
383    /// Earliest block (genesis)
384    Earliest,
385    /// Pending block (not yet part of the blockchain)
386    Pending,
387    /// Block by number
388    Number(u64),
389}
390#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, DeepSizeOf)]
391pub struct EntryPoint {
392    /// Entry point id
393    pub external_id: String,
394    /// The address of the contract to trace.
395    pub target: Address,
396    /// The signature of the function to trace.
397    pub signature: String,
398}
399
400impl EntryPoint {
401    pub fn new(external_id: String, target: Address, signature: String) -> Self {
402        Self { external_id, target, signature }
403    }
404}
405
406impl From<dto::EntryPoint> for EntryPoint {
407    fn from(value: dto::EntryPoint) -> Self {
408        Self { external_id: value.external_id, target: value.target, signature: value.signature }
409    }
410}
411
412/// A struct that combines an entry point with its associated tracing params.
413#[derive(Debug, Clone, PartialEq, Eq, Hash, DeepSizeOf)]
414pub struct EntryPointWithTracingParams {
415    /// The entry point to trace, containing the target contract address and function signature
416    pub entry_point: EntryPoint,
417    /// The tracing parameters for this entry point
418    pub params: TracingParams,
419}
420
421impl From<dto::EntryPointWithTracingParams> for EntryPointWithTracingParams {
422    fn from(value: dto::EntryPointWithTracingParams) -> Self {
423        match value.params {
424            dto::TracingParams::RPCTracer(ref tracer_params) => Self {
425                entry_point: EntryPoint {
426                    external_id: value.entry_point.external_id,
427                    target: value.entry_point.target,
428                    signature: value.entry_point.signature,
429                },
430                params: TracingParams::RPCTracer(RPCTracerParams {
431                    caller: tracer_params.caller.clone(),
432                    calldata: tracer_params.calldata.clone(),
433                    state_overrides: tracer_params
434                        .state_overrides
435                        .clone()
436                        .map(|s| {
437                            s.into_iter()
438                                .map(|(k, v)| (k, v.into()))
439                                .collect()
440                        }),
441                    prune_addresses: tracer_params.prune_addresses.clone(),
442                }),
443            },
444        }
445    }
446}
447
448impl EntryPointWithTracingParams {
449    pub fn new(entry_point: EntryPoint, params: TracingParams) -> Self {
450        Self { entry_point, params }
451    }
452}
453
454impl std::fmt::Display for EntryPointWithTracingParams {
455    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
456        let tracer_type = match &self.params {
457            TracingParams::RPCTracer(_) => "RPC",
458        };
459        write!(f, "{} [{}]", self.entry_point.external_id, tracer_type)
460    }
461}
462
463#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Eq, Hash, DeepSizeOf)]
464/// An entry point to trace. Different types of entry points tracing will be supported in the
465/// future. Like RPC debug tracing, symbolic execution, etc.
466pub enum TracingParams {
467    /// Uses RPC calls to retrieve the called addresses and retriggers
468    RPCTracer(RPCTracerParams),
469}
470
471impl std::fmt::Display for TracingParams {
472    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
473        match self {
474            TracingParams::RPCTracer(params) => write!(f, "RPC: {params}"),
475        }
476    }
477}
478
479impl From<dto::TracingParams> for TracingParams {
480    fn from(value: dto::TracingParams) -> Self {
481        match value {
482            dto::TracingParams::RPCTracer(tracer_params) => {
483                TracingParams::RPCTracer(tracer_params.into())
484            }
485        }
486    }
487}
488
489#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Eq, Hash, DeepSizeOf)]
490pub enum StorageOverride {
491    Diff(BTreeMap<StoreKey, StoreVal>),
492    Replace(BTreeMap<StoreKey, StoreVal>),
493}
494
495impl From<dto::StorageOverride> for StorageOverride {
496    fn from(value: dto::StorageOverride) -> Self {
497        match value {
498            dto::StorageOverride::Diff(diff) => StorageOverride::Diff(diff),
499            dto::StorageOverride::Replace(replace) => StorageOverride::Replace(replace),
500        }
501    }
502}
503
504#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Eq, Hash, DeepSizeOf)]
505pub struct AccountOverrides {
506    pub slots: Option<StorageOverride>,
507    pub native_balance: Option<Balance>,
508    pub code: Option<Code>,
509}
510
511impl From<dto::AccountOverrides> for AccountOverrides {
512    fn from(value: dto::AccountOverrides) -> Self {
513        Self {
514            slots: value.slots.map(|s| s.into()),
515            native_balance: value.native_balance,
516            code: value.code,
517        }
518    }
519}
520
521#[derive(Debug, Clone, PartialEq, Deserialize, Eq, Hash, DeepSizeOf)]
522pub struct RPCTracerParams {
523    /// The caller address of the transaction, if not provided tracing will use the default value
524    /// for an address defined by the VM.
525    pub caller: Option<Address>,
526    /// The call data used for the tracing call, this needs to include the function selector
527    pub calldata: Bytes,
528    /// Optionally allow for state overrides so that the call works as expected
529    pub state_overrides: Option<BTreeMap<Address, AccountOverrides>>,
530    /// Addresses to prune from trace results. Useful for hooks that use mock
531    /// accounts/routers that shouldn't be tracked in the final DCI results.
532    pub prune_addresses: Option<Vec<Address>>,
533}
534
535impl From<dto::RPCTracerParams> for RPCTracerParams {
536    fn from(value: dto::RPCTracerParams) -> Self {
537        Self {
538            caller: value.caller,
539            calldata: value.calldata,
540            state_overrides: value.state_overrides.map(|overrides| {
541                overrides
542                    .into_iter()
543                    .map(|(address, account_overrides)| (address, account_overrides.into()))
544                    .collect()
545            }),
546            prune_addresses: value.prune_addresses,
547        }
548    }
549}
550
551impl RPCTracerParams {
552    pub fn new(caller: Option<Address>, calldata: Bytes) -> Self {
553        Self { caller, calldata, state_overrides: None, prune_addresses: None }
554    }
555
556    pub fn with_state_overrides(mut self, state: BTreeMap<Address, AccountOverrides>) -> Self {
557        self.state_overrides = Some(state);
558        self
559    }
560
561    pub fn with_prune_addresses(mut self, addresses: Vec<Address>) -> Self {
562        self.prune_addresses = Some(addresses);
563        self
564    }
565}
566
567impl std::fmt::Display for RPCTracerParams {
568    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
569        let caller_str = match &self.caller {
570            Some(addr) => format!("caller={addr}"),
571            None => String::new(),
572        };
573
574        let calldata_str = if self.calldata.len() >= 8 {
575            format!(
576                "calldata=0x{}..({} bytes)",
577                hex::encode(&self.calldata[..8]),
578                self.calldata.len()
579            )
580        } else {
581            format!("calldata={}", self.calldata)
582        };
583
584        let overrides_str = match &self.state_overrides {
585            Some(overrides) if !overrides.is_empty() => {
586                format!(", {} state override(s)", overrides.len())
587            }
588            _ => String::new(),
589        };
590
591        write!(f, "{caller_str}, {calldata_str}{overrides_str}")
592    }
593}
594
595// Ensure serialization order, required by the storage layer
596impl Serialize for RPCTracerParams {
597    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
598    where
599        S: Serializer,
600    {
601        // Count fields: always serialize caller and calldata, plus optional fields
602        let mut field_count = 2;
603        if self.state_overrides.is_some() {
604            field_count += 1;
605        }
606        if self.prune_addresses.is_some() {
607            field_count += 1;
608        }
609
610        let mut state = serializer.serialize_struct("RPCTracerEntryPoint", field_count)?;
611        state.serialize_field("caller", &self.caller)?;
612        state.serialize_field("calldata", &self.calldata)?;
613
614        // Only serialize optional fields if they are present
615        if let Some(ref overrides) = self.state_overrides {
616            state.serialize_field("state_overrides", overrides)?;
617        }
618        if let Some(ref prune_addrs) = self.prune_addresses {
619            state.serialize_field("prune_addresses", prune_addrs)?;
620        }
621
622        state.end()
623    }
624}
625
626#[derive(
627    Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, DeepSizeOf,
628)]
629pub struct AddressStorageLocation {
630    pub key: StoreKey,
631    pub offset: u8,
632}
633
634impl AddressStorageLocation {
635    pub fn new(key: StoreKey, offset: u8) -> Self {
636        Self { key, offset }
637    }
638}
639
640#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, DeepSizeOf)]
641pub struct TracingResult {
642    /// A set of (address, storage slot) pairs representing state that contain a called address.
643    /// If any of these storage slots change, the execution path might change.
644    pub retriggers: HashSet<(Address, AddressStorageLocation)>,
645    /// A map of all addresses that were called during the trace with a list of storage slots that
646    /// were accessed.
647    pub accessed_slots: HashMap<Address, HashSet<StoreKey>>,
648}
649
650impl TracingResult {
651    pub fn new(
652        retriggers: HashSet<(Address, AddressStorageLocation)>,
653        accessed_slots: HashMap<Address, HashSet<StoreKey>>,
654    ) -> Self {
655        Self { retriggers, accessed_slots }
656    }
657
658    /// Merges this tracing result with another one.
659    ///
660    /// The method combines two [`TracingResult`] instances.
661    pub fn merge(&mut self, other: TracingResult) {
662        self.retriggers.extend(other.retriggers);
663        for (address, slots) in other.accessed_slots {
664            self.accessed_slots
665                .entry(address)
666                .or_default()
667                .extend(slots);
668        }
669    }
670}
671
672#[derive(Debug, Clone, PartialEq, DeepSizeOf)]
673/// Represents a traced entry point and the results of the tracing operation.
674pub struct TracedEntryPoint {
675    /// The combined entry point and tracing params that was traced
676    pub entry_point_with_params: EntryPointWithTracingParams,
677    /// The block hash of the block that the entry point was traced on.
678    pub detection_block_hash: BlockHash,
679    /// The results of the tracing operation
680    pub tracing_result: TracingResult,
681}
682
683impl TracedEntryPoint {
684    pub fn new(
685        entry_point_with_params: EntryPointWithTracingParams,
686        detection_block_hash: BlockHash,
687        result: TracingResult,
688    ) -> Self {
689        Self { entry_point_with_params, detection_block_hash, tracing_result: result }
690    }
691
692    pub fn entry_point_id(&self) -> String {
693        self.entry_point_with_params
694            .entry_point
695            .external_id
696            .clone()
697    }
698}
699
700impl std::fmt::Display for TracedEntryPoint {
701    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
702        write!(
703            f,
704            "[{}: {} retriggers, {} accessed addresses]",
705            self.entry_point_id(),
706            self.tracing_result.retriggers.len(),
707            self.tracing_result.accessed_slots.len()
708        )
709    }
710}
711
712#[cfg(test)]
713pub mod fixtures {
714    use std::str::FromStr;
715
716    use rstest::rstest;
717
718    use super::*;
719    use crate::models::ChangeType;
720
721    pub fn transaction01() -> Transaction {
722        Transaction::new(
723            Bytes::zero(32),
724            Bytes::zero(32),
725            Bytes::zero(20),
726            Some(Bytes::zero(20)),
727            10,
728        )
729    }
730
731    pub fn create_transaction(hash: &str, block: &str, index: u64) -> Transaction {
732        Transaction::new(
733            hash.parse().unwrap(),
734            block.parse().unwrap(),
735            Bytes::zero(20),
736            Some(Bytes::zero(20)),
737            index,
738        )
739    }
740
741    #[test]
742    fn test_merge_tx_with_changes() {
743        let base_token = Bytes::from_str("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap();
744        let quote_token = Bytes::from_str("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap();
745        let contract_addr = Bytes::from_str("aaaaaaaaa24eeeb8d57d431224f73832bc34f688").unwrap();
746        let tx_hash0 = "0x2f6350a292c0fc918afe67cb893744a080dacb507b0cea4cc07437b8aff23cdb";
747        let tx_hash1 = "0x0d9e0da36cf9f305a189965b248fc79c923619801e8ab5ef158d4fd528a291ad";
748        let block = "0x0000000000000000000000000000000000000000000000000000000000000000";
749        let component = ProtocolComponent::new(
750            "ambient_USDC_ETH",
751            "test",
752            "vm:pool",
753            Chain::Ethereum,
754            vec![base_token.clone(), quote_token.clone()],
755            vec![contract_addr.clone()],
756            Default::default(),
757            Default::default(),
758            Bytes::from_str(tx_hash0).unwrap(),
759            Default::default(),
760        );
761        let account_delta = AccountDelta::new(
762            Chain::Ethereum,
763            contract_addr.clone(),
764            HashMap::new(),
765            None,
766            Some(vec![0, 0, 0, 0].into()),
767            ChangeType::Creation,
768        );
769
770        let mut changes1 = TxWithChanges::new(
771            create_transaction(tx_hash0, block, 1),
772            HashMap::from([(component.id.clone(), component.clone())]),
773            HashMap::from([(contract_addr.clone(), account_delta.clone())]),
774            HashMap::new(),
775            HashMap::from([(
776                component.id.clone(),
777                HashMap::from([(
778                    base_token.clone(),
779                    ComponentBalance {
780                        token: base_token.clone(),
781                        balance: Bytes::from(800_u64).lpad(32, 0),
782                        balance_float: 800.0,
783                        component_id: component.id.clone(),
784                        modify_tx: Bytes::from_str(tx_hash0).unwrap(),
785                    },
786                )]),
787            )]),
788            HashMap::from([(
789                contract_addr.clone(),
790                HashMap::from([(
791                    base_token.clone(),
792                    AccountBalance {
793                        token: base_token.clone(),
794                        balance: Bytes::from(800_u64).lpad(32, 0),
795                        modify_tx: Bytes::from_str(tx_hash0).unwrap(),
796                        account: contract_addr.clone(),
797                    },
798                )]),
799            )]),
800            HashMap::from([(
801                component.id.clone(),
802                HashSet::from([EntryPoint::new(
803                    "test".to_string(),
804                    contract_addr.clone(),
805                    "function()".to_string(),
806                )]),
807            )]),
808            HashMap::from([(
809                "test".to_string(),
810                HashSet::from([(
811                    TracingParams::RPCTracer(RPCTracerParams::new(
812                        None,
813                        Bytes::from_str("0x000001ef").unwrap(),
814                    )),
815                    Some(component.id.clone()),
816                )]),
817            )]),
818        );
819        let changes2 = TxWithChanges::new(
820            create_transaction(tx_hash1, block, 2),
821            HashMap::from([(
822                component.id.clone(),
823                ProtocolComponent {
824                    creation_tx: Bytes::from_str(tx_hash1).unwrap(),
825                    ..component.clone()
826                },
827            )]),
828            HashMap::from([(
829                contract_addr.clone(),
830                AccountDelta::new(
831                    Chain::Ethereum,
832                    contract_addr.clone(),
833                    HashMap::from([(vec![0, 0, 0, 0].into(), Some(vec![0, 0, 0, 0].into()))]),
834                    None,
835                    None,
836                    ChangeType::Update,
837                ),
838            )]),
839            HashMap::new(),
840            HashMap::from([(
841                component.id.clone(),
842                HashMap::from([(
843                    base_token.clone(),
844                    ComponentBalance {
845                        token: base_token.clone(),
846                        balance: Bytes::from(1000_u64).lpad(32, 0),
847                        balance_float: 1000.0,
848                        component_id: component.id.clone(),
849                        modify_tx: Bytes::from_str(tx_hash1).unwrap(),
850                    },
851                )]),
852            )]),
853            HashMap::from([(
854                contract_addr.clone(),
855                HashMap::from([(
856                    base_token.clone(),
857                    AccountBalance {
858                        token: base_token.clone(),
859                        balance: Bytes::from(1000_u64).lpad(32, 0),
860                        modify_tx: Bytes::from_str(tx_hash1).unwrap(),
861                        account: contract_addr.clone(),
862                    },
863                )]),
864            )]),
865            HashMap::from([(
866                component.id.clone(),
867                HashSet::from([
868                    EntryPoint::new(
869                        "test".to_string(),
870                        contract_addr.clone(),
871                        "function()".to_string(),
872                    ),
873                    EntryPoint::new(
874                        "test2".to_string(),
875                        contract_addr.clone(),
876                        "function_2()".to_string(),
877                    ),
878                ]),
879            )]),
880            HashMap::from([(
881                "test2".to_string(),
882                HashSet::from([(
883                    TracingParams::RPCTracer(RPCTracerParams::new(
884                        None,
885                        Bytes::from_str("0x000001").unwrap(),
886                    )),
887                    None,
888                )]),
889            )]),
890        );
891
892        assert!(changes1.merge(changes2).is_ok());
893        assert_eq!(
894            changes1
895                .account_balance_changes
896                .get(&contract_addr)
897                .unwrap()
898                .get(&base_token)
899                .unwrap()
900                .balance,
901            Bytes::from(1000_u64).lpad(32, 0),
902        );
903        assert_eq!(
904            changes1
905                .balance_changes
906                .get(&component.id)
907                .unwrap()
908                .get(&base_token)
909                .unwrap()
910                .balance,
911            Bytes::from(1000_u64).lpad(32, 0),
912        );
913        assert_eq!(changes1.tx.hash, Bytes::from_str(tx_hash1).unwrap(),);
914        assert_eq!(changes1.entrypoints.len(), 1);
915        assert_eq!(
916            changes1
917                .entrypoints
918                .get(&component.id)
919                .unwrap()
920                .len(),
921            2
922        );
923        let mut expected_entry_points = changes1
924            .entrypoints
925            .values()
926            .flat_map(|ep| ep.iter())
927            .map(|ep| ep.signature.clone())
928            .collect::<Vec<_>>();
929        expected_entry_points.sort();
930        assert_eq!(
931            expected_entry_points,
932            vec!["function()".to_string(), "function_2()".to_string()],
933        );
934    }
935
936    #[rstest]
937    #[case::mismatched_blocks(
938        fixtures::create_transaction("0x01", "0x0abc", 1),
939        fixtures::create_transaction("0x02", "0x0def", 2)
940    )]
941    #[case::older_transaction(
942        fixtures::create_transaction("0x02", "0x0abc", 2),
943        fixtures::create_transaction("0x01", "0x0abc", 1)
944    )]
945    fn test_merge_errors(#[case] tx1: Transaction, #[case] tx2: Transaction) {
946        let mut changes1 = TxWithChanges { tx: tx1, ..Default::default() };
947
948        let changes2 = TxWithChanges { tx: tx2, ..Default::default() };
949
950        assert!(changes1.merge(changes2).is_err());
951    }
952
953    #[test]
954    fn test_rpc_tracer_entry_point_serialization_order() {
955        use std::str::FromStr;
956
957        use serde_json;
958
959        let entry_point = RPCTracerParams::new(
960            Some(Address::from_str("0x1234567890123456789012345678901234567890").unwrap()),
961            Bytes::from_str("0xabcdef").unwrap(),
962        );
963
964        let serialized = serde_json::to_string(&entry_point).unwrap();
965
966        // Verify that "caller" comes before "calldata" in the serialized output
967        assert!(serialized.find("\"caller\"").unwrap() < serialized.find("\"calldata\"").unwrap());
968
969        // Verify we can deserialize it back
970        let deserialized: RPCTracerParams = serde_json::from_str(&serialized).unwrap();
971        assert_eq!(entry_point, deserialized);
972    }
973
974    #[test]
975    fn test_tracing_result_merge() {
976        let address1 = Address::from_str("0x1234567890123456789012345678901234567890").unwrap();
977        let address2 = Address::from_str("0x2345678901234567890123456789012345678901").unwrap();
978        let address3 = Address::from_str("0x3456789012345678901234567890123456789012").unwrap();
979
980        let store_key1 = StoreKey::from(vec![1, 2, 3, 4]);
981        let store_key2 = StoreKey::from(vec![5, 6, 7, 8]);
982
983        let mut result1 = TracingResult::new(
984            HashSet::from([(
985                address1.clone(),
986                AddressStorageLocation::new(store_key1.clone(), 12),
987            )]),
988            HashMap::from([
989                (address2.clone(), HashSet::from([store_key1.clone()])),
990                (address3.clone(), HashSet::from([store_key2.clone()])),
991            ]),
992        );
993
994        let result2 = TracingResult::new(
995            HashSet::from([(
996                address3.clone(),
997                AddressStorageLocation::new(store_key2.clone(), 12),
998            )]),
999            HashMap::from([
1000                (address1.clone(), HashSet::from([store_key1.clone()])),
1001                (address2.clone(), HashSet::from([store_key2.clone()])),
1002            ]),
1003        );
1004
1005        result1.merge(result2);
1006
1007        // Verify retriggers were merged
1008        assert_eq!(result1.retriggers.len(), 2);
1009        assert!(result1
1010            .retriggers
1011            .contains(&(address1.clone(), AddressStorageLocation::new(store_key1.clone(), 12))));
1012        assert!(result1
1013            .retriggers
1014            .contains(&(address3.clone(), AddressStorageLocation::new(store_key2.clone(), 12))));
1015
1016        // Verify accessed slots were merged
1017        assert_eq!(result1.accessed_slots.len(), 3);
1018        assert!(result1
1019            .accessed_slots
1020            .contains_key(&address1));
1021        assert!(result1
1022            .accessed_slots
1023            .contains_key(&address2));
1024        assert!(result1
1025            .accessed_slots
1026            .contains_key(&address3));
1027
1028        assert_eq!(
1029            result1
1030                .accessed_slots
1031                .get(&address2)
1032                .unwrap(),
1033            &HashSet::from([store_key1.clone(), store_key2.clone()])
1034        );
1035    }
1036
1037    #[test]
1038    fn test_entry_point_with_tracing_params_display() {
1039        use std::str::FromStr;
1040
1041        let entry_point = EntryPoint::new(
1042            "uniswap_v3_pool_swap".to_string(),
1043            Address::from_str("0x1234567890123456789012345678901234567890").unwrap(),
1044            "swapExactETHForTokens(uint256,address[],address,uint256)".to_string(),
1045        );
1046
1047        let tracing_params = TracingParams::RPCTracer(RPCTracerParams::new(
1048            Some(Address::from_str("0x9876543210987654321098765432109876543210").unwrap()),
1049            Bytes::from_str("0xabcdef").unwrap(),
1050        ));
1051
1052        let entry_point_with_params = EntryPointWithTracingParams::new(entry_point, tracing_params);
1053
1054        let display_output = entry_point_with_params.to_string();
1055        assert_eq!(display_output, "uniswap_v3_pool_swap [RPC]");
1056    }
1057
1058    #[test]
1059    fn test_traced_entry_point_display() {
1060        use std::str::FromStr;
1061
1062        let entry_point = EntryPoint::new(
1063            "uniswap_v3_pool_swap".to_string(),
1064            Address::from_str("0x1234567890123456789012345678901234567890").unwrap(),
1065            "swapExactETHForTokens(uint256,address[],address,uint256)".to_string(),
1066        );
1067
1068        let tracing_params = TracingParams::RPCTracer(RPCTracerParams::new(
1069            Some(Address::from_str("0x9876543210987654321098765432109876543210").unwrap()),
1070            Bytes::from_str("0xabcdef").unwrap(),
1071        ));
1072
1073        let entry_point_with_params = EntryPointWithTracingParams::new(entry_point, tracing_params);
1074
1075        // Create tracing result with 2 retriggers and 3 accessed addresses
1076        let address1 = Address::from_str("0x1111111111111111111111111111111111111111").unwrap();
1077        let address2 = Address::from_str("0x2222222222222222222222222222222222222222").unwrap();
1078        let address3 = Address::from_str("0x3333333333333333333333333333333333333333").unwrap();
1079
1080        let store_key1 = StoreKey::from(vec![1, 2, 3, 4]);
1081        let store_key2 = StoreKey::from(vec![5, 6, 7, 8]);
1082
1083        let tracing_result = TracingResult::new(
1084            HashSet::from([
1085                (address1.clone(), AddressStorageLocation::new(store_key1.clone(), 0)),
1086                (address2.clone(), AddressStorageLocation::new(store_key2.clone(), 12)),
1087            ]),
1088            HashMap::from([
1089                (address1.clone(), HashSet::from([store_key1.clone()])),
1090                (address2.clone(), HashSet::from([store_key2.clone()])),
1091                (address3.clone(), HashSet::from([store_key1.clone()])),
1092            ]),
1093        );
1094
1095        let traced_entry_point = TracedEntryPoint::new(
1096            entry_point_with_params,
1097            Bytes::from_str("0xabcdef1234567890").unwrap(),
1098            tracing_result,
1099        );
1100
1101        let display_output = traced_entry_point.to_string();
1102        assert_eq!(display_output, "[uniswap_v3_pool_swap: 2 retriggers, 3 accessed addresses]");
1103    }
1104}