tycho_common/models/
blockchain.rs

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