Skip to main content

tycho_common/
dto.rs

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