Skip to main content

tycho_common/
dto.rs

1//! Data Transfer Objects (or structs)
2//!
3//! These structs serve to serialise and deserialize messages between server and client, they should
4//! be very simple and ideally not contain any business logic.
5//!
6//! Structs in here implement utoipa traits so they can be used to derive an OpenAPI schema.
7#![allow(deprecated)]
8use std::{
9    collections::{BTreeMap, HashMap, HashSet},
10    fmt,
11    hash::{Hash, Hasher},
12};
13
14use chrono::{NaiveDateTime, Utc};
15use deepsize::{Context, DeepSizeOf};
16use serde::{de, Deserialize, Deserializer, Serialize};
17use strum_macros::{Display, EnumString};
18use thiserror::Error;
19use utoipa::{IntoParams, ToSchema};
20use uuid::Uuid;
21
22use crate::{
23    models::{
24        self, blockchain::BlockAggregatedChanges, Address, Balance, Code, ComponentId, StoreKey,
25        StoreVal,
26    },
27    serde_primitives::{
28        hex_bytes, hex_bytes_option, hex_hashmap_key, hex_hashmap_key_value, hex_hashmap_value,
29    },
30    Bytes,
31};
32
33/// Currently supported Blockchains
34#[derive(
35    Debug,
36    Clone,
37    Copy,
38    PartialEq,
39    Eq,
40    Hash,
41    Serialize,
42    Deserialize,
43    EnumString,
44    Display,
45    Default,
46    ToSchema,
47    DeepSizeOf,
48)]
49#[serde(rename_all = "lowercase")]
50#[strum(serialize_all = "lowercase")]
51pub enum Chain {
52    #[default]
53    Ethereum,
54    Starknet,
55    ZkSync,
56    Arbitrum,
57    Base,
58    Bsc,
59    Unichain,
60}
61
62impl From<models::contract::Account> for ResponseAccount {
63    fn from(value: models::contract::Account) -> Self {
64        ResponseAccount::new(
65            value.chain.into(),
66            value.address,
67            value.title,
68            value.slots,
69            value.native_balance,
70            value
71                .token_balances
72                .into_iter()
73                .map(|(k, v)| (k, v.balance))
74                .collect(),
75            value.code,
76            value.code_hash,
77            value.balance_modify_tx,
78            value.code_modify_tx,
79            value.creation_tx,
80        )
81    }
82}
83
84impl From<models::Chain> for Chain {
85    fn from(value: models::Chain) -> Self {
86        match value {
87            models::Chain::Ethereum => Chain::Ethereum,
88            models::Chain::Starknet => Chain::Starknet,
89            models::Chain::ZkSync => Chain::ZkSync,
90            models::Chain::Arbitrum => Chain::Arbitrum,
91            models::Chain::Base => Chain::Base,
92            models::Chain::Bsc => Chain::Bsc,
93            models::Chain::Unichain => Chain::Unichain,
94        }
95    }
96}
97
98#[derive(
99    Debug,
100    PartialEq,
101    Default,
102    Copy,
103    Clone,
104    Deserialize,
105    Serialize,
106    ToSchema,
107    EnumString,
108    Display,
109    DeepSizeOf,
110)]
111pub enum ChangeType {
112    #[default]
113    Update,
114    Deletion,
115    Creation,
116    Unspecified,
117}
118
119impl From<models::ChangeType> for ChangeType {
120    fn from(value: models::ChangeType) -> Self {
121        match value {
122            models::ChangeType::Update => ChangeType::Update,
123            models::ChangeType::Creation => ChangeType::Creation,
124            models::ChangeType::Deletion => ChangeType::Deletion,
125        }
126    }
127}
128
129impl ChangeType {
130    pub fn merge(&self, other: &Self) -> Self {
131        if matches!(self, Self::Creation) {
132            Self::Creation
133        } else {
134            *other
135        }
136    }
137}
138
139#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash, Default)]
140pub struct ExtractorIdentity {
141    pub chain: Chain,
142    pub name: String,
143}
144
145impl ExtractorIdentity {
146    pub fn new(chain: Chain, name: &str) -> Self {
147        Self { chain, name: name.to_owned() }
148    }
149}
150
151impl fmt::Display for ExtractorIdentity {
152    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153        write!(f, "{}:{}", self.chain, self.name)
154    }
155}
156
157/// A command sent from the client to the server
158#[derive(Deserialize, Serialize, Debug, PartialEq, Eq)]
159#[serde(tag = "method", rename_all = "lowercase")]
160pub enum Command {
161    Subscribe {
162        extractor_id: ExtractorIdentity,
163        include_state: bool,
164        /// Enable zstd compression for messages in this subscription.
165        /// Defaults to false for backward compatibility.
166        #[serde(default)]
167        compression: bool,
168        /// Enables receiving partial block update messages in this subscription.
169        /// Defaults to false for backward compatibility.
170        #[serde(default)]
171        partial_blocks: bool,
172    },
173    Unsubscribe {
174        subscription_id: Uuid,
175    },
176}
177
178/// A easy serializable version of `models::error::WebsocketError`
179///
180/// This serves purely to transfer errors via websocket. It is meant to render
181/// similarly to the original struct but does not have server side debug information
182/// attached.
183///
184/// It should contain information needed to handle errors correctly on the client side.
185#[derive(Error, Debug, Serialize, Deserialize, Eq, PartialEq, Clone)]
186pub enum WebsocketError {
187    #[error("Extractor not found: {0}")]
188    ExtractorNotFound(ExtractorIdentity),
189
190    #[error("Subscription not found: {0}")]
191    SubscriptionNotFound(Uuid),
192
193    #[error("Failed to parse JSON: {1}, msg: {0}")]
194    ParseError(String, String),
195
196    #[error("Failed to subscribe to extractor: {0}")]
197    SubscribeError(ExtractorIdentity),
198
199    #[error("Failed to compress message for subscription: {0}, error: {1}")]
200    CompressionError(Uuid, String),
201}
202
203impl From<crate::models::error::WebsocketError> for WebsocketError {
204    fn from(value: crate::models::error::WebsocketError) -> Self {
205        match value {
206            crate::models::error::WebsocketError::ExtractorNotFound(eid) => {
207                Self::ExtractorNotFound(eid.into())
208            }
209            crate::models::error::WebsocketError::SubscriptionNotFound(sid) => {
210                Self::SubscriptionNotFound(sid)
211            }
212            crate::models::error::WebsocketError::ParseError(raw, error) => {
213                Self::ParseError(error.to_string(), raw)
214            }
215            crate::models::error::WebsocketError::SubscribeError(eid) => {
216                Self::SubscribeError(eid.into())
217            }
218            crate::models::error::WebsocketError::CompressionError(sid, error) => {
219                Self::CompressionError(sid, error.to_string())
220            }
221        }
222    }
223}
224
225/// A response sent from the server to the client
226#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)]
227#[serde(tag = "method", rename_all = "lowercase")]
228pub enum Response {
229    NewSubscription { extractor_id: ExtractorIdentity, subscription_id: Uuid },
230    SubscriptionEnded { subscription_id: Uuid },
231    Error(WebsocketError),
232}
233
234/// A message sent from the server to the client
235#[allow(clippy::large_enum_variant)]
236#[derive(Serialize, Deserialize, Debug, Display, Clone)]
237#[serde(untagged)]
238pub enum WebSocketMessage {
239    BlockChanges { subscription_id: Uuid, deltas: BlockChanges },
240    Response(Response),
241}
242
243#[derive(Debug, PartialEq, Clone, Deserialize, Serialize, Default, ToSchema)]
244pub struct Block {
245    pub number: u64,
246    #[serde(with = "hex_bytes")]
247    pub hash: Bytes,
248    #[serde(with = "hex_bytes")]
249    pub parent_hash: Bytes,
250    pub chain: Chain,
251    pub ts: NaiveDateTime,
252}
253
254#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema, Eq, Hash, DeepSizeOf)]
255#[serde(deny_unknown_fields)]
256pub struct BlockParam {
257    #[schema(value_type=Option<String>)]
258    #[serde(with = "hex_bytes_option", default)]
259    pub hash: Option<Bytes>,
260    #[deprecated(
261        note = "The `chain` field is deprecated and will be removed in a future version."
262    )]
263    #[serde(default)]
264    pub chain: Option<Chain>,
265    #[serde(default)]
266    pub number: Option<i64>,
267}
268
269impl From<&Block> for BlockParam {
270    fn from(value: &Block) -> Self {
271        // The hash should uniquely identify a block across chains
272        BlockParam { hash: Some(value.hash.clone()), chain: None, number: None }
273    }
274}
275
276#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
277pub struct TokenBalances(#[serde(with = "hex_hashmap_key")] pub HashMap<Bytes, ComponentBalance>);
278
279impl From<HashMap<Bytes, ComponentBalance>> for TokenBalances {
280    fn from(value: HashMap<Bytes, ComponentBalance>) -> Self {
281        TokenBalances(value)
282    }
283}
284
285#[derive(Debug, PartialEq, Clone, Default, Deserialize, Serialize)]
286pub struct Transaction {
287    #[serde(with = "hex_bytes")]
288    pub hash: Bytes,
289    #[serde(with = "hex_bytes")]
290    pub block_hash: Bytes,
291    #[serde(with = "hex_bytes")]
292    pub from: Bytes,
293    #[serde(with = "hex_bytes_option")]
294    pub to: Option<Bytes>,
295    pub index: u64,
296}
297
298impl Transaction {
299    pub fn new(hash: Bytes, block_hash: Bytes, from: Bytes, to: Option<Bytes>, index: u64) -> Self {
300        Self { hash, block_hash, from, to, index }
301    }
302}
303
304/// A container for updates grouped by account/component.
305#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
306pub struct BlockChanges {
307    pub extractor: String,
308    pub chain: Chain,
309    pub block: Block,
310    pub finalized_block_height: u64,
311    pub revert: bool,
312    #[serde(with = "hex_hashmap_key", default)]
313    pub new_tokens: HashMap<Bytes, ResponseToken>,
314    #[serde(alias = "account_deltas", with = "hex_hashmap_key")]
315    pub account_updates: HashMap<Bytes, AccountUpdate>,
316    #[serde(alias = "state_deltas")]
317    pub state_updates: HashMap<String, ProtocolStateDelta>,
318    pub new_protocol_components: HashMap<String, ProtocolComponent>,
319    pub deleted_protocol_components: HashMap<String, ProtocolComponent>,
320    pub component_balances: HashMap<String, TokenBalances>,
321    pub account_balances: HashMap<Bytes, HashMap<Bytes, AccountBalance>>,
322    pub component_tvl: HashMap<String, f64>,
323    pub dci_update: DCIUpdate,
324    /// The index of the partial block. None if it's a full block.
325    #[serde(default, skip_serializing_if = "Option::is_none")]
326    pub partial_block_index: Option<u32>,
327}
328
329impl BlockChanges {
330    #[allow(clippy::too_many_arguments)]
331    pub fn new(
332        extractor: &str,
333        chain: Chain,
334        block: Block,
335        finalized_block_height: u64,
336        revert: bool,
337        account_updates: HashMap<Bytes, AccountUpdate>,
338        state_updates: HashMap<String, ProtocolStateDelta>,
339        new_protocol_components: HashMap<String, ProtocolComponent>,
340        deleted_protocol_components: HashMap<String, ProtocolComponent>,
341        component_balances: HashMap<String, HashMap<Bytes, ComponentBalance>>,
342        account_balances: HashMap<Bytes, HashMap<Bytes, AccountBalance>>,
343        dci_update: DCIUpdate,
344    ) -> Self {
345        BlockChanges {
346            extractor: extractor.to_owned(),
347            chain,
348            block,
349            finalized_block_height,
350            revert,
351            new_tokens: HashMap::new(),
352            account_updates,
353            state_updates,
354            new_protocol_components,
355            deleted_protocol_components,
356            component_balances: component_balances
357                .into_iter()
358                .map(|(k, v)| (k, v.into()))
359                .collect(),
360            account_balances,
361            component_tvl: HashMap::new(),
362            dci_update,
363            partial_block_index: None,
364        }
365    }
366
367    pub fn merge(mut self, other: Self) -> Self {
368        other
369            .account_updates
370            .into_iter()
371            .for_each(|(k, v)| {
372                self.account_updates
373                    .entry(k)
374                    .and_modify(|e| {
375                        e.merge(&v);
376                    })
377                    .or_insert(v);
378            });
379
380        other
381            .state_updates
382            .into_iter()
383            .for_each(|(k, v)| {
384                self.state_updates
385                    .entry(k)
386                    .and_modify(|e| {
387                        e.merge(&v);
388                    })
389                    .or_insert(v);
390            });
391
392        other
393            .component_balances
394            .into_iter()
395            .for_each(|(k, v)| {
396                self.component_balances
397                    .entry(k)
398                    .and_modify(|e| e.0.extend(v.0.clone()))
399                    .or_insert_with(|| v);
400            });
401
402        other
403            .account_balances
404            .into_iter()
405            .for_each(|(k, v)| {
406                self.account_balances
407                    .entry(k)
408                    .and_modify(|e| e.extend(v.clone()))
409                    .or_insert(v);
410            });
411
412        self.component_tvl
413            .extend(other.component_tvl);
414        self.new_protocol_components
415            .extend(other.new_protocol_components);
416        self.deleted_protocol_components
417            .extend(other.deleted_protocol_components);
418        self.revert = other.revert;
419        self.block = other.block;
420
421        self
422    }
423
424    pub fn get_block(&self) -> &Block {
425        &self.block
426    }
427
428    pub fn is_revert(&self) -> bool {
429        self.revert
430    }
431
432    pub fn filter_by_component<F: Fn(&str) -> bool>(&mut self, keep: F) {
433        self.state_updates
434            .retain(|k, _| keep(k));
435        self.component_balances
436            .retain(|k, _| keep(k));
437        self.component_tvl
438            .retain(|k, _| keep(k));
439    }
440
441    pub fn filter_by_contract<F: Fn(&Bytes) -> bool>(&mut self, keep: F) {
442        self.account_updates
443            .retain(|k, _| keep(k));
444        self.account_balances
445            .retain(|k, _| keep(k));
446    }
447
448    pub fn n_changes(&self) -> usize {
449        self.account_updates.len() + self.state_updates.len()
450    }
451
452    pub fn drop_state(&self) -> Self {
453        Self {
454            extractor: self.extractor.clone(),
455            chain: self.chain,
456            block: self.block.clone(),
457            finalized_block_height: self.finalized_block_height,
458            revert: self.revert,
459            new_tokens: self.new_tokens.clone(),
460            account_updates: HashMap::new(),
461            state_updates: HashMap::new(),
462            new_protocol_components: self.new_protocol_components.clone(),
463            deleted_protocol_components: self.deleted_protocol_components.clone(),
464            component_balances: self.component_balances.clone(),
465            account_balances: self.account_balances.clone(),
466            component_tvl: self.component_tvl.clone(),
467            dci_update: self.dci_update.clone(),
468            partial_block_index: self.partial_block_index,
469        }
470    }
471
472    pub fn is_partial_block(&self) -> bool {
473        self.partial_block_index.is_some()
474    }
475}
476
477impl From<models::blockchain::Block> for Block {
478    fn from(value: models::blockchain::Block) -> Self {
479        Self {
480            number: value.number,
481            hash: value.hash,
482            parent_hash: value.parent_hash,
483            chain: value.chain.into(),
484            ts: value.ts,
485        }
486    }
487}
488
489impl From<models::protocol::ComponentBalance> for ComponentBalance {
490    fn from(value: models::protocol::ComponentBalance) -> Self {
491        Self {
492            token: value.token,
493            balance: value.balance,
494            balance_float: value.balance_float,
495            modify_tx: value.modify_tx,
496            component_id: value.component_id,
497        }
498    }
499}
500
501impl From<models::contract::AccountBalance> for AccountBalance {
502    fn from(value: models::contract::AccountBalance) -> Self {
503        Self {
504            account: value.account,
505            token: value.token,
506            balance: value.balance,
507            modify_tx: value.modify_tx,
508        }
509    }
510}
511
512impl From<BlockAggregatedChanges> for BlockChanges {
513    fn from(value: BlockAggregatedChanges) -> Self {
514        Self {
515            extractor: value.extractor,
516            chain: value.chain.into(),
517            block: value.block.into(),
518            finalized_block_height: value.finalized_block_height,
519            revert: value.revert,
520            account_updates: value
521                .account_deltas
522                .into_iter()
523                .map(|(k, v)| (k, v.into()))
524                .collect(),
525            state_updates: value
526                .state_deltas
527                .into_iter()
528                .map(|(k, v)| (k, v.into()))
529                .collect(),
530            new_protocol_components: value
531                .new_protocol_components
532                .into_iter()
533                .map(|(k, v)| (k, v.into()))
534                .collect(),
535            deleted_protocol_components: value
536                .deleted_protocol_components
537                .into_iter()
538                .map(|(k, v)| (k, v.into()))
539                .collect(),
540            component_balances: value
541                .component_balances
542                .into_iter()
543                .map(|(component_id, v)| {
544                    let balances: HashMap<Bytes, ComponentBalance> = v
545                        .into_iter()
546                        .map(|(k, v)| (k, ComponentBalance::from(v)))
547                        .collect();
548                    (component_id, balances.into())
549                })
550                .collect(),
551            account_balances: value
552                .account_balances
553                .into_iter()
554                .map(|(k, v)| {
555                    (
556                        k,
557                        v.into_iter()
558                            .map(|(k, v)| (k, v.into()))
559                            .collect(),
560                    )
561                })
562                .collect(),
563            dci_update: value.dci_update.into(),
564            new_tokens: value
565                .new_tokens
566                .into_iter()
567                .map(|(k, v)| (k, v.into()))
568                .collect(),
569            component_tvl: value.component_tvl,
570            partial_block_index: value.partial_block_index,
571        }
572    }
573}
574
575#[derive(PartialEq, Serialize, Deserialize, Clone, Debug, ToSchema)]
576pub struct AccountUpdate {
577    #[serde(with = "hex_bytes")]
578    #[schema(value_type=Vec<String>)]
579    pub address: Bytes,
580    pub chain: Chain,
581    #[serde(with = "hex_hashmap_key_value")]
582    #[schema(value_type=HashMap<String, String>)]
583    pub slots: HashMap<Bytes, Bytes>,
584    #[serde(with = "hex_bytes_option")]
585    #[schema(value_type=Option<String>)]
586    pub balance: Option<Bytes>,
587    #[serde(with = "hex_bytes_option")]
588    #[schema(value_type=Option<String>)]
589    pub code: Option<Bytes>,
590    pub change: ChangeType,
591}
592
593impl AccountUpdate {
594    pub fn new(
595        address: Bytes,
596        chain: Chain,
597        slots: HashMap<Bytes, Bytes>,
598        balance: Option<Bytes>,
599        code: Option<Bytes>,
600        change: ChangeType,
601    ) -> Self {
602        Self { address, chain, slots, balance, code, change }
603    }
604
605    pub fn merge(&mut self, other: &Self) {
606        self.slots.extend(
607            other
608                .slots
609                .iter()
610                .map(|(k, v)| (k.clone(), v.clone())),
611        );
612        self.balance.clone_from(&other.balance);
613        self.code.clone_from(&other.code);
614        self.change = self.change.merge(&other.change);
615    }
616}
617
618impl From<models::contract::AccountDelta> for AccountUpdate {
619    fn from(value: models::contract::AccountDelta) -> Self {
620        let code = value.code().clone();
621        let change_type = value.change_type().into();
622        AccountUpdate::new(
623            value.address,
624            value.chain.into(),
625            value
626                .slots
627                .into_iter()
628                .map(|(k, v)| (k, v.unwrap_or_default()))
629                .collect(),
630            value.balance,
631            code,
632            change_type,
633        )
634    }
635}
636
637/// Represents the static parts of a protocol component.
638#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize, ToSchema)]
639pub struct ProtocolComponent {
640    /// Unique identifier for this component
641    pub id: String,
642    /// Protocol system this component is part of
643    pub protocol_system: String,
644    /// Type of the protocol system
645    pub protocol_type_name: String,
646    pub chain: Chain,
647    /// Token addresses the component operates on
648    #[schema(value_type=Vec<String>)]
649    pub tokens: Vec<Bytes>,
650    /// Contract addresses involved in the components operations (may be empty for
651    /// native implementations)
652    #[serde(alias = "contract_addresses")]
653    #[schema(value_type=Vec<String>)]
654    pub contract_ids: Vec<Bytes>,
655    /// Constant attributes of the component
656    #[serde(with = "hex_hashmap_value")]
657    #[schema(value_type=HashMap<String, String>)]
658    pub static_attributes: HashMap<String, Bytes>,
659    /// Indicates if last change was update, create or delete (for internal use only).
660    #[serde(default)]
661    pub change: ChangeType,
662    /// Transaction hash which created this component
663    #[serde(with = "hex_bytes")]
664    #[schema(value_type=String)]
665    pub creation_tx: Bytes,
666    /// Date time of creation in UTC time
667    pub created_at: NaiveDateTime,
668}
669
670// Manual impl as `NaiveDateTime` structure referenced in `created_at` does not implement DeepSizeOf
671impl DeepSizeOf for ProtocolComponent {
672    fn deep_size_of_children(&self, ctx: &mut Context) -> usize {
673        self.id.deep_size_of_children(ctx) +
674            self.protocol_system
675                .deep_size_of_children(ctx) +
676            self.protocol_type_name
677                .deep_size_of_children(ctx) +
678            self.chain.deep_size_of_children(ctx) +
679            self.tokens.deep_size_of_children(ctx) +
680            self.contract_ids
681                .deep_size_of_children(ctx) +
682            self.static_attributes
683                .deep_size_of_children(ctx) +
684            self.change.deep_size_of_children(ctx) +
685            self.creation_tx
686                .deep_size_of_children(ctx)
687    }
688}
689
690impl From<models::protocol::ProtocolComponent> for ProtocolComponent {
691    fn from(value: models::protocol::ProtocolComponent) -> Self {
692        Self {
693            id: value.id,
694            protocol_system: value.protocol_system,
695            protocol_type_name: value.protocol_type_name,
696            chain: value.chain.into(),
697            tokens: value.tokens,
698            contract_ids: value.contract_addresses,
699            static_attributes: value.static_attributes,
700            change: value.change.into(),
701            creation_tx: value.creation_tx,
702            created_at: value.created_at,
703        }
704    }
705}
706
707#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
708pub struct ComponentBalance {
709    #[serde(with = "hex_bytes")]
710    pub token: Bytes,
711    pub balance: Bytes,
712    pub balance_float: f64,
713    #[serde(with = "hex_bytes")]
714    pub modify_tx: Bytes,
715    pub component_id: String,
716}
717
718#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, ToSchema)]
719/// Represents a change in protocol state.
720pub struct ProtocolStateDelta {
721    pub component_id: String,
722    #[schema(value_type=HashMap<String, String>)]
723    pub updated_attributes: HashMap<String, Bytes>,
724    pub deleted_attributes: HashSet<String>,
725}
726
727impl From<models::protocol::ProtocolComponentStateDelta> for ProtocolStateDelta {
728    fn from(value: models::protocol::ProtocolComponentStateDelta) -> Self {
729        Self {
730            component_id: value.component_id,
731            updated_attributes: value.updated_attributes,
732            deleted_attributes: value.deleted_attributes,
733        }
734    }
735}
736
737impl ProtocolStateDelta {
738    /// Merges 'other' into 'self'.
739    ///
740    ///
741    /// During merge of these deltas a special situation can arise when an attribute is present in
742    /// `self.deleted_attributes` and `other.update_attributes``. If we would just merge the sets
743    /// of deleted attributes or vice versa, it would be ambiguous and potential lead to a
744    /// deletion of an attribute that should actually be present, or retention of an actually
745    /// deleted attribute.
746    ///
747    /// This situation is handled the following way:
748    ///
749    ///     - If an attribute is deleted and in the next message recreated, it is removed from the
750    ///       set of deleted attributes and kept in updated_attributes. This way it's temporary
751    ///       deletion is never communicated to the final receiver.
752    ///     - If an attribute was updated and is deleted in the next message, it is removed from
753    ///       updated attributes and kept in deleted. This way the attributes temporary update (or
754    ///       potentially short-lived existence) before its deletion is never communicated to the
755    ///       final receiver.
756    pub fn merge(&mut self, other: &Self) {
757        // either updated and then deleted -> keep in deleted, remove from updated
758        self.updated_attributes
759            .retain(|k, _| !other.deleted_attributes.contains(k));
760
761        // or deleted and then updated/recreated -> remove from deleted and keep in updated
762        self.deleted_attributes.retain(|attr| {
763            !other
764                .updated_attributes
765                .contains_key(attr)
766        });
767
768        // simply merge updates
769        self.updated_attributes.extend(
770            other
771                .updated_attributes
772                .iter()
773                .map(|(k, v)| (k.clone(), v.clone())),
774        );
775
776        // simply merge deletions
777        self.deleted_attributes
778            .extend(other.deleted_attributes.iter().cloned());
779    }
780}
781
782/// Pagination parameter
783#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema, Eq, Hash, DeepSizeOf)]
784#[serde(deny_unknown_fields)]
785pub struct PaginationParams {
786    /// What page to retrieve
787    #[serde(default)]
788    pub page: i64,
789    /// How many results to return per page
790    #[serde(default)]
791    #[schema(default = 100)]
792    pub page_size: i64,
793}
794
795impl PaginationParams {
796    pub fn new(page: i64, page_size: i64) -> Self {
797        Self { page, page_size }
798    }
799}
800
801impl Default for PaginationParams {
802    fn default() -> Self {
803        PaginationParams { page: 0, page_size: 100 }
804    }
805}
806
807/// Defines pagination size limits for request types.
808///
809/// Different limits apply based on whether compression is enabled,
810/// as compressed responses can safely transfer more data.
811pub trait PaginationLimits {
812    /// Maximum page size when compression is enabled (e.g., zstd)
813    const MAX_PAGE_SIZE_COMPRESSED: i64;
814
815    /// Maximum page size when compression is disabled
816    const MAX_PAGE_SIZE_UNCOMPRESSED: i64;
817
818    /// Returns the effective maximum page size based on compression setting
819    fn effective_max_page_size(compression: bool) -> i64 {
820        if compression {
821            Self::MAX_PAGE_SIZE_COMPRESSED
822        } else {
823            Self::MAX_PAGE_SIZE_UNCOMPRESSED
824        }
825    }
826
827    /// Returns a reference to the pagination parameters
828    fn pagination(&self) -> &PaginationParams;
829}
830
831/// Macro to implement PaginationLimits for request types
832///
833/// When INCREASING these limits, ensure to immediately redeploy the servers.
834///
835/// Why: pagination limits are shared. Clients use these constants to set their max page size.
836/// When clients upgrade before servers, they request more than old servers allow and get errors.
837macro_rules! impl_pagination_limits {
838    ($type:ty, compressed = $comp:expr, uncompressed = $uncomp:expr) => {
839        impl $crate::dto::PaginationLimits for $type {
840            const MAX_PAGE_SIZE_COMPRESSED: i64 = $comp;
841            const MAX_PAGE_SIZE_UNCOMPRESSED: i64 = $uncomp;
842
843            fn pagination(&self) -> &$crate::dto::PaginationParams {
844                &self.pagination
845            }
846        }
847    };
848}
849
850#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema, Eq, Hash, DeepSizeOf)]
851#[serde(deny_unknown_fields)]
852pub struct PaginationResponse {
853    pub page: i64,
854    pub page_size: i64,
855    /// The total number of items available across all pages of results
856    pub total: i64,
857}
858
859/// Current pagination information
860impl PaginationResponse {
861    pub fn new(page: i64, page_size: i64, total: i64) -> Self {
862        Self { page, page_size, total }
863    }
864
865    pub fn total_pages(&self) -> i64 {
866        // ceil(total / page_size)
867        (self.total + self.page_size - 1) / self.page_size
868    }
869}
870
871#[derive(
872    Clone, Serialize, Debug, Default, Deserialize, PartialEq, ToSchema, Eq, Hash, DeepSizeOf,
873)]
874#[serde(deny_unknown_fields)]
875pub struct StateRequestBody {
876    /// Filters response by contract addresses
877    #[serde(alias = "contractIds")]
878    #[schema(value_type=Option<Vec<String>>)]
879    pub contract_ids: Option<Vec<Bytes>>,
880    /// Does not filter response, only required to correctly apply unconfirmed state
881    /// from ReorgBuffers
882    #[serde(alias = "protocolSystem", default)]
883    pub protocol_system: String,
884    #[serde(default = "VersionParam::default")]
885    pub version: VersionParam,
886    #[serde(default)]
887    pub chain: Chain,
888    #[serde(default)]
889    pub pagination: PaginationParams,
890}
891
892// When INCREASING these limits, please read the warning in the macro definition.
893impl_pagination_limits!(StateRequestBody, compressed = 1200, uncompressed = 100);
894
895impl StateRequestBody {
896    pub fn new(
897        contract_ids: Option<Vec<Bytes>>,
898        protocol_system: String,
899        version: VersionParam,
900        chain: Chain,
901        pagination: PaginationParams,
902    ) -> Self {
903        Self { contract_ids, protocol_system, version, chain, pagination }
904    }
905
906    pub fn from_block(protocol_system: &str, block: BlockParam) -> Self {
907        Self {
908            contract_ids: None,
909            protocol_system: protocol_system.to_string(),
910            version: VersionParam { timestamp: None, block: Some(block.clone()) },
911            chain: block.chain.unwrap_or_default(),
912            pagination: PaginationParams::default(),
913        }
914    }
915
916    pub fn from_timestamp(protocol_system: &str, timestamp: NaiveDateTime, chain: Chain) -> Self {
917        Self {
918            contract_ids: None,
919            protocol_system: protocol_system.to_string(),
920            version: VersionParam { timestamp: Some(timestamp), block: None },
921            chain,
922            pagination: PaginationParams::default(),
923        }
924    }
925}
926
927/// Response from Tycho server for a contract state request.
928#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema, DeepSizeOf)]
929pub struct StateRequestResponse {
930    pub accounts: Vec<ResponseAccount>,
931    pub pagination: PaginationResponse,
932}
933
934impl StateRequestResponse {
935    pub fn new(accounts: Vec<ResponseAccount>, pagination: PaginationResponse) -> Self {
936        Self { accounts, pagination }
937    }
938}
939
940#[derive(PartialEq, Clone, Serialize, Deserialize, Default, ToSchema, DeepSizeOf)]
941#[serde(rename = "Account")]
942/// Account struct for the response from Tycho server for a contract state request.
943///
944/// Code is serialized as a hex string instead of a list of bytes.
945pub struct ResponseAccount {
946    pub chain: Chain,
947    /// The address of the account as hex encoded string
948    #[schema(value_type=String, example="0xc9f2e6ea1637E499406986ac50ddC92401ce1f58")]
949    #[serde(with = "hex_bytes")]
950    pub address: Bytes,
951    /// The title of the account usualy specifying its function within the protocol
952    #[schema(value_type=String, example="Protocol Vault")]
953    pub title: String,
954    /// Contract storage map of hex encoded string values
955    #[schema(value_type=HashMap<String, String>, example=json!({"0x....": "0x...."}))]
956    #[serde(with = "hex_hashmap_key_value")]
957    pub slots: HashMap<Bytes, Bytes>,
958    /// The balance of the account in the native token
959    #[schema(value_type=String, example="0x00")]
960    #[serde(with = "hex_bytes")]
961    pub native_balance: Bytes,
962    /// Balances of this account in other tokens (only tokens balance that are
963    /// relevant to the protocol are returned here)
964    #[schema(value_type=HashMap<String, String>, example=json!({"0x....": "0x...."}))]
965    #[serde(with = "hex_hashmap_key_value")]
966    pub token_balances: HashMap<Bytes, Bytes>,
967    /// The accounts code as hex encoded string
968    #[schema(value_type=String, example="0xBADBABE")]
969    #[serde(with = "hex_bytes")]
970    pub code: Bytes,
971    /// The hash of above code
972    #[schema(value_type=String, example="0x123456789")]
973    #[serde(with = "hex_bytes")]
974    pub code_hash: Bytes,
975    /// Transaction hash which last modified native balance
976    #[schema(value_type=String, example="0x8f1133bfb054a23aedfe5d25b1d81b96195396d8b88bd5d4bcf865fc1ae2c3f4")]
977    #[serde(with = "hex_bytes")]
978    pub balance_modify_tx: Bytes,
979    /// Transaction hash which last modified code
980    #[schema(value_type=String, example="0x8f1133bfb054a23aedfe5d25b1d81b96195396d8b88bd5d4bcf865fc1ae2c3f4")]
981    #[serde(with = "hex_bytes")]
982    pub code_modify_tx: Bytes,
983    /// Transaction hash which created the account
984    #[deprecated(note = "The `creation_tx` field is deprecated.")]
985    #[schema(value_type=Option<String>, example="0x8f1133bfb054a23aedfe5d25b1d81b96195396d8b88bd5d4bcf865fc1ae2c3f4")]
986    #[serde(with = "hex_bytes_option")]
987    pub creation_tx: Option<Bytes>,
988}
989
990impl ResponseAccount {
991    #[allow(clippy::too_many_arguments)]
992    pub fn new(
993        chain: Chain,
994        address: Bytes,
995        title: String,
996        slots: HashMap<Bytes, Bytes>,
997        native_balance: Bytes,
998        token_balances: HashMap<Bytes, Bytes>,
999        code: Bytes,
1000        code_hash: Bytes,
1001        balance_modify_tx: Bytes,
1002        code_modify_tx: Bytes,
1003        creation_tx: Option<Bytes>,
1004    ) -> Self {
1005        Self {
1006            chain,
1007            address,
1008            title,
1009            slots,
1010            native_balance,
1011            token_balances,
1012            code,
1013            code_hash,
1014            balance_modify_tx,
1015            code_modify_tx,
1016            creation_tx,
1017        }
1018    }
1019}
1020
1021/// Implement Debug for ResponseAccount manually to avoid printing the code field.
1022impl fmt::Debug for ResponseAccount {
1023    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1024        f.debug_struct("ResponseAccount")
1025            .field("chain", &self.chain)
1026            .field("address", &self.address)
1027            .field("title", &self.title)
1028            .field("slots", &self.slots)
1029            .field("native_balance", &self.native_balance)
1030            .field("token_balances", &self.token_balances)
1031            .field("code", &format!("[{} bytes]", self.code.len()))
1032            .field("code_hash", &self.code_hash)
1033            .field("balance_modify_tx", &self.balance_modify_tx)
1034            .field("code_modify_tx", &self.code_modify_tx)
1035            .field("creation_tx", &self.creation_tx)
1036            .finish()
1037    }
1038}
1039
1040#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
1041pub struct AccountBalance {
1042    #[serde(with = "hex_bytes")]
1043    pub account: Bytes,
1044    #[serde(with = "hex_bytes")]
1045    pub token: Bytes,
1046    #[serde(with = "hex_bytes")]
1047    pub balance: Bytes,
1048    #[serde(with = "hex_bytes")]
1049    pub modify_tx: Bytes,
1050}
1051
1052#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
1053#[serde(deny_unknown_fields)]
1054pub struct ContractId {
1055    #[serde(with = "hex_bytes")]
1056    #[schema(value_type=String)]
1057    pub address: Bytes,
1058    pub chain: Chain,
1059}
1060
1061/// Uniquely identifies a contract on a specific chain.
1062impl ContractId {
1063    pub fn new(chain: Chain, address: Bytes) -> Self {
1064        Self { address, chain }
1065    }
1066
1067    pub fn address(&self) -> &Bytes {
1068        &self.address
1069    }
1070}
1071
1072impl fmt::Display for ContractId {
1073    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1074        write!(f, "{:?}: 0x{}", self.chain, hex::encode(&self.address))
1075    }
1076}
1077
1078/// The version of the requested state, given as either a timestamp or a block.
1079///
1080/// If block is provided, the state at that exact block is returned. Will error if the block
1081/// has not been processed yet. If timestamp is provided, the state at the latest block before
1082/// that timestamp is returned.
1083/// Defaults to the current time.
1084#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema, Eq, Hash)]
1085#[serde(deny_unknown_fields)]
1086pub struct VersionParam {
1087    pub timestamp: Option<NaiveDateTime>,
1088    pub block: Option<BlockParam>,
1089}
1090
1091impl DeepSizeOf for VersionParam {
1092    fn deep_size_of_children(&self, ctx: &mut Context) -> usize {
1093        if let Some(block) = &self.block {
1094            return block.deep_size_of_children(ctx);
1095        }
1096
1097        0
1098    }
1099}
1100
1101impl VersionParam {
1102    pub fn new(timestamp: Option<NaiveDateTime>, block: Option<BlockParam>) -> Self {
1103        Self { timestamp, block }
1104    }
1105}
1106
1107impl Default for VersionParam {
1108    fn default() -> Self {
1109        VersionParam { timestamp: Some(Utc::now().naive_utc()), block: None }
1110    }
1111}
1112
1113#[deprecated(note = "Use StateRequestBody instead")]
1114#[derive(Serialize, Deserialize, Default, Debug, IntoParams)]
1115pub struct StateRequestParameters {
1116    /// The minimum TVL of the protocol components to return, denoted in the chain's native token.
1117    #[param(default = 0)]
1118    pub tvl_gt: Option<u64>,
1119    /// The minimum inertia of the protocol components to return.
1120    #[param(default = 0)]
1121    pub inertia_min_gt: Option<u64>,
1122    /// Whether to include ERC20 balances in the response.
1123    #[serde(default = "default_include_balances_flag")]
1124    pub include_balances: bool,
1125    #[serde(default)]
1126    pub pagination: PaginationParams,
1127}
1128
1129impl StateRequestParameters {
1130    pub fn new(include_balances: bool) -> Self {
1131        Self {
1132            tvl_gt: None,
1133            inertia_min_gt: None,
1134            include_balances,
1135            pagination: PaginationParams::default(),
1136        }
1137    }
1138
1139    pub fn to_query_string(&self) -> String {
1140        let mut parts = vec![format!("include_balances={}", self.include_balances)];
1141
1142        if let Some(tvl_gt) = self.tvl_gt {
1143            parts.push(format!("tvl_gt={tvl_gt}"));
1144        }
1145
1146        if let Some(inertia) = self.inertia_min_gt {
1147            parts.push(format!("inertia_min_gt={inertia}"));
1148        }
1149
1150        let mut res = parts.join("&");
1151        if !res.is_empty() {
1152            res = format!("?{res}");
1153        }
1154        res
1155    }
1156}
1157
1158#[derive(
1159    Serialize, Deserialize, Debug, Default, PartialEq, ToSchema, Eq, Hash, Clone, DeepSizeOf,
1160)]
1161#[serde(deny_unknown_fields)]
1162pub struct TokensRequestBody {
1163    /// Filters tokens by addresses
1164    #[serde(alias = "tokenAddresses")]
1165    #[schema(value_type=Option<Vec<String>>)]
1166    pub token_addresses: Option<Vec<Bytes>>,
1167    /// Quality is between 0-100, where:
1168    ///  - 100: Normal ERC-20 Token behavior
1169    ///  - 75: Rebasing token
1170    ///  - 50: Fee-on-transfer token
1171    ///  - 10: Token analysis failed at first detection
1172    ///  - 5: Token analysis failed multiple times (after creation)
1173    ///  - 0: Failed to extract attributes, like Decimal or Symbol
1174    #[serde(default)]
1175    pub min_quality: Option<i32>,
1176    /// Filters tokens by recent trade activity
1177    #[serde(default)]
1178    pub traded_n_days_ago: Option<u64>,
1179    /// Max page size supported is 3000
1180    #[serde(default)]
1181    pub pagination: PaginationParams,
1182    /// Filter tokens by blockchain, default 'ethereum'
1183    #[serde(default)]
1184    pub chain: Chain,
1185}
1186
1187// When INCREASING these limits, please read the warning in the macro definition.
1188impl_pagination_limits!(TokensRequestBody, compressed = 12900, uncompressed = 3000);
1189
1190/// Response from Tycho server for a tokens request.
1191#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema, Eq, Hash, DeepSizeOf)]
1192pub struct TokensRequestResponse {
1193    pub tokens: Vec<ResponseToken>,
1194    pub pagination: PaginationResponse,
1195}
1196
1197impl TokensRequestResponse {
1198    pub fn new(tokens: Vec<ResponseToken>, pagination_request: &PaginationResponse) -> Self {
1199        Self { tokens, pagination: pagination_request.clone() }
1200    }
1201}
1202
1203#[derive(
1204    PartialEq, Debug, Clone, Serialize, Deserialize, Default, ToSchema, Eq, Hash, DeepSizeOf,
1205)]
1206#[serde(rename = "Token")]
1207/// Token struct for the response from Tycho server for a tokens request.
1208pub struct ResponseToken {
1209    pub chain: Chain,
1210    /// The address of this token as hex encoded string
1211    #[schema(value_type=String, example="0xc9f2e6ea1637E499406986ac50ddC92401ce1f58")]
1212    #[serde(with = "hex_bytes")]
1213    pub address: Bytes,
1214    /// A shorthand symbol for this token (not unique)
1215    #[schema(value_type=String, example="WETH")]
1216    pub symbol: String,
1217    /// The number of decimals used to represent token values
1218    pub decimals: u32,
1219    /// The tax this token charges on transfers in basis points
1220    pub tax: u64,
1221    /// Gas usage of the token, currently is always a single averaged value
1222    pub gas: Vec<Option<u64>>,
1223    /// Quality is between 0-100, where:
1224    ///  - 100: Normal ERC-20 Token behavior
1225    ///  - 75: Rebasing token
1226    ///  - 50: Fee-on-transfer token
1227    ///  - 10: Token analysis failed at first detection
1228    ///  - 5: Token analysis failed multiple times (after creation)
1229    ///  - 0: Failed to extract attributes, like Decimal or Symbol
1230    pub quality: u32,
1231}
1232
1233impl From<models::token::Token> for ResponseToken {
1234    fn from(value: models::token::Token) -> Self {
1235        Self {
1236            chain: value.chain.into(),
1237            address: value.address,
1238            symbol: value.symbol,
1239            decimals: value.decimals,
1240            tax: value.tax,
1241            gas: value.gas,
1242            quality: value.quality,
1243        }
1244    }
1245}
1246
1247#[derive(Serialize, Deserialize, Debug, Default, ToSchema, Clone, DeepSizeOf)]
1248#[serde(deny_unknown_fields)]
1249pub struct ProtocolComponentsRequestBody {
1250    /// Filters by protocol, required to correctly apply unconfirmed state from
1251    /// ReorgBuffers
1252    pub protocol_system: String,
1253    /// Filter by component ids
1254    #[schema(value_type=Option<Vec<String>>)]
1255    #[serde(alias = "componentAddresses")]
1256    pub component_ids: Option<Vec<ComponentId>>,
1257    /// The minimum TVL of the protocol components to return, denoted in the chain's
1258    /// native token.
1259    #[serde(default)]
1260    pub tvl_gt: Option<f64>,
1261    #[serde(default)]
1262    pub chain: Chain,
1263    /// Max page size supported is 500
1264    #[serde(default)]
1265    pub pagination: PaginationParams,
1266}
1267
1268// When INCREASING these limits, please read the warning in the macro definition.
1269impl_pagination_limits!(ProtocolComponentsRequestBody, compressed = 2550, uncompressed = 500);
1270
1271// Implement PartialEq where tvl is considered equal if the difference is less than 1e-6
1272impl PartialEq for ProtocolComponentsRequestBody {
1273    fn eq(&self, other: &Self) -> bool {
1274        let tvl_close_enough = match (self.tvl_gt, other.tvl_gt) {
1275            (Some(a), Some(b)) => (a - b).abs() < 1e-6,
1276            (None, None) => true,
1277            _ => false,
1278        };
1279
1280        self.protocol_system == other.protocol_system &&
1281            self.component_ids == other.component_ids &&
1282            tvl_close_enough &&
1283            self.chain == other.chain &&
1284            self.pagination == other.pagination
1285    }
1286}
1287
1288// Implement Eq without any new logic
1289impl Eq for ProtocolComponentsRequestBody {}
1290
1291impl Hash for ProtocolComponentsRequestBody {
1292    fn hash<H: Hasher>(&self, state: &mut H) {
1293        self.protocol_system.hash(state);
1294        self.component_ids.hash(state);
1295
1296        // Handle the f64 `tvl_gt` field by converting it into a hashable integer
1297        if let Some(tvl) = self.tvl_gt {
1298            // Convert f64 to bits and hash those bits
1299            tvl.to_bits().hash(state);
1300        } else {
1301            // Use a constant value to represent None
1302            state.write_u8(0);
1303        }
1304
1305        self.chain.hash(state);
1306        self.pagination.hash(state);
1307    }
1308}
1309
1310impl ProtocolComponentsRequestBody {
1311    pub fn system_filtered(system: &str, tvl_gt: Option<f64>, chain: Chain) -> Self {
1312        Self {
1313            protocol_system: system.to_string(),
1314            component_ids: None,
1315            tvl_gt,
1316            chain,
1317            pagination: Default::default(),
1318        }
1319    }
1320
1321    pub fn id_filtered(system: &str, ids: Vec<String>, chain: Chain) -> Self {
1322        Self {
1323            protocol_system: system.to_string(),
1324            component_ids: Some(ids),
1325            tvl_gt: None,
1326            chain,
1327            pagination: Default::default(),
1328        }
1329    }
1330}
1331
1332impl ProtocolComponentsRequestBody {
1333    pub fn new(
1334        protocol_system: String,
1335        component_ids: Option<Vec<String>>,
1336        tvl_gt: Option<f64>,
1337        chain: Chain,
1338        pagination: PaginationParams,
1339    ) -> Self {
1340        Self { protocol_system, component_ids, tvl_gt, chain, pagination }
1341    }
1342}
1343
1344#[deprecated(note = "Use ProtocolComponentsRequestBody instead")]
1345#[derive(Serialize, Deserialize, Default, Debug, IntoParams)]
1346pub struct ProtocolComponentRequestParameters {
1347    /// The minimum TVL of the protocol components to return, denoted in the chain's native token.
1348    #[param(default = 0)]
1349    pub tvl_gt: Option<f64>,
1350}
1351
1352impl ProtocolComponentRequestParameters {
1353    pub fn tvl_filtered(min_tvl: f64) -> Self {
1354        Self { tvl_gt: Some(min_tvl) }
1355    }
1356}
1357
1358impl ProtocolComponentRequestParameters {
1359    pub fn to_query_string(&self) -> String {
1360        if let Some(tvl_gt) = self.tvl_gt {
1361            return format!("?tvl_gt={tvl_gt}");
1362        }
1363        String::new()
1364    }
1365}
1366
1367/// Response from Tycho server for a protocol components request.
1368#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema, DeepSizeOf)]
1369pub struct ProtocolComponentRequestResponse {
1370    pub protocol_components: Vec<ProtocolComponent>,
1371    pub pagination: PaginationResponse,
1372}
1373
1374impl ProtocolComponentRequestResponse {
1375    pub fn new(
1376        protocol_components: Vec<ProtocolComponent>,
1377        pagination: PaginationResponse,
1378    ) -> Self {
1379        Self { protocol_components, pagination }
1380    }
1381}
1382
1383#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema, Eq, Hash)]
1384#[serde(deny_unknown_fields)]
1385#[deprecated]
1386pub struct ProtocolId {
1387    pub id: String,
1388    pub chain: Chain,
1389}
1390
1391impl From<ProtocolId> for String {
1392    fn from(protocol_id: ProtocolId) -> Self {
1393        protocol_id.id
1394    }
1395}
1396
1397impl AsRef<str> for ProtocolId {
1398    fn as_ref(&self) -> &str {
1399        &self.id
1400    }
1401}
1402
1403/// Protocol State struct for the response from Tycho server for a protocol state request.
1404#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize, ToSchema, DeepSizeOf)]
1405pub struct ResponseProtocolState {
1406    /// Component id this state belongs to
1407    pub component_id: String,
1408    /// Attributes of the component. If an attribute's value is a `bigint`,
1409    /// it will be encoded as a big endian signed hex string.
1410    #[schema(value_type=HashMap<String, String>)]
1411    #[serde(with = "hex_hashmap_value")]
1412    pub attributes: HashMap<String, Bytes>,
1413    /// Sum aggregated balances of the component
1414    #[schema(value_type=HashMap<String, String>)]
1415    #[serde(with = "hex_hashmap_key_value")]
1416    pub balances: HashMap<Bytes, Bytes>,
1417}
1418
1419impl From<models::protocol::ProtocolComponentState> for ResponseProtocolState {
1420    fn from(value: models::protocol::ProtocolComponentState) -> Self {
1421        Self {
1422            component_id: value.component_id,
1423            attributes: value.attributes,
1424            balances: value.balances,
1425        }
1426    }
1427}
1428
1429fn default_include_balances_flag() -> bool {
1430    true
1431}
1432
1433/// Max page size supported is 100
1434#[derive(Clone, Debug, Serialize, PartialEq, ToSchema, Default, Eq, Hash, DeepSizeOf)]
1435#[serde(deny_unknown_fields)]
1436pub struct ProtocolStateRequestBody {
1437    /// Filters response by protocol components ids
1438    #[serde(alias = "protocolIds")]
1439    pub protocol_ids: Option<Vec<String>>,
1440    /// Filters by protocol, required to correctly apply unconfirmed state from
1441    /// ReorgBuffers
1442    #[serde(alias = "protocolSystem")]
1443    pub protocol_system: String,
1444    #[serde(default)]
1445    pub chain: Chain,
1446    /// Whether to include account balances in the response. Defaults to true.
1447    #[serde(default = "default_include_balances_flag")]
1448    pub include_balances: bool,
1449    #[serde(default = "VersionParam::default")]
1450    pub version: VersionParam,
1451    #[serde(default)]
1452    pub pagination: PaginationParams,
1453}
1454
1455// When INCREASING these limits, please read the warning in the macro definition.
1456impl_pagination_limits!(ProtocolStateRequestBody, compressed = 360, uncompressed = 100);
1457
1458impl ProtocolStateRequestBody {
1459    pub fn id_filtered<I, T>(ids: I) -> Self
1460    where
1461        I: IntoIterator<Item = T>,
1462        T: Into<String>,
1463    {
1464        Self {
1465            protocol_ids: Some(
1466                ids.into_iter()
1467                    .map(Into::into)
1468                    .collect(),
1469            ),
1470            ..Default::default()
1471        }
1472    }
1473}
1474
1475/// Custom deserializer for ProtocolStateRequestBody to support backwards compatibility with the old
1476/// ProtocolIds format.
1477/// To be removed when the old format is no longer supported.
1478impl<'de> Deserialize<'de> for ProtocolStateRequestBody {
1479    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1480    where
1481        D: Deserializer<'de>,
1482    {
1483        #[derive(Deserialize)]
1484        #[serde(untagged)]
1485        enum ProtocolIdOrString {
1486            Old(Vec<ProtocolId>),
1487            New(Vec<String>),
1488        }
1489
1490        struct ProtocolStateRequestBodyVisitor;
1491
1492        impl<'de> de::Visitor<'de> for ProtocolStateRequestBodyVisitor {
1493            type Value = ProtocolStateRequestBody;
1494
1495            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
1496                formatter.write_str("struct ProtocolStateRequestBody")
1497            }
1498
1499            fn visit_map<V>(self, mut map: V) -> Result<ProtocolStateRequestBody, V::Error>
1500            where
1501                V: de::MapAccess<'de>,
1502            {
1503                let mut protocol_ids = None;
1504                let mut protocol_system = None;
1505                let mut version = None;
1506                let mut chain = None;
1507                let mut include_balances = None;
1508                let mut pagination = None;
1509
1510                while let Some(key) = map.next_key::<String>()? {
1511                    match key.as_str() {
1512                        "protocol_ids" | "protocolIds" => {
1513                            let value: ProtocolIdOrString = map.next_value()?;
1514                            protocol_ids = match value {
1515                                ProtocolIdOrString::Old(ids) => {
1516                                    Some(ids.into_iter().map(|p| p.id).collect())
1517                                }
1518                                ProtocolIdOrString::New(ids_str) => Some(ids_str),
1519                            };
1520                        }
1521                        "protocol_system" | "protocolSystem" => {
1522                            protocol_system = Some(map.next_value()?);
1523                        }
1524                        "version" => {
1525                            version = Some(map.next_value()?);
1526                        }
1527                        "chain" => {
1528                            chain = Some(map.next_value()?);
1529                        }
1530                        "include_balances" => {
1531                            include_balances = Some(map.next_value()?);
1532                        }
1533                        "pagination" => {
1534                            pagination = Some(map.next_value()?);
1535                        }
1536                        _ => {
1537                            return Err(de::Error::unknown_field(
1538                                &key,
1539                                &[
1540                                    "contract_ids",
1541                                    "protocol_system",
1542                                    "version",
1543                                    "chain",
1544                                    "include_balances",
1545                                    "pagination",
1546                                ],
1547                            ))
1548                        }
1549                    }
1550                }
1551
1552                Ok(ProtocolStateRequestBody {
1553                    protocol_ids,
1554                    protocol_system: protocol_system.unwrap_or_default(),
1555                    version: version.unwrap_or_else(VersionParam::default),
1556                    chain: chain.unwrap_or_else(Chain::default),
1557                    include_balances: include_balances.unwrap_or(true),
1558                    pagination: pagination.unwrap_or_else(PaginationParams::default),
1559                })
1560            }
1561        }
1562
1563        deserializer.deserialize_struct(
1564            "ProtocolStateRequestBody",
1565            &[
1566                "contract_ids",
1567                "protocol_system",
1568                "version",
1569                "chain",
1570                "include_balances",
1571                "pagination",
1572            ],
1573            ProtocolStateRequestBodyVisitor,
1574        )
1575    }
1576}
1577
1578#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema, DeepSizeOf)]
1579pub struct ProtocolStateRequestResponse {
1580    pub states: Vec<ResponseProtocolState>,
1581    pub pagination: PaginationResponse,
1582}
1583
1584impl ProtocolStateRequestResponse {
1585    pub fn new(states: Vec<ResponseProtocolState>, pagination: PaginationResponse) -> Self {
1586        Self { states, pagination }
1587    }
1588}
1589
1590#[derive(Serialize, Clone, PartialEq, Hash, Eq)]
1591pub struct ProtocolComponentId {
1592    pub chain: Chain,
1593    pub system: String,
1594    pub id: String,
1595}
1596
1597#[derive(Debug, Serialize, ToSchema)]
1598#[serde(tag = "status", content = "message")]
1599#[schema(example = json!({"status": "NotReady", "message": "No db connection"}))]
1600pub enum Health {
1601    Ready,
1602    Starting(String),
1603    NotReady(String),
1604}
1605
1606#[derive(Serialize, Deserialize, Debug, Default, PartialEq, ToSchema, Eq, Hash, Clone)]
1607#[serde(deny_unknown_fields)]
1608pub struct ProtocolSystemsRequestBody {
1609    #[serde(default)]
1610    pub chain: Chain,
1611    #[serde(default)]
1612    pub pagination: PaginationParams,
1613}
1614
1615// When INCREASING these limits, please read the warning in the macro definition.
1616impl_pagination_limits!(ProtocolSystemsRequestBody, compressed = 100, uncompressed = 100);
1617
1618#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema, Eq, Hash)]
1619pub struct ProtocolSystemsRequestResponse {
1620    /// List of currently supported protocol systems
1621    pub protocol_systems: Vec<String>,
1622    pub pagination: PaginationResponse,
1623}
1624
1625impl ProtocolSystemsRequestResponse {
1626    pub fn new(protocol_systems: Vec<String>, pagination: PaginationResponse) -> Self {
1627        Self { protocol_systems, pagination }
1628    }
1629}
1630
1631#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
1632pub struct DCIUpdate {
1633    /// Map of component id to the new entrypoints associated with the component
1634    pub new_entrypoints: HashMap<ComponentId, HashSet<EntryPoint>>,
1635    /// Map of entrypoint id to the new entrypoint params associtated with it (and optionally the
1636    /// component linked to those params)
1637    pub new_entrypoint_params: HashMap<String, HashSet<(TracingParams, String)>>,
1638    /// Map of entrypoint id to its trace result
1639    pub trace_results: HashMap<String, TracingResult>,
1640}
1641
1642impl From<models::blockchain::DCIUpdate> for DCIUpdate {
1643    fn from(value: models::blockchain::DCIUpdate) -> Self {
1644        Self {
1645            new_entrypoints: value
1646                .new_entrypoints
1647                .into_iter()
1648                .map(|(k, v)| {
1649                    (
1650                        k,
1651                        v.into_iter()
1652                            .map(|v| v.into())
1653                            .collect(),
1654                    )
1655                })
1656                .collect(),
1657            new_entrypoint_params: value
1658                .new_entrypoint_params
1659                .into_iter()
1660                .map(|(k, v)| {
1661                    (
1662                        k,
1663                        v.into_iter()
1664                            .map(|(params, i)| (params.into(), i))
1665                            .collect(),
1666                    )
1667                })
1668                .collect(),
1669            trace_results: value
1670                .trace_results
1671                .into_iter()
1672                .map(|(k, v)| (k, v.into()))
1673                .collect(),
1674        }
1675    }
1676}
1677
1678#[derive(Serialize, Deserialize, Debug, Default, PartialEq, ToSchema, Eq, Hash, Clone)]
1679#[serde(deny_unknown_fields)]
1680pub struct ComponentTvlRequestBody {
1681    #[serde(default)]
1682    pub chain: Chain,
1683    /// Filters protocol components by protocol system
1684    /// Useful when `component_ids` is omitted to fetch all components under a specific system.
1685    #[serde(alias = "protocolSystem")]
1686    pub protocol_system: Option<String>,
1687    #[serde(default)]
1688    pub component_ids: Option<Vec<String>>,
1689    #[serde(default)]
1690    pub pagination: PaginationParams,
1691}
1692
1693// When INCREASING these limits, please read the warning in the macro definition.
1694impl_pagination_limits!(ComponentTvlRequestBody, compressed = 100, uncompressed = 100);
1695
1696impl ComponentTvlRequestBody {
1697    pub fn system_filtered(system: &str, chain: Chain) -> Self {
1698        Self {
1699            chain,
1700            protocol_system: Some(system.to_string()),
1701            component_ids: None,
1702            pagination: Default::default(),
1703        }
1704    }
1705
1706    pub fn id_filtered(ids: Vec<String>, chain: Chain) -> Self {
1707        Self {
1708            chain,
1709            protocol_system: None,
1710            component_ids: Some(ids),
1711            pagination: Default::default(),
1712        }
1713    }
1714}
1715// #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema, Eq, Hash)]
1716#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema)]
1717pub struct ComponentTvlRequestResponse {
1718    pub tvl: HashMap<String, f64>,
1719    pub pagination: PaginationResponse,
1720}
1721
1722impl ComponentTvlRequestResponse {
1723    pub fn new(tvl: HashMap<String, f64>, pagination: PaginationResponse) -> Self {
1724        Self { tvl, pagination }
1725    }
1726}
1727
1728#[derive(
1729    Serialize, Deserialize, Debug, Default, PartialEq, ToSchema, Eq, Hash, Clone, DeepSizeOf,
1730)]
1731pub struct TracedEntryPointRequestBody {
1732    #[serde(default)]
1733    pub chain: Chain,
1734    /// Filters by protocol, required to correctly apply unconfirmed state from
1735    /// ReorgBuffers
1736    pub protocol_system: String,
1737    /// Filter by component ids
1738    #[schema(value_type = Option<Vec<String>>)]
1739    pub component_ids: Option<Vec<ComponentId>>,
1740    /// Max page size supported is 100
1741    #[serde(default)]
1742    pub pagination: PaginationParams,
1743}
1744
1745// When INCREASING these limits, please read the warning in the macro definition.
1746impl_pagination_limits!(TracedEntryPointRequestBody, compressed = 100, uncompressed = 100);
1747
1748#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema, Eq, Hash, DeepSizeOf)]
1749pub struct EntryPoint {
1750    #[schema(example = "0xEdf63cce4bA70cbE74064b7687882E71ebB0e988:getRate()")]
1751    /// Entry point id.
1752    pub external_id: String,
1753    #[schema(value_type=String, example="0x8f4E8439b970363648421C692dd897Fb9c0Bd1D9")]
1754    #[serde(with = "hex_bytes")]
1755    /// The address of the contract to trace.
1756    pub target: Bytes,
1757    #[schema(example = "getRate()")]
1758    /// The signature of the function to trace.
1759    pub signature: String,
1760}
1761
1762#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema, Eq, Hash, DeepSizeOf)]
1763pub enum StorageOverride {
1764    /// Applies changes incrementally to the existing account storage.
1765    /// Only modifies the specific storage slots provided in the map while
1766    /// preserving all other storage slots.
1767    #[schema(value_type=HashMap<String, String>)]
1768    Diff(BTreeMap<StoreKey, StoreVal>),
1769
1770    /// Completely replaces the account's storage state.
1771    /// Only the storage slots provided in the map will exist after the operation,
1772    /// and any existing storage slots not included will be cleared/zeroed.
1773    #[schema(value_type=HashMap<String, String>)]
1774    Replace(BTreeMap<StoreKey, StoreVal>),
1775}
1776
1777impl From<models::blockchain::StorageOverride> for StorageOverride {
1778    fn from(value: models::blockchain::StorageOverride) -> Self {
1779        match value {
1780            models::blockchain::StorageOverride::Diff(diff) => StorageOverride::Diff(diff),
1781            models::blockchain::StorageOverride::Replace(replace) => {
1782                StorageOverride::Replace(replace)
1783            }
1784        }
1785    }
1786}
1787
1788/// State overrides for an account.
1789///
1790/// Used to modify account state. Commonly used for testing contract interactions with specific
1791/// state conditions or simulating transactions with modified balances/code.
1792#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema, Eq, Hash, DeepSizeOf)]
1793pub struct AccountOverrides {
1794    /// Storage slots to override
1795    pub slots: Option<StorageOverride>,
1796    #[schema(value_type=Option<String>)]
1797    /// Native token balance override
1798    pub native_balance: Option<Balance>,
1799    #[schema(value_type=Option<String>)]
1800    /// Contract code override
1801    pub code: Option<Code>,
1802}
1803
1804impl From<models::blockchain::AccountOverrides> for AccountOverrides {
1805    fn from(value: models::blockchain::AccountOverrides) -> Self {
1806        AccountOverrides {
1807            slots: value.slots.map(|s| s.into()),
1808            native_balance: value.native_balance,
1809            code: value.code,
1810        }
1811    }
1812}
1813
1814#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema, Eq, Hash, DeepSizeOf)]
1815pub struct RPCTracerParams {
1816    /// The caller address of the transaction, if not provided tracing uses the default value
1817    /// for an address defined by the VM.
1818    #[schema(value_type=Option<String>)]
1819    #[serde(with = "hex_bytes_option", default)]
1820    pub caller: Option<Bytes>,
1821    /// The call data used for the tracing call, this needs to include the function selector
1822    #[schema(value_type=String, example="0x679aefce")]
1823    #[serde(with = "hex_bytes")]
1824    pub calldata: Bytes,
1825    /// Optionally allow for state overrides so that the call works as expected
1826    pub state_overrides: Option<BTreeMap<Address, AccountOverrides>>,
1827    /// Addresses to prune from trace results. Useful for hooks that use mock
1828    /// accounts/routers that shouldn't be tracked in the final DCI results.
1829    #[schema(value_type=Option<Vec<String>>)]
1830    #[serde(default)]
1831    pub prune_addresses: Option<Vec<Address>>,
1832}
1833
1834impl From<models::blockchain::RPCTracerParams> for RPCTracerParams {
1835    fn from(value: models::blockchain::RPCTracerParams) -> Self {
1836        RPCTracerParams {
1837            caller: value.caller,
1838            calldata: value.calldata,
1839            state_overrides: value.state_overrides.map(|overrides| {
1840                overrides
1841                    .into_iter()
1842                    .map(|(address, account_overrides)| (address, account_overrides.into()))
1843                    .collect()
1844            }),
1845            prune_addresses: value.prune_addresses,
1846        }
1847    }
1848}
1849
1850#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone, Hash, DeepSizeOf, ToSchema)]
1851#[serde(tag = "method", rename_all = "lowercase")]
1852pub enum TracingParams {
1853    /// Uses RPC calls to retrieve the called addresses and retriggers
1854    RPCTracer(RPCTracerParams),
1855}
1856
1857impl From<models::blockchain::TracingParams> for TracingParams {
1858    fn from(value: models::blockchain::TracingParams) -> Self {
1859        match value {
1860            models::blockchain::TracingParams::RPCTracer(params) => {
1861                TracingParams::RPCTracer(params.into())
1862            }
1863        }
1864    }
1865}
1866
1867impl From<models::blockchain::EntryPoint> for EntryPoint {
1868    fn from(value: models::blockchain::EntryPoint) -> Self {
1869        Self { external_id: value.external_id, target: value.target, signature: value.signature }
1870    }
1871}
1872
1873#[derive(Serialize, Deserialize, Debug, PartialEq, ToSchema, Eq, Clone, DeepSizeOf)]
1874pub struct EntryPointWithTracingParams {
1875    /// The entry point object
1876    pub entry_point: EntryPoint,
1877    /// The parameters used
1878    pub params: TracingParams,
1879}
1880
1881impl From<models::blockchain::EntryPointWithTracingParams> for EntryPointWithTracingParams {
1882    fn from(value: models::blockchain::EntryPointWithTracingParams) -> Self {
1883        Self { entry_point: value.entry_point.into(), params: value.params.into() }
1884    }
1885}
1886
1887#[derive(
1888    Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash, Serialize, Deserialize, DeepSizeOf,
1889)]
1890pub struct AddressStorageLocation {
1891    pub key: StoreKey,
1892    pub offset: u8,
1893}
1894
1895impl AddressStorageLocation {
1896    pub fn new(key: StoreKey, offset: u8) -> Self {
1897        Self { key, offset }
1898    }
1899}
1900
1901impl From<models::blockchain::AddressStorageLocation> for AddressStorageLocation {
1902    fn from(value: models::blockchain::AddressStorageLocation) -> Self {
1903        Self { key: value.key, offset: value.offset }
1904    }
1905}
1906
1907fn deserialize_retriggers_from_value(
1908    value: &serde_json::Value,
1909) -> Result<HashSet<(StoreKey, AddressStorageLocation)>, String> {
1910    use serde::Deserialize;
1911    use serde_json::Value;
1912
1913    let mut result = HashSet::new();
1914
1915    if let Value::Array(items) = value {
1916        for item in items {
1917            if let Value::Array(pair) = item {
1918                if pair.len() == 2 {
1919                    let key = StoreKey::deserialize(&pair[0])
1920                        .map_err(|e| format!("Failed to deserialize key: {}", e))?;
1921
1922                    // Handle both old format (string) and new format (AddressStorageLocation)
1923                    let addr_storage = match &pair[1] {
1924                        Value::String(_) => {
1925                            // Old format: just a string key with offset defaulted to 0
1926                            let storage_key = StoreKey::deserialize(&pair[1]).map_err(|e| {
1927                                format!("Failed to deserialize old format storage key: {}", e)
1928                            })?;
1929                            AddressStorageLocation::new(storage_key, 12)
1930                        }
1931                        Value::Object(_) => {
1932                            // New format: AddressStorageLocation struct
1933                            AddressStorageLocation::deserialize(&pair[1]).map_err(|e| {
1934                                format!("Failed to deserialize AddressStorageLocation: {}", e)
1935                            })?
1936                        }
1937                        _ => return Err("Invalid retrigger format".to_string()),
1938                    };
1939
1940                    result.insert((key, addr_storage));
1941                }
1942            }
1943        }
1944    }
1945
1946    Ok(result)
1947}
1948
1949#[derive(Serialize, Debug, Default, PartialEq, ToSchema, Eq, Clone, DeepSizeOf)]
1950pub struct TracingResult {
1951    #[schema(value_type=HashSet<(String, String)>)]
1952    pub retriggers: HashSet<(StoreKey, AddressStorageLocation)>,
1953    #[schema(value_type=HashMap<String,HashSet<String>>)]
1954    pub accessed_slots: HashMap<Address, HashSet<StoreKey>>,
1955}
1956
1957/// Deserialize TracingResult with backward compatibility for retriggers
1958/// TODO: remove this after offset detection is deployed in production
1959impl<'de> Deserialize<'de> for TracingResult {
1960    fn deserialize<D>(deserializer: D) -> Result<TracingResult, D::Error>
1961    where
1962        D: Deserializer<'de>,
1963    {
1964        use serde::de::Error;
1965        use serde_json::Value;
1966
1967        let value = Value::deserialize(deserializer)?;
1968        let mut result = TracingResult::default();
1969
1970        if let Value::Object(map) = value {
1971            // Deserialize retriggers using our custom deserializer
1972            if let Some(retriggers_value) = map.get("retriggers") {
1973                result.retriggers =
1974                    deserialize_retriggers_from_value(retriggers_value).map_err(|e| {
1975                        D::Error::custom(format!("Failed to deserialize retriggers: {}", e))
1976                    })?;
1977            }
1978
1979            // Deserialize accessed_slots normally
1980            if let Some(accessed_slots_value) = map.get("accessed_slots") {
1981                result.accessed_slots = serde_json::from_value(accessed_slots_value.clone())
1982                    .map_err(|e| {
1983                        D::Error::custom(format!("Failed to deserialize accessed_slots: {}", e))
1984                    })?;
1985            }
1986        }
1987
1988        Ok(result)
1989    }
1990}
1991
1992impl From<models::blockchain::TracingResult> for TracingResult {
1993    fn from(value: models::blockchain::TracingResult) -> Self {
1994        TracingResult {
1995            retriggers: value
1996                .retriggers
1997                .into_iter()
1998                .map(|(k, v)| (k, v.into()))
1999                .collect(),
2000            accessed_slots: value.accessed_slots,
2001        }
2002    }
2003}
2004
2005#[derive(Serialize, PartialEq, ToSchema, Eq, Clone, Debug, Deserialize, DeepSizeOf)]
2006pub struct TracedEntryPointRequestResponse {
2007    /// Map of protocol component id to a list of a tuple containing each entry point with its
2008    /// tracing parameters and its corresponding tracing results.
2009    #[schema(value_type = HashMap<String, Vec<(EntryPointWithTracingParams, TracingResult)>>)]
2010    pub traced_entry_points:
2011        HashMap<ComponentId, Vec<(EntryPointWithTracingParams, TracingResult)>>,
2012    pub pagination: PaginationResponse,
2013}
2014impl From<TracedEntryPointRequestResponse> for DCIUpdate {
2015    fn from(response: TracedEntryPointRequestResponse) -> Self {
2016        let mut new_entrypoints = HashMap::new();
2017        let mut new_entrypoint_params = HashMap::new();
2018        let mut trace_results = HashMap::new();
2019
2020        for (component, traces) in response.traced_entry_points {
2021            let mut entrypoints = HashSet::new();
2022
2023            for (entrypoint, trace) in traces {
2024                let entrypoint_id = entrypoint
2025                    .entry_point
2026                    .external_id
2027                    .clone();
2028
2029                // Collect entrypoints
2030                entrypoints.insert(entrypoint.entry_point.clone());
2031
2032                // Collect entrypoint params
2033                new_entrypoint_params
2034                    .entry(entrypoint_id.clone())
2035                    .or_insert_with(HashSet::new)
2036                    .insert((entrypoint.params, component.clone()));
2037
2038                // Collect trace results
2039                trace_results
2040                    .entry(entrypoint_id)
2041                    .and_modify(|existing_trace: &mut TracingResult| {
2042                        // Merge traces for the same entrypoint
2043                        existing_trace
2044                            .retriggers
2045                            .extend(trace.retriggers.clone());
2046                        for (address, slots) in trace.accessed_slots.clone() {
2047                            existing_trace
2048                                .accessed_slots
2049                                .entry(address)
2050                                .or_default()
2051                                .extend(slots);
2052                        }
2053                    })
2054                    .or_insert(trace);
2055            }
2056
2057            if !entrypoints.is_empty() {
2058                new_entrypoints.insert(component, entrypoints);
2059            }
2060        }
2061
2062        DCIUpdate { new_entrypoints, new_entrypoint_params, trace_results }
2063    }
2064}
2065
2066#[derive(Serialize, Deserialize, Debug, Default, PartialEq, ToSchema, Eq, Clone)]
2067pub struct AddEntryPointRequestBody {
2068    #[serde(default)]
2069    pub chain: Chain,
2070    #[schema(value_type=String)]
2071    #[serde(default)]
2072    pub block_hash: Bytes,
2073    /// The map of component ids to their tracing params to insert
2074    #[schema(value_type = Vec<(String, Vec<EntryPointWithTracingParams>)>)]
2075    pub entry_points_with_tracing_data: Vec<(ComponentId, Vec<EntryPointWithTracingParams>)>,
2076}
2077
2078#[derive(Serialize, PartialEq, ToSchema, Eq, Clone, Debug, Deserialize)]
2079pub struct AddEntryPointRequestResponse {
2080    /// Map of protocol component id to a list of a tuple containing each entry point with its
2081    /// tracing parameters and its corresponding tracing results.
2082    #[schema(value_type = HashMap<String, Vec<(EntryPointWithTracingParams, TracingResult)>>)]
2083    pub traced_entry_points:
2084        HashMap<ComponentId, Vec<(EntryPointWithTracingParams, TracingResult)>>,
2085}
2086
2087#[cfg(test)]
2088mod test {
2089    use std::str::FromStr;
2090
2091    use maplit::hashmap;
2092    use rstest::rstest;
2093
2094    use super::*;
2095
2096    /// Test backward compatibility for Command::Subscribe compression field.
2097    /// Should default to false when not specified.
2098    #[rstest]
2099    #[case::legacy_format(None, false)]
2100    #[case::explicit_true(Some(true), true)]
2101    #[case::explicit_false(Some(false), false)]
2102    fn test_subscribe_compression_backward_compatibility(
2103        #[case] compression: Option<bool>,
2104        #[case] expected: bool,
2105    ) {
2106        use serde_json::json;
2107
2108        let mut json_value = json!({
2109            "method": "subscribe",
2110            "extractor_id": {
2111                "chain": "ethereum",
2112                "name": "test"
2113            },
2114            "include_state": true
2115        });
2116
2117        if let Some(value) = compression {
2118            json_value["compression"] = json!(value);
2119        }
2120
2121        let command: Command =
2122            serde_json::from_value(json_value).expect("Failed to deserialize Subscribe command");
2123
2124        if let Command::Subscribe { compression, .. } = command {
2125            assert_eq!(compression, expected);
2126        } else {
2127            panic!("Expected Subscribe command");
2128        }
2129    }
2130
2131    /// Test backward compatibility for Command::Subscribe partial_blocks field.
2132    /// Should default to false when not specified.
2133    #[rstest]
2134    #[case::legacy_format(None, false)]
2135    #[case::explicit_true(Some(true), true)]
2136    #[case::explicit_false(Some(false), false)]
2137    fn test_subscribe_partial_blocks_backward_compatibility(
2138        #[case] partial_blocks: Option<bool>,
2139        #[case] expected: bool,
2140    ) {
2141        use serde_json::json;
2142
2143        let mut json_value = json!({
2144            "method": "subscribe",
2145            "extractor_id": {
2146                "chain": "ethereum",
2147                "name": "test"
2148            },
2149            "include_state": true
2150        });
2151
2152        if let Some(value) = partial_blocks {
2153            json_value["partial_blocks"] = json!(value);
2154        }
2155
2156        let command: Command =
2157            serde_json::from_value(json_value).expect("Failed to deserialize Subscribe command");
2158
2159        if let Command::Subscribe { partial_blocks, .. } = command {
2160            assert_eq!(partial_blocks, expected);
2161        } else {
2162            panic!("Expected Subscribe command");
2163        }
2164    }
2165
2166    #[test]
2167    fn test_tracing_result_backward_compatibility() {
2168        use serde_json::json;
2169
2170        // Test old format (string storage locations)
2171        let old_format_json = json!({
2172            "retriggers": [
2173                ["0x01", "0x02"],
2174                ["0x03", "0x04"]
2175            ],
2176            "accessed_slots": {
2177                "0x05": ["0x06", "0x07"]
2178            }
2179        });
2180
2181        let result: TracingResult = serde_json::from_value(old_format_json).unwrap();
2182
2183        // Check that retriggers were deserialized correctly with offset 0
2184        assert_eq!(result.retriggers.len(), 2);
2185        let retriggers_vec: Vec<_> = result.retriggers.iter().collect();
2186        assert!(retriggers_vec.iter().any(|(k, v)| {
2187            k == &Bytes::from("0x01") && v.key == Bytes::from("0x02") && v.offset == 12
2188        }));
2189        assert!(retriggers_vec.iter().any(|(k, v)| {
2190            k == &Bytes::from("0x03") && v.key == Bytes::from("0x04") && v.offset == 12
2191        }));
2192
2193        // Test new format (AddressStorageLocation objects)
2194        let new_format_json = json!({
2195            "retriggers": [
2196                ["0x01", {"key": "0x02", "offset": 12}],
2197                ["0x03", {"key": "0x04", "offset": 5}]
2198            ],
2199            "accessed_slots": {
2200                "0x05": ["0x06", "0x07"]
2201            }
2202        });
2203
2204        let result2: TracingResult = serde_json::from_value(new_format_json).unwrap();
2205
2206        // Check that new format retriggers were deserialized correctly with proper offsets
2207        assert_eq!(result2.retriggers.len(), 2);
2208        let retriggers_vec2: Vec<_> = result2.retriggers.iter().collect();
2209        assert!(retriggers_vec2.iter().any(|(k, v)| {
2210            k == &Bytes::from("0x01") && v.key == Bytes::from("0x02") && v.offset == 12
2211        }));
2212        assert!(retriggers_vec2.iter().any(|(k, v)| {
2213            k == &Bytes::from("0x03") && v.key == Bytes::from("0x04") && v.offset == 5
2214        }));
2215    }
2216
2217    #[rstest]
2218    #[case::legacy_format(None, None)]
2219    #[case::full_block(Some(None), None)]
2220    #[case::partial_block(Some(Some(1)), Some(1))]
2221    fn test_block_changes_is_partial_backward_compatibility(
2222        #[case] has_partial_value: Option<Option<u32>>,
2223        #[case] expected: Option<u32>,
2224    ) {
2225        use serde_json::json;
2226
2227        let mut json_value = json!({
2228            "extractor": "test_extractor",
2229            "chain": "ethereum",
2230            "block": {
2231                "number": 100,
2232                "hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
2233                "parent_hash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
2234                "chain": "ethereum",
2235                "ts": "2024-01-01T00:00:00"
2236            },
2237            "finalized_block_height": 99,
2238            "revert": false,
2239            "new_tokens": {},
2240            "account_updates": {},
2241            "state_updates": {},
2242            "new_protocol_components": {},
2243            "deleted_protocol_components": {},
2244            "component_balances": {},
2245            "account_balances": {},
2246            "component_tvl": {},
2247            "dci_update": {
2248                "new_entrypoints": {},
2249                "new_entrypoint_params": {},
2250                "trace_results": {}
2251            }
2252        });
2253
2254        // Add is_partial field only if specified
2255        if let Some(partial_value) = has_partial_value {
2256            json_value["partial_block_index"] = json!(partial_value);
2257        }
2258
2259        let block_changes: BlockChanges =
2260            serde_json::from_value(json_value).expect("Failed to deserialize BlockChanges");
2261
2262        assert_eq!(block_changes.partial_block_index, expected);
2263    }
2264
2265    #[test]
2266    fn test_protocol_components_equality() {
2267        let body1 = ProtocolComponentsRequestBody {
2268            protocol_system: "protocol1".to_string(),
2269            component_ids: Some(vec!["component1".to_string(), "component2".to_string()]),
2270            tvl_gt: Some(1000.0),
2271            chain: Chain::Ethereum,
2272            pagination: PaginationParams::default(),
2273        };
2274
2275        let body2 = ProtocolComponentsRequestBody {
2276            protocol_system: "protocol1".to_string(),
2277            component_ids: Some(vec!["component1".to_string(), "component2".to_string()]),
2278            tvl_gt: Some(1000.0 + 1e-7), // Within the tolerance ±1e-6
2279            chain: Chain::Ethereum,
2280            pagination: PaginationParams::default(),
2281        };
2282
2283        // These should be considered equal due to the tolerance in tvl_gt
2284        assert_eq!(body1, body2);
2285    }
2286
2287    #[test]
2288    fn test_protocol_components_inequality() {
2289        let body1 = ProtocolComponentsRequestBody {
2290            protocol_system: "protocol1".to_string(),
2291            component_ids: Some(vec!["component1".to_string(), "component2".to_string()]),
2292            tvl_gt: Some(1000.0),
2293            chain: Chain::Ethereum,
2294            pagination: PaginationParams::default(),
2295        };
2296
2297        let body2 = ProtocolComponentsRequestBody {
2298            protocol_system: "protocol1".to_string(),
2299            component_ids: Some(vec!["component1".to_string(), "component2".to_string()]),
2300            tvl_gt: Some(1000.0 + 1e-5), // Outside the tolerance ±1e-6
2301            chain: Chain::Ethereum,
2302            pagination: PaginationParams::default(),
2303        };
2304
2305        // These should not be equal due to the difference in tvl_gt
2306        assert_ne!(body1, body2);
2307    }
2308
2309    #[test]
2310    fn test_parse_state_request() {
2311        let json_str = r#"
2312    {
2313        "contractIds": [
2314            "0xb4eccE46b8D4e4abFd03C9B806276A6735C9c092"
2315        ],
2316        "protocol_system": "uniswap_v2",
2317        "version": {
2318            "timestamp": "2069-01-01T04:20:00",
2319            "block": {
2320                "hash": "0x24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4",
2321                "number": 213,
2322                "chain": "ethereum"
2323            }
2324        }
2325    }
2326    "#;
2327
2328        let result: StateRequestBody = serde_json::from_str(json_str).unwrap();
2329
2330        let contract0 = "b4eccE46b8D4e4abFd03C9B806276A6735C9c092"
2331            .parse()
2332            .unwrap();
2333        let block_hash = "24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4"
2334            .parse()
2335            .unwrap();
2336        let block_number = 213;
2337
2338        let expected_timestamp =
2339            NaiveDateTime::parse_from_str("2069-01-01T04:20:00", "%Y-%m-%dT%H:%M:%S").unwrap();
2340
2341        let expected = StateRequestBody {
2342            contract_ids: Some(vec![contract0]),
2343            protocol_system: "uniswap_v2".to_string(),
2344            version: VersionParam {
2345                timestamp: Some(expected_timestamp),
2346                block: Some(BlockParam {
2347                    hash: Some(block_hash),
2348                    chain: Some(Chain::Ethereum),
2349                    number: Some(block_number),
2350                }),
2351            },
2352            chain: Chain::Ethereum,
2353            pagination: PaginationParams::default(),
2354        };
2355
2356        assert_eq!(result, expected);
2357    }
2358
2359    #[test]
2360    fn test_parse_state_request_dual_interface() {
2361        let json_common = r#"
2362    {
2363        "__CONTRACT_IDS__": [
2364            "0xb4eccE46b8D4e4abFd03C9B806276A6735C9c092"
2365        ],
2366        "version": {
2367            "timestamp": "2069-01-01T04:20:00",
2368            "block": {
2369                "hash": "0x24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4",
2370                "number": 213,
2371                "chain": "ethereum"
2372            }
2373        }
2374    }
2375    "#;
2376
2377        let json_str_snake = json_common.replace("\"__CONTRACT_IDS__\"", "\"contract_ids\"");
2378        let json_str_camel = json_common.replace("\"__CONTRACT_IDS__\"", "\"contractIds\"");
2379
2380        let snake: StateRequestBody = serde_json::from_str(&json_str_snake).unwrap();
2381        let camel: StateRequestBody = serde_json::from_str(&json_str_camel).unwrap();
2382
2383        assert_eq!(snake, camel);
2384    }
2385
2386    #[test]
2387    fn test_parse_state_request_unknown_field() {
2388        let body = r#"
2389    {
2390        "contract_ids_with_typo_error": [
2391            {
2392                "address": "0xb4eccE46b8D4e4abFd03C9B806276A6735C9c092",
2393                "chain": "ethereum"
2394            }
2395        ],
2396        "version": {
2397            "timestamp": "2069-01-01T04:20:00",
2398            "block": {
2399                "hash": "0x24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4",
2400                "parentHash": "0x8d75152454e60413efe758cc424bfd339897062d7e658f302765eb7b50971815",
2401                "number": 213,
2402                "chain": "ethereum"
2403            }
2404        }
2405    }
2406    "#;
2407
2408        let decoded = serde_json::from_str::<StateRequestBody>(body);
2409
2410        assert!(decoded.is_err(), "Expected an error due to unknown field");
2411
2412        if let Err(e) = decoded {
2413            assert!(
2414                e.to_string()
2415                    .contains("unknown field `contract_ids_with_typo_error`"),
2416                "Error message does not contain expected unknown field information"
2417            );
2418        }
2419    }
2420
2421    #[test]
2422    fn test_parse_state_request_no_contract_specified() {
2423        let json_str = r#"
2424    {
2425        "protocol_system": "uniswap_v2",
2426        "version": {
2427            "timestamp": "2069-01-01T04:20:00",
2428            "block": {
2429                "hash": "0x24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4",
2430                "number": 213,
2431                "chain": "ethereum"
2432            }
2433        }
2434    }
2435    "#;
2436
2437        let result: StateRequestBody = serde_json::from_str(json_str).unwrap();
2438
2439        let block_hash = "24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4".into();
2440        let block_number = 213;
2441        let expected_timestamp =
2442            NaiveDateTime::parse_from_str("2069-01-01T04:20:00", "%Y-%m-%dT%H:%M:%S").unwrap();
2443
2444        let expected = StateRequestBody {
2445            contract_ids: None,
2446            protocol_system: "uniswap_v2".to_string(),
2447            version: VersionParam {
2448                timestamp: Some(expected_timestamp),
2449                block: Some(BlockParam {
2450                    hash: Some(block_hash),
2451                    chain: Some(Chain::Ethereum),
2452                    number: Some(block_number),
2453                }),
2454            },
2455            chain: Chain::Ethereum,
2456            pagination: PaginationParams { page: 0, page_size: 100 },
2457        };
2458
2459        assert_eq!(result, expected);
2460    }
2461
2462    #[rstest]
2463    #[case::deprecated_ids(
2464        r#"
2465    {
2466        "protocol_ids": [
2467            {
2468                "id": "0xb4eccE46b8D4e4abFd03C9B806276A6735C9c092",
2469                "chain": "ethereum"
2470            }
2471        ],
2472        "protocol_system": "uniswap_v2",
2473        "include_balances": false,
2474        "version": {
2475            "timestamp": "2069-01-01T04:20:00",
2476            "block": {
2477                "hash": "0x24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4",
2478                "number": 213,
2479                "chain": "ethereum"
2480            }
2481        }
2482    }
2483    "#
2484    )]
2485    #[case(
2486        r#"
2487    {
2488        "protocolIds": [
2489            "0xb4eccE46b8D4e4abFd03C9B806276A6735C9c092"
2490        ],
2491        "protocol_system": "uniswap_v2",
2492        "include_balances": false,
2493        "version": {
2494            "timestamp": "2069-01-01T04:20:00",
2495            "block": {
2496                "hash": "0x24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4",
2497                "number": 213,
2498                "chain": "ethereum"
2499            }
2500        }
2501    }
2502    "#
2503    )]
2504    fn test_parse_protocol_state_request(#[case] json_str: &str) {
2505        let result: ProtocolStateRequestBody = serde_json::from_str(json_str).unwrap();
2506
2507        let block_hash = "24101f9cb26cd09425b52da10e8c2f56ede94089a8bbe0f31f1cda5f4daa52c4"
2508            .parse()
2509            .unwrap();
2510        let block_number = 213;
2511
2512        let expected_timestamp =
2513            NaiveDateTime::parse_from_str("2069-01-01T04:20:00", "%Y-%m-%dT%H:%M:%S").unwrap();
2514
2515        let expected = ProtocolStateRequestBody {
2516            protocol_ids: Some(vec!["0xb4eccE46b8D4e4abFd03C9B806276A6735C9c092".to_string()]),
2517            protocol_system: "uniswap_v2".to_string(),
2518            version: VersionParam {
2519                timestamp: Some(expected_timestamp),
2520                block: Some(BlockParam {
2521                    hash: Some(block_hash),
2522                    chain: Some(Chain::Ethereum),
2523                    number: Some(block_number),
2524                }),
2525            },
2526            chain: Chain::Ethereum,
2527            include_balances: false,
2528            pagination: PaginationParams::default(),
2529        };
2530
2531        assert_eq!(result, expected);
2532    }
2533
2534    #[rstest]
2535    #[case::with_protocol_ids(vec![ProtocolId { id: "id1".to_string(), chain: Chain::Ethereum }, ProtocolId { id: "id2".to_string(), chain: Chain::Ethereum }], vec!["id1".to_string(), "id2".to_string()])]
2536    #[case::with_strings(vec!["id1".to_string(), "id2".to_string()], vec!["id1".to_string(), "id2".to_string()])]
2537    fn test_id_filtered<T>(#[case] input_ids: Vec<T>, #[case] expected_ids: Vec<String>)
2538    where
2539        T: Into<String> + Clone,
2540    {
2541        let request_body = ProtocolStateRequestBody::id_filtered(input_ids);
2542
2543        assert_eq!(request_body.protocol_ids, Some(expected_ids));
2544    }
2545
2546    fn create_models_block_changes() -> crate::models::blockchain::BlockAggregatedChanges {
2547        let base_ts = 1694534400; // Example base timestamp for 2023-09-14T00:00:00
2548
2549        crate::models::blockchain::BlockAggregatedChanges {
2550            extractor: "native_name".to_string(),
2551            block: models::blockchain::Block::new(
2552                3,
2553                models::Chain::Ethereum,
2554                Bytes::from_str("0x0000000000000000000000000000000000000000000000000000000000000003").unwrap(),
2555                Bytes::from_str("0x0000000000000000000000000000000000000000000000000000000000000002").unwrap(),
2556                chrono::DateTime::from_timestamp(base_ts + 3000, 0).unwrap().naive_utc(),
2557            ),
2558            db_committed_block_height: Some(1),
2559            finalized_block_height: 1,
2560            revert: true,
2561            state_deltas: HashMap::from([
2562                ("pc_1".to_string(), models::protocol::ProtocolComponentStateDelta {
2563                    component_id: "pc_1".to_string(),
2564                    updated_attributes: HashMap::from([
2565                        ("attr_2".to_string(), Bytes::from("0x0000000000000002")),
2566                        ("attr_1".to_string(), Bytes::from("0x00000000000003e8")),
2567                    ]),
2568                    deleted_attributes: HashSet::new(),
2569                }),
2570            ]),
2571            new_protocol_components: HashMap::from([
2572                ("pc_2".to_string(), crate::models::protocol::ProtocolComponent {
2573                    id: "pc_2".to_string(),
2574                    protocol_system: "native_protocol_system".to_string(),
2575                    protocol_type_name: "pt_1".to_string(),
2576                    chain: models::Chain::Ethereum,
2577                    tokens: vec![
2578                        Bytes::from_str("0xdac17f958d2ee523a2206206994597c13d831ec7").unwrap(),
2579                        Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
2580                    ],
2581                    contract_addresses: vec![],
2582                    static_attributes: HashMap::new(),
2583                    change: models::ChangeType::Creation,
2584                    creation_tx: Bytes::from_str("0x000000000000000000000000000000000000000000000000000000000000c351").unwrap(),
2585                    created_at: chrono::DateTime::from_timestamp(base_ts + 5000, 0).unwrap().naive_utc(),
2586                }),
2587            ]),
2588            deleted_protocol_components: HashMap::from([
2589                ("pc_3".to_string(), crate::models::protocol::ProtocolComponent {
2590                    id: "pc_3".to_string(),
2591                    protocol_system: "native_protocol_system".to_string(),
2592                    protocol_type_name: "pt_2".to_string(),
2593                    chain: models::Chain::Ethereum,
2594                    tokens: vec![
2595                        Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(),
2596                        Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(),
2597                    ],
2598                    contract_addresses: vec![],
2599                    static_attributes: HashMap::new(),
2600                    change: models::ChangeType::Deletion,
2601                    creation_tx: Bytes::from_str("0x0000000000000000000000000000000000000000000000000000000000009c41").unwrap(),
2602                    created_at: chrono::DateTime::from_timestamp(base_ts + 4000, 0).unwrap().naive_utc(),
2603                }),
2604            ]),
2605            component_balances: HashMap::from([
2606                ("pc_1".to_string(), HashMap::from([
2607                    (Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(), models::protocol::ComponentBalance {
2608                        token: Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
2609                        balance: Bytes::from("0x00000001"),
2610                        balance_float: 1.0,
2611                        modify_tx: Bytes::from_str("0x0000000000000000000000000000000000000000000000000000000000000000").unwrap(),
2612                        component_id: "pc_1".to_string(),
2613                    }),
2614                    (Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(), models::protocol::ComponentBalance {
2615                        token: Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(),
2616                        balance: Bytes::from("0x000003e8"),
2617                        balance_float: 1000.0,
2618                        modify_tx: Bytes::from_str("0x0000000000000000000000000000000000000000000000000000000000007531").unwrap(),
2619                        component_id: "pc_1".to_string(),
2620                    }),
2621                ])),
2622            ]),
2623            account_balances: HashMap::from([
2624                (Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(), HashMap::from([
2625                    (Bytes::from_str("0x7a250d5630b4cf539739df2c5dacb4c659f2488d").unwrap(), models::contract::AccountBalance {
2626                        account: Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(),
2627                        token: Bytes::from_str("0x7a250d5630b4cf539739df2c5dacb4c659f2488d").unwrap(),
2628                        balance: Bytes::from("0x000003e8"),
2629                        modify_tx: Bytes::from_str("0x0000000000000000000000000000000000000000000000000000000000007531").unwrap(),
2630                    }),
2631                    ])),
2632            ]),
2633            ..Default::default()
2634        }
2635    }
2636
2637    #[test]
2638    fn test_serialize_deserialize_block_changes() {
2639        // Test that models::BlockAggregatedChanges serialized as json can be deserialized as
2640        // dto::BlockChanges.
2641
2642        // Create a models::BlockAggregatedChanges instance
2643        let block_entity_changes = create_models_block_changes();
2644
2645        // Serialize the struct into JSON
2646        let json_data = serde_json::to_string(&block_entity_changes).expect("Failed to serialize");
2647
2648        // Deserialize the JSON back into a dto::BlockChanges struct
2649        serde_json::from_str::<BlockChanges>(&json_data).expect("parsing failed");
2650    }
2651
2652    #[test]
2653    fn test_parse_block_changes() {
2654        let json_data = r#"
2655        {
2656            "extractor": "vm:ambient",
2657            "chain": "ethereum",
2658            "block": {
2659                "number": 123,
2660                "hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
2661                "parent_hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
2662                "chain": "ethereum",
2663                "ts": "2023-09-14T00:00:00"
2664            },
2665            "finalized_block_height": 0,
2666            "revert": false,
2667            "new_tokens": {},
2668            "account_updates": {
2669                "0x7a250d5630b4cf539739df2c5dacb4c659f2488d": {
2670                    "address": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
2671                    "chain": "ethereum",
2672                    "slots": {},
2673                    "balance": "0x01f4",
2674                    "code": "",
2675                    "change": "Update"
2676                }
2677            },
2678            "state_updates": {
2679                "component_1": {
2680                    "component_id": "component_1",
2681                    "updated_attributes": {"attr1": "0x01"},
2682                    "deleted_attributes": ["attr2"]
2683                }
2684            },
2685            "new_protocol_components":
2686                { "protocol_1": {
2687                        "id": "protocol_1",
2688                        "protocol_system": "system_1",
2689                        "protocol_type_name": "type_1",
2690                        "chain": "ethereum",
2691                        "tokens": ["0x01", "0x02"],
2692                        "contract_ids": ["0x01", "0x02"],
2693                        "static_attributes": {"attr1": "0x01f4"},
2694                        "change": "Update",
2695                        "creation_tx": "0x01",
2696                        "created_at": "2023-09-14T00:00:00"
2697                    }
2698                },
2699            "deleted_protocol_components": {},
2700            "component_balances": {
2701                "protocol_1":
2702                    {
2703                        "0x01": {
2704                            "token": "0x01",
2705                            "balance": "0xb77831d23691653a01",
2706                            "balance_float": 3.3844151001790677e21,
2707                            "modify_tx": "0x01",
2708                            "component_id": "protocol_1"
2709                        }
2710                    }
2711            },
2712            "account_balances": {
2713                "0x7a250d5630b4cf539739df2c5dacb4c659f2488d": {
2714                    "0x7a250d5630b4cf539739df2c5dacb4c659f2488d": {
2715                        "account": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
2716                        "token": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
2717                        "balance": "0x01f4",
2718                        "modify_tx": "0x01"
2719                    }
2720                }
2721            },
2722            "component_tvl": {
2723                "protocol_1": 1000.0
2724            },
2725            "dci_update": {
2726                "new_entrypoints": {
2727                    "component_1": [
2728                        {
2729                            "external_id": "0x01:sig()",
2730                            "target": "0x01",
2731                            "signature": "sig()"
2732                        }
2733                    ]
2734                },
2735                "new_entrypoint_params": {
2736                    "0x01:sig()": [
2737                        [
2738                            {
2739                                "method": "rpctracer",
2740                                "caller": "0x01",
2741                                "calldata": "0x02"
2742                            },
2743                            "component_1"
2744                        ]
2745                    ]
2746                },
2747                "trace_results": {
2748                    "0x01:sig()": {
2749                        "retriggers": [
2750                            ["0x01", {"key": "0x02", "offset": 12}]
2751                        ],
2752                        "accessed_slots": {
2753                            "0x03": ["0x03", "0x04"]
2754                        }
2755                    }
2756                }
2757            }
2758        }
2759        "#;
2760
2761        serde_json::from_str::<BlockChanges>(json_data).expect("parsing failed");
2762    }
2763
2764    #[test]
2765    fn test_parse_websocket_message() {
2766        let json_data = r#"
2767        {
2768            "subscription_id": "5d23bfbe-89ad-4ea3-8672-dc9e973ac9dc",
2769            "deltas": {
2770                "type": "BlockChanges",
2771                "extractor": "uniswap_v2",
2772                "chain": "ethereum",
2773                "block": {
2774                    "number": 19291517,
2775                    "hash": "0xbc3ea4896c0be8da6229387a8571b72818aa258daf4fab46471003ad74c4ee83",
2776                    "parent_hash": "0x89ca5b8d593574cf6c886f41ef8208bf6bdc1a90ef36046cb8c84bc880b9af8f",
2777                    "chain": "ethereum",
2778                    "ts": "2024-02-23T16:35:35"
2779                },
2780                "finalized_block_height": 0,
2781                "revert": false,
2782                "new_tokens": {},
2783                "account_updates": {
2784                    "0x7a250d5630b4cf539739df2c5dacb4c659f2488d": {
2785                        "address": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
2786                        "chain": "ethereum",
2787                        "slots": {},
2788                        "balance": "0x01f4",
2789                        "code": "",
2790                        "change": "Update"
2791                    }
2792                },
2793                "state_updates": {
2794                    "0xde6faedbcae38eec6d33ad61473a04a6dd7f6e28": {
2795                        "component_id": "0xde6faedbcae38eec6d33ad61473a04a6dd7f6e28",
2796                        "updated_attributes": {
2797                            "reserve0": "0x87f7b5973a7f28a8b32404",
2798                            "reserve1": "0x09e9564b11"
2799                        },
2800                        "deleted_attributes": []
2801                    },
2802                    "0x99c59000f5a76c54c4fd7d82720c045bdcf1450d": {
2803                        "component_id": "0x99c59000f5a76c54c4fd7d82720c045bdcf1450d",
2804                        "updated_attributes": {
2805                            "reserve1": "0x44d9a8fd662c2f4d03",
2806                            "reserve0": "0x500b1261f811d5bf423e"
2807                        },
2808                        "deleted_attributes": []
2809                    }
2810                },
2811                "new_protocol_components": {},
2812                "deleted_protocol_components": {},
2813                "component_balances": {
2814                    "0x99c59000f5a76c54c4fd7d82720c045bdcf1450d": {
2815                        "0x9012744b7a564623b6c3e40b144fc196bdedf1a9": {
2816                            "token": "0x9012744b7a564623b6c3e40b144fc196bdedf1a9",
2817                            "balance": "0x500b1261f811d5bf423e",
2818                            "balance_float": 3.779935574269033E23,
2819                            "modify_tx": "0xe46c4db085fb6c6f3408a65524555797adb264e1d5cf3b66ad154598f85ac4bf",
2820                            "component_id": "0x99c59000f5a76c54c4fd7d82720c045bdcf1450d"
2821                        },
2822                        "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2": {
2823                            "token": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
2824                            "balance": "0x44d9a8fd662c2f4d03",
2825                            "balance_float": 1.270062661329837E21,
2826                            "modify_tx": "0xe46c4db085fb6c6f3408a65524555797adb264e1d5cf3b66ad154598f85ac4bf",
2827                            "component_id": "0x99c59000f5a76c54c4fd7d82720c045bdcf1450d"
2828                        }
2829                    }
2830                },
2831                "account_balances": {
2832                    "0x7a250d5630b4cf539739df2c5dacb4c659f2488d": {
2833                        "0x7a250d5630b4cf539739df2c5dacb4c659f2488d": {
2834                            "account": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
2835                            "token": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
2836                            "balance": "0x01f4",
2837                            "modify_tx": "0x01"
2838                        }
2839                    }
2840                },
2841                "component_tvl": {},
2842                "dci_update": {
2843                    "new_entrypoints": {
2844                        "0xde6faedbcae38eec6d33ad61473a04a6dd7f6e28": [
2845                            {
2846                                "external_id": "0x01:sig()",
2847                                "target": "0x01",
2848                                "signature": "sig()"
2849                            }
2850                        ]
2851                    },
2852                    "new_entrypoint_params": {
2853                        "0x01:sig()": [
2854                            [
2855                                {
2856                                    "method": "rpctracer",
2857                                    "caller": "0x01",
2858                                    "calldata": "0x02"
2859                                },
2860                                "0xde6faedbcae38eec6d33ad61473a04a6dd7f6e28"
2861                            ]
2862                        ]
2863                    },
2864                    "trace_results": {
2865                        "0x01:sig()": {
2866                            "retriggers": [
2867                                ["0x01", {"key": "0x02", "offset": 12}]
2868                            ],
2869                            "accessed_slots": {
2870                                "0x03": ["0x03", "0x04"]
2871                            }
2872                        }
2873                    }
2874                }
2875            }
2876        }
2877        "#;
2878        serde_json::from_str::<WebSocketMessage>(json_data).expect("parsing failed");
2879    }
2880
2881    #[test]
2882    fn test_protocol_state_delta_merge_update_delete() {
2883        // Initialize ProtocolStateDelta instances
2884        let mut delta1 = ProtocolStateDelta {
2885            component_id: "Component1".to_string(),
2886            updated_attributes: HashMap::from([(
2887                "Attribute1".to_string(),
2888                Bytes::from("0xbadbabe420"),
2889            )]),
2890            deleted_attributes: HashSet::new(),
2891        };
2892        let delta2 = ProtocolStateDelta {
2893            component_id: "Component1".to_string(),
2894            updated_attributes: HashMap::from([(
2895                "Attribute2".to_string(),
2896                Bytes::from("0x0badbabe"),
2897            )]),
2898            deleted_attributes: HashSet::from(["Attribute1".to_string()]),
2899        };
2900        let exp = ProtocolStateDelta {
2901            component_id: "Component1".to_string(),
2902            updated_attributes: HashMap::from([(
2903                "Attribute2".to_string(),
2904                Bytes::from("0x0badbabe"),
2905            )]),
2906            deleted_attributes: HashSet::from(["Attribute1".to_string()]),
2907        };
2908
2909        delta1.merge(&delta2);
2910
2911        assert_eq!(delta1, exp);
2912    }
2913
2914    #[test]
2915    fn test_protocol_state_delta_merge_delete_update() {
2916        // Initialize ProtocolStateDelta instances
2917        let mut delta1 = ProtocolStateDelta {
2918            component_id: "Component1".to_string(),
2919            updated_attributes: HashMap::new(),
2920            deleted_attributes: HashSet::from(["Attribute1".to_string()]),
2921        };
2922        let delta2 = ProtocolStateDelta {
2923            component_id: "Component1".to_string(),
2924            updated_attributes: HashMap::from([(
2925                "Attribute1".to_string(),
2926                Bytes::from("0x0badbabe"),
2927            )]),
2928            deleted_attributes: HashSet::new(),
2929        };
2930        let exp = ProtocolStateDelta {
2931            component_id: "Component1".to_string(),
2932            updated_attributes: HashMap::from([(
2933                "Attribute1".to_string(),
2934                Bytes::from("0x0badbabe"),
2935            )]),
2936            deleted_attributes: HashSet::new(),
2937        };
2938
2939        delta1.merge(&delta2);
2940
2941        assert_eq!(delta1, exp);
2942    }
2943
2944    #[test]
2945    fn test_account_update_merge() {
2946        // Initialize AccountUpdate instances with same address and valid hex strings for Bytes
2947        let mut account1 = AccountUpdate::new(
2948            Bytes::from(b"0x1234"),
2949            Chain::Ethereum,
2950            HashMap::from([(Bytes::from("0xaabb"), Bytes::from("0xccdd"))]),
2951            Some(Bytes::from("0x1000")),
2952            Some(Bytes::from("0xdeadbeaf")),
2953            ChangeType::Creation,
2954        );
2955
2956        let account2 = AccountUpdate::new(
2957            Bytes::from(b"0x1234"), // Same id as account1
2958            Chain::Ethereum,
2959            HashMap::from([(Bytes::from("0xeeff"), Bytes::from("0x11223344"))]),
2960            Some(Bytes::from("0x2000")),
2961            Some(Bytes::from("0xcafebabe")),
2962            ChangeType::Update,
2963        );
2964
2965        // Merge account2 into account1
2966        account1.merge(&account2);
2967
2968        // Define the expected state after merge
2969        let expected = AccountUpdate::new(
2970            Bytes::from(b"0x1234"), // Same id as before the merge
2971            Chain::Ethereum,
2972            HashMap::from([
2973                (Bytes::from("0xaabb"), Bytes::from("0xccdd")), // Original slot from account1
2974                (Bytes::from("0xeeff"), Bytes::from("0x11223344")), // New slot from account2
2975            ]),
2976            Some(Bytes::from("0x2000")),     // Updated balance
2977            Some(Bytes::from("0xcafebabe")), // Updated code
2978            ChangeType::Creation,            // Updated change type
2979        );
2980
2981        // Assert the new account1 equals to the expected state
2982        assert_eq!(account1, expected);
2983    }
2984
2985    #[test]
2986    fn test_block_account_changes_merge() {
2987        // Prepare account updates
2988        let old_account_updates: HashMap<Bytes, AccountUpdate> = [(
2989            Bytes::from("0x0011"),
2990            AccountUpdate {
2991                address: Bytes::from("0x00"),
2992                chain: Chain::Ethereum,
2993                slots: HashMap::from([(Bytes::from("0x0022"), Bytes::from("0x0033"))]),
2994                balance: Some(Bytes::from("0x01")),
2995                code: Some(Bytes::from("0x02")),
2996                change: ChangeType::Creation,
2997            },
2998        )]
2999        .into_iter()
3000        .collect();
3001        let new_account_updates: HashMap<Bytes, AccountUpdate> = [(
3002            Bytes::from("0x0011"),
3003            AccountUpdate {
3004                address: Bytes::from("0x00"),
3005                chain: Chain::Ethereum,
3006                slots: HashMap::from([(Bytes::from("0x0044"), Bytes::from("0x0055"))]),
3007                balance: Some(Bytes::from("0x03")),
3008                code: Some(Bytes::from("0x04")),
3009                change: ChangeType::Update,
3010            },
3011        )]
3012        .into_iter()
3013        .collect();
3014        // Create initial and new BlockAccountChanges instances
3015        let block_account_changes_initial = BlockChanges {
3016            extractor: "extractor1".to_string(),
3017            revert: false,
3018            account_updates: old_account_updates,
3019            ..Default::default()
3020        };
3021
3022        let block_account_changes_new = BlockChanges {
3023            extractor: "extractor2".to_string(),
3024            revert: true,
3025            account_updates: new_account_updates,
3026            ..Default::default()
3027        };
3028
3029        // Merge the new BlockChanges into the initial one
3030        let res = block_account_changes_initial.merge(block_account_changes_new);
3031
3032        // Create the expected result of the merge operation
3033        let expected_account_updates: HashMap<Bytes, AccountUpdate> = [(
3034            Bytes::from("0x0011"),
3035            AccountUpdate {
3036                address: Bytes::from("0x00"),
3037                chain: Chain::Ethereum,
3038                slots: HashMap::from([
3039                    (Bytes::from("0x0044"), Bytes::from("0x0055")),
3040                    (Bytes::from("0x0022"), Bytes::from("0x0033")),
3041                ]),
3042                balance: Some(Bytes::from("0x03")),
3043                code: Some(Bytes::from("0x04")),
3044                change: ChangeType::Creation,
3045            },
3046        )]
3047        .into_iter()
3048        .collect();
3049        let block_account_changes_expected = BlockChanges {
3050            extractor: "extractor1".to_string(),
3051            revert: true,
3052            account_updates: expected_account_updates,
3053            ..Default::default()
3054        };
3055        assert_eq!(res, block_account_changes_expected);
3056    }
3057
3058    #[test]
3059    fn test_block_entity_changes_merge() {
3060        // Initialize two BlockChanges instances with different details
3061        let block_entity_changes_result1 = BlockChanges {
3062            extractor: String::from("extractor1"),
3063            revert: false,
3064            state_updates: hashmap! { "state1".to_string() => ProtocolStateDelta::default() },
3065            new_protocol_components: hashmap! { "component1".to_string() => ProtocolComponent::default() },
3066            deleted_protocol_components: HashMap::new(),
3067            component_balances: hashmap! {
3068                "component1".to_string() => TokenBalances(hashmap! {
3069                    Bytes::from("0x01") => ComponentBalance {
3070                            token: Bytes::from("0x01"),
3071                            balance: Bytes::from("0x01"),
3072                            balance_float: 1.0,
3073                            modify_tx: Bytes::from("0x00"),
3074                            component_id: "component1".to_string()
3075                        },
3076                    Bytes::from("0x02") => ComponentBalance {
3077                        token: Bytes::from("0x02"),
3078                        balance: Bytes::from("0x02"),
3079                        balance_float: 2.0,
3080                        modify_tx: Bytes::from("0x00"),
3081                        component_id: "component1".to_string()
3082                    },
3083                })
3084
3085            },
3086            component_tvl: hashmap! { "tvl1".to_string() => 1000.0 },
3087            ..Default::default()
3088        };
3089        let block_entity_changes_result2 = BlockChanges {
3090            extractor: String::from("extractor2"),
3091            revert: true,
3092            state_updates: hashmap! { "state2".to_string() => ProtocolStateDelta::default() },
3093            new_protocol_components: hashmap! { "component2".to_string() => ProtocolComponent::default() },
3094            deleted_protocol_components: hashmap! { "component3".to_string() => ProtocolComponent::default() },
3095            component_balances: hashmap! {
3096                "component1".to_string() => TokenBalances::default(),
3097                "component2".to_string() => TokenBalances::default()
3098            },
3099            component_tvl: hashmap! { "tvl2".to_string() => 2000.0 },
3100            ..Default::default()
3101        };
3102
3103        let res = block_entity_changes_result1.merge(block_entity_changes_result2);
3104
3105        let expected_block_entity_changes_result = BlockChanges {
3106            extractor: String::from("extractor1"),
3107            revert: true,
3108            state_updates: hashmap! {
3109                "state1".to_string() => ProtocolStateDelta::default(),
3110                "state2".to_string() => ProtocolStateDelta::default(),
3111            },
3112            new_protocol_components: hashmap! {
3113                "component1".to_string() => ProtocolComponent::default(),
3114                "component2".to_string() => ProtocolComponent::default(),
3115            },
3116            deleted_protocol_components: hashmap! {
3117                "component3".to_string() => ProtocolComponent::default(),
3118            },
3119            component_balances: hashmap! {
3120                "component1".to_string() => TokenBalances(hashmap! {
3121                    Bytes::from("0x01") => ComponentBalance {
3122                            token: Bytes::from("0x01"),
3123                            balance: Bytes::from("0x01"),
3124                            balance_float: 1.0,
3125                            modify_tx: Bytes::from("0x00"),
3126                            component_id: "component1".to_string()
3127                        },
3128                    Bytes::from("0x02") => ComponentBalance {
3129                        token: Bytes::from("0x02"),
3130                        balance: Bytes::from("0x02"),
3131                        balance_float: 2.0,
3132                        modify_tx: Bytes::from("0x00"),
3133                        component_id: "component1".to_string()
3134                        },
3135                    }),
3136                "component2".to_string() => TokenBalances::default(),
3137            },
3138            component_tvl: hashmap! {
3139                "tvl1".to_string() => 1000.0,
3140                "tvl2".to_string() => 2000.0
3141            },
3142            ..Default::default()
3143        };
3144
3145        assert_eq!(res, expected_block_entity_changes_result);
3146    }
3147
3148    #[test]
3149    fn test_websocket_error_serialization() {
3150        let extractor_id = ExtractorIdentity::new(Chain::Ethereum, "test_extractor");
3151        let subscription_id = Uuid::new_v4();
3152
3153        // Test ExtractorNotFound serialization
3154        let error = WebsocketError::ExtractorNotFound(extractor_id.clone());
3155        let json = serde_json::to_string(&error).unwrap();
3156        let deserialized: WebsocketError = serde_json::from_str(&json).unwrap();
3157        assert_eq!(error, deserialized);
3158
3159        // Test SubscriptionNotFound serialization
3160        let error = WebsocketError::SubscriptionNotFound(subscription_id);
3161        let json = serde_json::to_string(&error).unwrap();
3162        let deserialized: WebsocketError = serde_json::from_str(&json).unwrap();
3163        assert_eq!(error, deserialized);
3164
3165        // Test ParseError serialization
3166        let error = WebsocketError::ParseError("{asd".to_string(), "invalid json".to_string());
3167        let json = serde_json::to_string(&error).unwrap();
3168        let deserialized: WebsocketError = serde_json::from_str(&json).unwrap();
3169        assert_eq!(error, deserialized);
3170
3171        // Test SubscribeError serialization
3172        let error = WebsocketError::SubscribeError(extractor_id.clone());
3173        let json = serde_json::to_string(&error).unwrap();
3174        let deserialized: WebsocketError = serde_json::from_str(&json).unwrap();
3175        assert_eq!(error, deserialized);
3176
3177        // Test CompressionError serialization
3178        let error =
3179            WebsocketError::CompressionError(subscription_id, "Compression failed".to_string());
3180        let json = serde_json::to_string(&error).unwrap();
3181        let deserialized: WebsocketError = serde_json::from_str(&json).unwrap();
3182        assert_eq!(error, deserialized);
3183    }
3184
3185    #[test]
3186    fn test_websocket_message_with_error_response() {
3187        let error =
3188            WebsocketError::ParseError("}asdfas".to_string(), "malformed request".to_string());
3189        let response = Response::Error(error.clone());
3190        let message = WebSocketMessage::Response(response);
3191
3192        let json = serde_json::to_string(&message).unwrap();
3193        let deserialized: WebSocketMessage = serde_json::from_str(&json).unwrap();
3194
3195        if let WebSocketMessage::Response(Response::Error(deserialized_error)) = deserialized {
3196            assert_eq!(error, deserialized_error);
3197        } else {
3198            panic!("Expected WebSocketMessage::Response(Response::Error)");
3199        }
3200    }
3201
3202    #[test]
3203    fn test_websocket_error_conversion_from_models() {
3204        use crate::models::error::WebsocketError as ModelsError;
3205
3206        let extractor_id =
3207            crate::models::ExtractorIdentity::new(crate::models::Chain::Ethereum, "test");
3208        let subscription_id = Uuid::new_v4();
3209
3210        // Test ExtractorNotFound conversion
3211        let models_error = ModelsError::ExtractorNotFound(extractor_id.clone());
3212        let dto_error: WebsocketError = models_error.into();
3213        assert_eq!(dto_error, WebsocketError::ExtractorNotFound(extractor_id.clone().into()));
3214
3215        // Test SubscriptionNotFound conversion
3216        let models_error = ModelsError::SubscriptionNotFound(subscription_id);
3217        let dto_error: WebsocketError = models_error.into();
3218        assert_eq!(dto_error, WebsocketError::SubscriptionNotFound(subscription_id));
3219
3220        // Test ParseError conversion - create a real JSON parse error
3221        let json_result: Result<serde_json::Value, _> = serde_json::from_str("{invalid json");
3222        let json_error = json_result.unwrap_err();
3223        let models_error = ModelsError::ParseError("{invalid json".to_string(), json_error);
3224        let dto_error: WebsocketError = models_error.into();
3225        if let WebsocketError::ParseError(msg, error) = dto_error {
3226            // Just check that we have a non-empty error message
3227            assert!(!error.is_empty(), "Error message should not be empty, got: '{}'", msg);
3228        } else {
3229            panic!("Expected ParseError variant");
3230        }
3231
3232        // Test SubscribeError conversion
3233        let models_error = ModelsError::SubscribeError(extractor_id.clone());
3234        let dto_error: WebsocketError = models_error.into();
3235        assert_eq!(dto_error, WebsocketError::SubscribeError(extractor_id.into()));
3236
3237        // Test CompressionError conversion
3238        let io_error = std::io::Error::other("Compression failed");
3239        let models_error = ModelsError::CompressionError(subscription_id, io_error);
3240        let dto_error: WebsocketError = models_error.into();
3241        if let WebsocketError::CompressionError(sub_id, msg) = &dto_error {
3242            assert_eq!(*sub_id, subscription_id);
3243            assert!(msg.contains("Compression failed"));
3244        } else {
3245            panic!("Expected CompressionError variant");
3246        }
3247    }
3248}
3249
3250#[cfg(test)]
3251mod memory_size_tests {
3252    use std::collections::HashMap;
3253
3254    use super::*;
3255
3256    #[test]
3257    fn test_state_request_response_memory_size_empty() {
3258        let response = StateRequestResponse {
3259            accounts: vec![],
3260            pagination: PaginationResponse::new(1, 10, 0),
3261        };
3262
3263        let size = response.deep_size_of();
3264
3265        // Should at least include base struct sizes
3266        assert!(size >= 48, "Empty response should have minimum size of 48 bytes, got {}", size);
3267        assert!(size < 200, "Empty response should not be too large, got {}", size);
3268    }
3269
3270    #[test]
3271    fn test_state_request_response_memory_size_scales_with_slots() {
3272        let create_response_with_slots = |slot_count: usize| {
3273            let mut slots = HashMap::new();
3274            for i in 0..slot_count {
3275                let key = vec![i as u8; 32]; // 32-byte key
3276                let value = vec![(i + 100) as u8; 32]; // 32-byte value
3277                slots.insert(key.into(), value.into());
3278            }
3279
3280            let account = ResponseAccount::new(
3281                Chain::Ethereum,
3282                vec![1; 20].into(),
3283                "Pool".to_string(),
3284                slots,
3285                vec![1; 32].into(),
3286                HashMap::new(),
3287                vec![].into(), // empty code
3288                vec![1; 32].into(),
3289                vec![1; 32].into(),
3290                vec![1; 32].into(),
3291                None,
3292            );
3293
3294            StateRequestResponse {
3295                accounts: vec![account],
3296                pagination: PaginationResponse::new(1, 10, 1),
3297            }
3298        };
3299
3300        let small_response = create_response_with_slots(10);
3301        let large_response = create_response_with_slots(100);
3302
3303        let small_size = small_response.deep_size_of();
3304        let large_size = large_response.deep_size_of();
3305
3306        // Large response should be significantly bigger
3307        assert!(
3308            large_size > small_size * 5,
3309            "Large response ({} bytes) should be much larger than small response ({} bytes)",
3310            large_size,
3311            small_size
3312        );
3313
3314        // Each slot should contribute at least 64 bytes (32 + 32 + overhead)
3315        let size_diff = large_size - small_size;
3316        let expected_min_diff = 90 * 64; // 90 additional slots * 64 bytes each
3317        assert!(
3318            size_diff > expected_min_diff,
3319            "Size difference ({} bytes) should reflect the additional slot data",
3320            size_diff
3321        );
3322    }
3323}
3324
3325#[cfg(test)]
3326mod pagination_limits_tests {
3327    use super::*;
3328
3329    // Test struct for pagination limits
3330    #[derive(Clone, Debug)]
3331    struct TestRequestBody {
3332        pagination: PaginationParams,
3333    }
3334
3335    // Implement pagination limits for test struct
3336    impl_pagination_limits!(TestRequestBody, compressed = 500, uncompressed = 50);
3337
3338    #[test]
3339    fn test_effective_max_page_size() {
3340        // Test effective max with compression enabled
3341        let max_size = TestRequestBody::effective_max_page_size(true);
3342        assert_eq!(max_size, 500, "Should return compressed limit when compression is enabled");
3343
3344        // Test effective max with compression disabled
3345        let max_size = TestRequestBody::effective_max_page_size(false);
3346        assert_eq!(max_size, 50, "Should return uncompressed limit when compression is disabled");
3347    }
3348}