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