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