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