tycho_common/
dto.rs

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