tycho_common/
dto.rs

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