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 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#[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 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 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 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 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 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 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 for (component_id, entrypoints) in other.entrypoints {
331 self.entrypoints
332 .entry(component_id)
333 .or_default()
334 .extend(entrypoints);
335 }
336
337 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,
378 Safe,
380 Latest,
382 Earliest,
384 Pending,
386 Number(u64),
388}
389#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, DeepSizeOf)]
390pub struct EntryPoint {
391 pub external_id: String,
393 pub target: Address,
395 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#[derive(Debug, Clone, PartialEq, Eq, Hash, DeepSizeOf)]
413pub struct EntryPointWithTracingParams {
414 pub entry_point: EntryPoint,
416 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)]
463pub enum TracingParams {
466 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 pub caller: Option<Address>,
525 pub calldata: Bytes,
527 pub state_overrides: Option<BTreeMap<Address, AccountOverrides>>,
529 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
594impl Serialize for RPCTracerParams {
596 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
597 where
598 S: Serializer,
599 {
600 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 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 pub retriggers: HashSet<(Address, AddressStorageLocation)>,
644 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 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)]
672pub struct TracedEntryPoint {
674 pub entry_point_with_params: EntryPointWithTracingParams,
676 pub detection_block_hash: BlockHash,
678 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 assert!(serialized.find("\"caller\"").unwrap() < serialized.find("\"calldata\"").unwrap());
967
968 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 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 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 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}