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