tycho_common/models/
blockchain.rs

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