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