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