tycho_common/
dto.rs

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