tycho_common/models/
blockchain.rs

1use std::collections::{hash_map::Entry, HashMap, HashSet};
2
3use chrono::NaiveDateTime;
4use serde::{ser::SerializeStruct, Deserialize, Serialize, Serializer};
5use tracing::warn;
6
7use crate::{
8    dto,
9    models::{
10        contract::{AccountBalance, AccountChangesWithTx, AccountDelta},
11        protocol::{
12            ComponentBalance, ProtocolChangesWithTx, ProtocolComponent, ProtocolComponentStateDelta,
13        },
14        token::Token,
15        Address, BlockHash, Chain, ComponentId, EntryPointId, MergeError, StoreKey,
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#[derive(Clone, Default, PartialEq, Debug, Eq, Hash)]
42pub struct Transaction {
43    pub hash: Bytes,
44    pub block_hash: Bytes,
45    pub from: Bytes,
46    pub to: Option<Bytes>,
47    pub index: u64,
48}
49
50impl Transaction {
51    pub fn new(hash: Bytes, block_hash: Bytes, from: Bytes, to: Option<Bytes>, index: u64) -> Self {
52        Transaction { hash, block_hash, from, to, index }
53    }
54}
55
56pub struct BlockTransactionDeltas<T> {
57    pub extractor: String,
58    pub chain: Chain,
59    pub block: Block,
60    pub revert: bool,
61    pub deltas: Vec<TransactionDeltaGroup<T>>,
62}
63
64#[allow(dead_code)]
65pub struct TransactionDeltaGroup<T> {
66    changes: T,
67    protocol_component: HashMap<String, ProtocolComponent>,
68    component_balances: HashMap<String, ComponentBalance>,
69    component_tvl: HashMap<String, f64>,
70    tx: Transaction,
71}
72
73#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
74pub struct BlockAggregatedChanges {
75    pub extractor: String,
76    pub chain: Chain,
77    pub block: Block,
78    pub finalized_block_height: u64,
79    pub revert: bool,
80    pub state_deltas: HashMap<String, ProtocolComponentStateDelta>,
81    pub account_deltas: HashMap<Bytes, AccountDelta>,
82    pub new_tokens: HashMap<Address, Token>,
83    pub new_protocol_components: HashMap<String, ProtocolComponent>,
84    pub deleted_protocol_components: HashMap<String, ProtocolComponent>,
85    pub component_balances: HashMap<ComponentId, HashMap<Bytes, ComponentBalance>>,
86    pub account_balances: HashMap<Address, HashMap<Address, AccountBalance>>,
87    pub component_tvl: HashMap<String, f64>,
88    pub dci_update: DCIUpdate,
89}
90
91impl BlockAggregatedChanges {
92    #[allow(clippy::too_many_arguments)]
93    pub fn new(
94        extractor: &str,
95        chain: Chain,
96        block: Block,
97        finalized_block_height: u64,
98        revert: bool,
99        state_deltas: HashMap<String, ProtocolComponentStateDelta>,
100        account_deltas: HashMap<Bytes, AccountDelta>,
101        new_tokens: HashMap<Address, Token>,
102        new_components: HashMap<String, ProtocolComponent>,
103        deleted_components: HashMap<String, ProtocolComponent>,
104        component_balances: HashMap<ComponentId, HashMap<Bytes, ComponentBalance>>,
105        account_balances: HashMap<Address, HashMap<Address, AccountBalance>>,
106        component_tvl: HashMap<String, f64>,
107        dci_update: DCIUpdate,
108    ) -> Self {
109        Self {
110            extractor: extractor.to_string(),
111            chain,
112            block,
113            finalized_block_height,
114            revert,
115            state_deltas,
116            account_deltas,
117            new_tokens,
118            new_protocol_components: new_components,
119            deleted_protocol_components: deleted_components,
120            component_balances,
121            account_balances,
122            component_tvl,
123            dci_update,
124        }
125    }
126}
127
128impl std::fmt::Display for BlockAggregatedChanges {
129    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130        write!(f, "block_number: {}, extractor: {}", self.block.number, self.extractor)
131    }
132}
133
134impl BlockAggregatedChanges {
135    pub fn drop_state(&self) -> Self {
136        Self {
137            extractor: self.extractor.clone(),
138            chain: self.chain,
139            block: self.block.clone(),
140            finalized_block_height: self.finalized_block_height,
141            revert: self.revert,
142            account_deltas: HashMap::new(),
143            state_deltas: HashMap::new(),
144            new_tokens: self.new_tokens.clone(),
145            new_protocol_components: self.new_protocol_components.clone(),
146            deleted_protocol_components: self.deleted_protocol_components.clone(),
147            component_balances: self.component_balances.clone(),
148            account_balances: self.account_balances.clone(),
149            component_tvl: self.component_tvl.clone(),
150            dci_update: self.dci_update.clone(),
151        }
152    }
153}
154
155pub trait BlockScoped {
156    fn block(&self) -> Block;
157}
158
159impl BlockScoped for BlockAggregatedChanges {
160    fn block(&self) -> Block {
161        self.block.clone()
162    }
163}
164
165impl From<dto::Block> for Block {
166    fn from(value: dto::Block) -> Self {
167        Self {
168            number: value.number,
169            chain: value.chain.into(),
170            hash: value.hash,
171            parent_hash: value.parent_hash,
172            ts: value.ts,
173        }
174    }
175}
176
177#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
178pub struct DCIUpdate {
179    pub new_entrypoints: HashMap<ComponentId, HashSet<EntryPoint>>,
180    pub new_entrypoint_params: HashMap<EntryPointId, HashSet<(TracingParams, Option<ComponentId>)>>,
181    pub trace_results: HashMap<EntryPointId, TracingResult>,
182}
183
184/// Changes grouped by their respective transaction.
185#[derive(Debug, Clone, PartialEq, Default)]
186pub struct TxWithChanges {
187    pub tx: Transaction,
188    pub protocol_components: HashMap<ComponentId, ProtocolComponent>,
189    pub account_deltas: HashMap<Address, AccountDelta>,
190    pub state_updates: HashMap<ComponentId, ProtocolComponentStateDelta>,
191    pub balance_changes: HashMap<ComponentId, HashMap<Address, ComponentBalance>>,
192    pub account_balance_changes: HashMap<Address, HashMap<Address, AccountBalance>>,
193    pub entrypoints: HashMap<ComponentId, HashSet<EntryPoint>>,
194    pub entrypoint_params: HashMap<EntryPointId, HashSet<(TracingParams, Option<ComponentId>)>>,
195}
196
197impl TxWithChanges {
198    #[allow(clippy::too_many_arguments)]
199    pub fn new(
200        tx: Transaction,
201        protocol_components: HashMap<ComponentId, ProtocolComponent>,
202        account_deltas: HashMap<Address, AccountDelta>,
203        protocol_states: HashMap<ComponentId, ProtocolComponentStateDelta>,
204        balance_changes: HashMap<ComponentId, HashMap<Address, ComponentBalance>>,
205        account_balance_changes: HashMap<Address, HashMap<Address, AccountBalance>>,
206        entrypoints: HashMap<ComponentId, HashSet<EntryPoint>>,
207        entrypoint_params: HashMap<EntryPointId, HashSet<(TracingParams, Option<ComponentId>)>>,
208    ) -> Self {
209        Self {
210            tx,
211            account_deltas,
212            protocol_components,
213            state_updates: protocol_states,
214            balance_changes,
215            account_balance_changes,
216            entrypoints,
217            entrypoint_params,
218        }
219    }
220
221    /// Merges this update with another one.
222    ///
223    /// The method combines two [`TxWithChanges`] instances if they are on the same block.
224    ///
225    /// NB: It is expected that `other` is a more recent update than `self` is and the two are
226    /// combined accordingly.
227    ///
228    /// # Errors
229    /// Returns a `MergeError` if any of the above conditions are violated.
230    pub fn merge(&mut self, other: TxWithChanges) -> Result<(), MergeError> {
231        if self.tx.block_hash != other.tx.block_hash {
232            return Err(MergeError::BlockMismatch(
233                "TxWithChanges".to_string(),
234                self.tx.block_hash.clone(),
235                other.tx.block_hash,
236            ));
237        }
238        if self.tx.index > other.tx.index {
239            return Err(MergeError::TransactionOrderError(
240                "TxWithChanges".to_string(),
241                self.tx.index,
242                other.tx.index,
243            ));
244        }
245
246        self.tx = other.tx;
247
248        // Merge new protocol components
249        // Log a warning if a new protocol component for the same id already exists, because this
250        // should never happen.
251        for (key, value) in other.protocol_components {
252            match self.protocol_components.entry(key) {
253                Entry::Occupied(mut entry) => {
254                    warn!(
255                        "Overwriting new protocol component for id {} with a new one. This should never happen! Please check logic",
256                        entry.get().id
257                    );
258                    entry.insert(value);
259                }
260                Entry::Vacant(entry) => {
261                    entry.insert(value);
262                }
263            }
264        }
265
266        // Merge account deltas
267        for (address, update) in other.account_deltas.clone().into_iter() {
268            match self.account_deltas.entry(address) {
269                Entry::Occupied(mut e) => {
270                    e.get_mut().merge(update)?;
271                }
272                Entry::Vacant(e) => {
273                    e.insert(update);
274                }
275            }
276        }
277
278        // Merge protocol state updates
279        for (key, value) in other.state_updates {
280            match self.state_updates.entry(key) {
281                Entry::Occupied(mut entry) => {
282                    entry.get_mut().merge(value)?;
283                }
284                Entry::Vacant(entry) => {
285                    entry.insert(value);
286                }
287            }
288        }
289
290        // Merge component balance changes
291        for (component_id, balance_changes) in other.balance_changes {
292            let token_balances = self
293                .balance_changes
294                .entry(component_id)
295                .or_default();
296            for (token, balance) in balance_changes {
297                token_balances.insert(token, balance);
298            }
299        }
300
301        // Merge account balance changes
302        for (account_addr, balance_changes) in other.account_balance_changes {
303            let token_balances = self
304                .account_balance_changes
305                .entry(account_addr)
306                .or_default();
307            for (token, balance) in balance_changes {
308                token_balances.insert(token, balance);
309            }
310        }
311
312        // Merge new entrypoints
313        for (component_id, entrypoints) in other.entrypoints {
314            self.entrypoints
315                .entry(component_id)
316                .or_default()
317                .extend(entrypoints);
318        }
319
320        // Merge new entrypoint params
321        for (entrypoint_id, params) in other.entrypoint_params {
322            self.entrypoint_params
323                .entry(entrypoint_id)
324                .or_default()
325                .extend(params);
326        }
327
328        Ok(())
329    }
330}
331
332impl From<AccountChangesWithTx> for TxWithChanges {
333    fn from(value: AccountChangesWithTx) -> Self {
334        Self {
335            tx: value.tx,
336            protocol_components: value.protocol_components,
337            account_deltas: value.account_deltas,
338            balance_changes: value.component_balances,
339            account_balance_changes: value.account_balances,
340            ..Default::default()
341        }
342    }
343}
344
345impl From<ProtocolChangesWithTx> for TxWithChanges {
346    fn from(value: ProtocolChangesWithTx) -> Self {
347        Self {
348            tx: value.tx,
349            protocol_components: value.new_protocol_components,
350            state_updates: value.protocol_states,
351            balance_changes: value.balance_changes,
352            ..Default::default()
353        }
354    }
355}
356
357#[derive(Copy, Clone, Debug, PartialEq)]
358pub enum BlockTag {
359    /// Finalized block
360    Finalized,
361    /// Safe block
362    Safe,
363    /// Latest block
364    Latest,
365    /// Earliest block (genesis)
366    Earliest,
367    /// Pending block (not yet part of the blockchain)
368    Pending,
369    /// Block by number
370    Number(u64),
371}
372#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
373pub struct EntryPoint {
374    /// Entry point id
375    pub external_id: String,
376    /// The address of the contract to trace.
377    pub target: Address,
378    /// The signature of the function to trace.
379    pub signature: String,
380}
381
382impl EntryPoint {
383    pub fn new(external_id: String, target: Address, signature: String) -> Self {
384        Self { external_id, target, signature }
385    }
386}
387
388impl From<dto::EntryPoint> for EntryPoint {
389    fn from(value: dto::EntryPoint) -> Self {
390        Self { external_id: value.external_id, target: value.target, signature: value.signature }
391    }
392}
393
394/// A struct that combines an entry point with its associated tracing params.
395#[derive(Debug, Clone, PartialEq, Eq, Hash)]
396pub struct EntryPointWithTracingParams {
397    /// The entry point to trace, containing the target contract address and function signature
398    pub entry_point: EntryPoint,
399    /// The tracing parameters for this entry point
400    pub params: TracingParams,
401}
402
403impl From<dto::EntryPointWithTracingParams> for EntryPointWithTracingParams {
404    fn from(value: dto::EntryPointWithTracingParams) -> Self {
405        match value.params {
406            dto::TracingParams::RPCTracer(ref tracer_params) => Self {
407                entry_point: EntryPoint {
408                    external_id: value.entry_point.external_id,
409                    target: value.entry_point.target,
410                    signature: value.entry_point.signature,
411                },
412                params: TracingParams::RPCTracer(RPCTracerParams {
413                    caller: tracer_params.caller.clone(),
414                    calldata: tracer_params.calldata.clone(),
415                }),
416            },
417        }
418    }
419}
420
421impl EntryPointWithTracingParams {
422    pub fn new(entry_point: EntryPoint, params: TracingParams) -> Self {
423        Self { entry_point, params }
424    }
425}
426
427#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Eq, Hash)]
428/// An entry point to trace. Different types of entry points tracing will be supported in the
429/// future. Like RPC debug tracing, symbolic execution, etc.
430pub enum TracingParams {
431    /// Uses RPC calls to retrieve the called addresses and retriggers
432    RPCTracer(RPCTracerParams),
433}
434
435impl From<dto::TracingParams> for TracingParams {
436    fn from(value: dto::TracingParams) -> Self {
437        match value {
438            dto::TracingParams::RPCTracer(tracer_params) => {
439                TracingParams::RPCTracer(tracer_params.into())
440            }
441        }
442    }
443}
444
445#[derive(Debug, Clone, PartialEq, Deserialize, Eq, Hash)]
446pub struct RPCTracerParams {
447    /// The caller address of the transaction, if not provided tracing will use the default value
448    /// for an address defined by the VM.
449    pub caller: Option<Address>,
450    /// The call data used for the tracing call, this needs to include the function selector
451    pub calldata: Bytes,
452}
453
454impl From<dto::RPCTracerParams> for RPCTracerParams {
455    fn from(value: dto::RPCTracerParams) -> Self {
456        Self { caller: value.caller, calldata: value.calldata }
457    }
458}
459
460impl RPCTracerParams {
461    pub fn new(caller: Option<Address>, calldata: Bytes) -> Self {
462        Self { caller, calldata }
463    }
464}
465
466// Ensure serialization order, required by the storage layer
467impl Serialize for RPCTracerParams {
468    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
469    where
470        S: Serializer,
471    {
472        let mut state = serializer.serialize_struct("RPCTracerEntryPoint", 2)?;
473        state.serialize_field("caller", &self.caller)?;
474        state.serialize_field("calldata", &self.calldata)?;
475        state.end()
476    }
477}
478
479#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
480pub struct TracingResult {
481    /// A set of (address, storage slot) pairs representing state that contain a called address.
482    /// If any of these storage slots change, the execution path might change.
483    pub retriggers: HashSet<(Address, StoreKey)>,
484    /// A map of all addresses that were called during the trace with a list of storage slots that
485    /// were accessed.
486    pub accessed_slots: HashMap<Address, HashSet<StoreKey>>,
487}
488
489impl TracingResult {
490    pub fn new(
491        retriggers: HashSet<(Address, StoreKey)>,
492        accessed_slots: HashMap<Address, HashSet<StoreKey>>,
493    ) -> Self {
494        Self { retriggers, accessed_slots }
495    }
496
497    /// Merges this tracing result with another one.
498    ///
499    /// The method combines two [`TracingResult`] instances.
500    pub fn merge(&mut self, other: TracingResult) {
501        self.retriggers.extend(other.retriggers);
502        for (address, slots) in other.accessed_slots {
503            self.accessed_slots
504                .entry(address)
505                .or_default()
506                .extend(slots);
507        }
508    }
509}
510
511#[derive(Debug, Clone, PartialEq)]
512/// Represents a traced entry point and the results of the tracing operation.
513pub struct TracedEntryPoint {
514    /// The combined entry point and tracing params that was traced
515    pub entry_point_with_params: EntryPointWithTracingParams,
516    /// The block hash of the block that the entry point was traced on.
517    pub detection_block_hash: BlockHash,
518    /// The results of the tracing operation
519    pub tracing_result: TracingResult,
520}
521
522impl TracedEntryPoint {
523    pub fn new(
524        entry_point_with_params: EntryPointWithTracingParams,
525        detection_block_hash: BlockHash,
526        result: TracingResult,
527    ) -> Self {
528        Self { entry_point_with_params, detection_block_hash, tracing_result: result }
529    }
530
531    pub fn entry_point_id(&self) -> String {
532        self.entry_point_with_params
533            .entry_point
534            .external_id
535            .clone()
536    }
537}
538
539#[cfg(test)]
540pub mod fixtures {
541    use std::str::FromStr;
542
543    use rstest::rstest;
544
545    use super::*;
546    use crate::models::ChangeType;
547
548    pub fn transaction01() -> Transaction {
549        Transaction::new(
550            Bytes::zero(32),
551            Bytes::zero(32),
552            Bytes::zero(20),
553            Some(Bytes::zero(20)),
554            10,
555        )
556    }
557
558    pub fn create_transaction(hash: &str, block: &str, index: u64) -> Transaction {
559        Transaction::new(
560            hash.parse().unwrap(),
561            block.parse().unwrap(),
562            Bytes::zero(20),
563            Some(Bytes::zero(20)),
564            index,
565        )
566    }
567
568    #[test]
569    fn test_merge_tx_with_changes() {
570        let base_token = Bytes::from_str("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap();
571        let quote_token = Bytes::from_str("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap();
572        let contract_addr = Bytes::from_str("aaaaaaaaa24eeeb8d57d431224f73832bc34f688").unwrap();
573        let tx_hash0 = "0x2f6350a292c0fc918afe67cb893744a080dacb507b0cea4cc07437b8aff23cdb";
574        let tx_hash1 = "0x0d9e0da36cf9f305a189965b248fc79c923619801e8ab5ef158d4fd528a291ad";
575        let block = "0x0000000000000000000000000000000000000000000000000000000000000000";
576        let component = ProtocolComponent::new(
577            "ambient_USDC_ETH",
578            "test",
579            "vm:pool",
580            Chain::Ethereum,
581            vec![base_token.clone(), quote_token.clone()],
582            vec![contract_addr.clone()],
583            Default::default(),
584            Default::default(),
585            Bytes::from_str(tx_hash0).unwrap(),
586            Default::default(),
587        );
588        let account_delta = AccountDelta::new(
589            Chain::Ethereum,
590            contract_addr.clone(),
591            HashMap::new(),
592            None,
593            Some(vec![0, 0, 0, 0].into()),
594            ChangeType::Creation,
595        );
596
597        let mut changes1 = TxWithChanges::new(
598            create_transaction(tx_hash0, block, 1),
599            HashMap::from([(component.id.clone(), component.clone())]),
600            HashMap::from([(contract_addr.clone(), account_delta.clone())]),
601            HashMap::new(),
602            HashMap::from([(
603                component.id.clone(),
604                HashMap::from([(
605                    base_token.clone(),
606                    ComponentBalance {
607                        token: base_token.clone(),
608                        balance: Bytes::from(800_u64).lpad(32, 0),
609                        balance_float: 800.0,
610                        component_id: component.id.clone(),
611                        modify_tx: Bytes::from_str(tx_hash0).unwrap(),
612                    },
613                )]),
614            )]),
615            HashMap::from([(
616                contract_addr.clone(),
617                HashMap::from([(
618                    base_token.clone(),
619                    AccountBalance {
620                        token: base_token.clone(),
621                        balance: Bytes::from(800_u64).lpad(32, 0),
622                        modify_tx: Bytes::from_str(tx_hash0).unwrap(),
623                        account: contract_addr.clone(),
624                    },
625                )]),
626            )]),
627            HashMap::from([(
628                component.id.clone(),
629                HashSet::from([EntryPoint::new(
630                    "test".to_string(),
631                    contract_addr.clone(),
632                    "function()".to_string(),
633                )]),
634            )]),
635            HashMap::from([(
636                "test".to_string(),
637                HashSet::from([(
638                    TracingParams::RPCTracer(RPCTracerParams::new(
639                        None,
640                        Bytes::from_str("0x000001ef").unwrap(),
641                    )),
642                    Some(component.id.clone()),
643                )]),
644            )]),
645        );
646        let changes2 = TxWithChanges::new(
647            create_transaction(tx_hash1, block, 2),
648            HashMap::from([(
649                component.id.clone(),
650                ProtocolComponent {
651                    creation_tx: Bytes::from_str(tx_hash1).unwrap(),
652                    ..component.clone()
653                },
654            )]),
655            HashMap::from([(
656                contract_addr.clone(),
657                AccountDelta {
658                    slots: HashMap::from([(
659                        vec![0, 0, 0, 0].into(),
660                        Some(vec![0, 0, 0, 0].into()),
661                    )]),
662                    change: ChangeType::Update,
663                    ..account_delta
664                },
665            )]),
666            HashMap::new(),
667            HashMap::from([(
668                component.id.clone(),
669                HashMap::from([(
670                    base_token.clone(),
671                    ComponentBalance {
672                        token: base_token.clone(),
673                        balance: Bytes::from(1000_u64).lpad(32, 0),
674                        balance_float: 1000.0,
675                        component_id: component.id.clone(),
676                        modify_tx: Bytes::from_str(tx_hash1).unwrap(),
677                    },
678                )]),
679            )]),
680            HashMap::from([(
681                contract_addr.clone(),
682                HashMap::from([(
683                    base_token.clone(),
684                    AccountBalance {
685                        token: base_token.clone(),
686                        balance: Bytes::from(1000_u64).lpad(32, 0),
687                        modify_tx: Bytes::from_str(tx_hash1).unwrap(),
688                        account: contract_addr.clone(),
689                    },
690                )]),
691            )]),
692            HashMap::from([(
693                component.id.clone(),
694                HashSet::from([
695                    EntryPoint::new(
696                        "test".to_string(),
697                        contract_addr.clone(),
698                        "function()".to_string(),
699                    ),
700                    EntryPoint::new(
701                        "test2".to_string(),
702                        contract_addr.clone(),
703                        "function_2()".to_string(),
704                    ),
705                ]),
706            )]),
707            HashMap::from([(
708                "test2".to_string(),
709                HashSet::from([(
710                    TracingParams::RPCTracer(RPCTracerParams::new(
711                        None,
712                        Bytes::from_str("0x000001").unwrap(),
713                    )),
714                    None,
715                )]),
716            )]),
717        );
718
719        assert!(changes1.merge(changes2).is_ok());
720        assert_eq!(
721            changes1
722                .account_balance_changes
723                .get(&contract_addr)
724                .unwrap()
725                .get(&base_token)
726                .unwrap()
727                .balance,
728            Bytes::from(1000_u64).lpad(32, 0),
729        );
730        assert_eq!(
731            changes1
732                .balance_changes
733                .get(&component.id)
734                .unwrap()
735                .get(&base_token)
736                .unwrap()
737                .balance,
738            Bytes::from(1000_u64).lpad(32, 0),
739        );
740        assert_eq!(changes1.tx.hash, Bytes::from_str(tx_hash1).unwrap(),);
741        assert_eq!(changes1.entrypoints.len(), 1);
742        assert_eq!(
743            changes1
744                .entrypoints
745                .get(&component.id)
746                .unwrap()
747                .len(),
748            2
749        );
750        let mut expected_entry_points = changes1
751            .entrypoints
752            .values()
753            .flat_map(|ep| ep.iter())
754            .map(|ep| ep.signature.clone())
755            .collect::<Vec<_>>();
756        expected_entry_points.sort();
757        assert_eq!(
758            expected_entry_points,
759            vec!["function()".to_string(), "function_2()".to_string()],
760        );
761    }
762
763    #[rstest]
764    #[case::mismatched_blocks(
765        fixtures::create_transaction("0x01", "0x0abc", 1),
766        fixtures::create_transaction("0x02", "0x0def", 2)
767    )]
768    #[case::older_transaction(
769        fixtures::create_transaction("0x02", "0x0abc", 2),
770        fixtures::create_transaction("0x01", "0x0abc", 1)
771    )]
772    fn test_merge_errors(#[case] tx1: Transaction, #[case] tx2: Transaction) {
773        let mut changes1 = TxWithChanges { tx: tx1, ..Default::default() };
774
775        let changes2 = TxWithChanges { tx: tx2, ..Default::default() };
776
777        assert!(changes1.merge(changes2).is_err());
778    }
779
780    #[test]
781    fn test_rpc_tracer_entry_point_serialization_order() {
782        use std::str::FromStr;
783
784        use serde_json;
785
786        let entry_point = RPCTracerParams::new(
787            Some(Address::from_str("0x1234567890123456789012345678901234567890").unwrap()),
788            Bytes::from_str("0xabcdef").unwrap(),
789        );
790
791        let serialized = serde_json::to_string(&entry_point).unwrap();
792
793        // Verify that "caller" comes before "calldata" in the serialized output
794        assert!(serialized.find("\"caller\"").unwrap() < serialized.find("\"calldata\"").unwrap());
795
796        // Verify we can deserialize it back
797        let deserialized: RPCTracerParams = serde_json::from_str(&serialized).unwrap();
798        assert_eq!(entry_point, deserialized);
799    }
800
801    #[test]
802    fn test_tracing_result_merge() {
803        let address1 = Address::from_str("0x1234567890123456789012345678901234567890").unwrap();
804        let address2 = Address::from_str("0x2345678901234567890123456789012345678901").unwrap();
805        let address3 = Address::from_str("0x3456789012345678901234567890123456789012").unwrap();
806
807        let store_key1 = StoreKey::from(vec![1, 2, 3, 4]);
808        let store_key2 = StoreKey::from(vec![5, 6, 7, 8]);
809
810        let mut result1 = TracingResult::new(
811            HashSet::from([(address1.clone(), store_key1.clone())]),
812            HashMap::from([
813                (address2.clone(), HashSet::from([store_key1.clone()])),
814                (address3.clone(), HashSet::from([store_key2.clone()])),
815            ]),
816        );
817
818        let result2 = TracingResult::new(
819            HashSet::from([(address3.clone(), store_key2.clone())]),
820            HashMap::from([
821                (address1.clone(), HashSet::from([store_key1.clone()])),
822                (address2.clone(), HashSet::from([store_key2.clone()])),
823            ]),
824        );
825
826        result1.merge(result2);
827
828        // Verify retriggers were merged
829        assert_eq!(result1.retriggers.len(), 2);
830        assert!(result1
831            .retriggers
832            .contains(&(address1.clone(), store_key1.clone())));
833        assert!(result1
834            .retriggers
835            .contains(&(address3.clone(), store_key2.clone())));
836
837        // Verify accessed slots were merged
838        assert_eq!(result1.accessed_slots.len(), 3);
839        assert!(result1
840            .accessed_slots
841            .contains_key(&address1));
842        assert!(result1
843            .accessed_slots
844            .contains_key(&address2));
845        assert!(result1
846            .accessed_slots
847            .contains_key(&address3));
848
849        assert_eq!(
850            result1
851                .accessed_slots
852                .get(&address2)
853                .unwrap(),
854            &HashSet::from([store_key1.clone(), store_key2.clone()])
855        );
856    }
857}