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