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