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