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