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