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