tycho_common/
dto.rs

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