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