Skip to main content

tycho_common/
dto.rs

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