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