tycho_common/models/
blockchain.rs

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