Skip to main content

tycho_common/
dto.rs

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