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