Skip to main content

tycho_common/models/
protocol.rs

1use std::{
2    collections::{HashMap, HashSet},
3    sync::Arc,
4};
5
6use chrono::NaiveDateTime;
7use deepsize::{Context, DeepSizeOf};
8use num_bigint::BigUint;
9use serde::{Deserialize, Serialize};
10
11use crate::{
12    dto,
13    models::{
14        token::Token, Address, AttrStoreKey, Balance, Chain, ChangeType, ComponentId, MergeError,
15        StoreVal, TxHash,
16    },
17    Bytes,
18};
19
20/// `ProtocolComponent` provides detailed descriptions of a component of a protocol,
21/// for example, swap pools that enables the exchange of two tokens.
22///
23/// A `ProtocolComponent` can be associated with an `Account`, and it has an identifier (`id`) that
24/// can be either the on-chain address or a custom one. It belongs to a specific `ProtocolSystem`
25/// and has a `ProtocolTypeID` that associates it with a `ProtocolType` that describes its behaviour
26/// e.g., swap, lend, bridge. The component is associated with a specific `Chain` and holds
27/// information about tradable tokens, related contract IDs, and static attributes.
28///
29/// Every values of a `ProtocolComponent` must be static, they can't ever be changed after creation.
30/// The dynamic values associated to a component must be given using `ProtocolComponentState`.
31#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
32pub struct ProtocolComponent<Token: Into<Address> + Clone = Address> {
33    pub id: ComponentId,
34    pub protocol_system: String,
35    pub protocol_type_name: String,
36    pub chain: Chain,
37    pub tokens: Vec<Token>,
38    pub contract_addresses: Vec<Address>,
39    pub static_attributes: HashMap<AttrStoreKey, StoreVal>,
40    pub change: ChangeType,
41    pub creation_tx: TxHash,
42    pub created_at: NaiveDateTime,
43}
44
45impl<T> ProtocolComponent<T>
46where
47    T: Into<Address> + Clone,
48{
49    #[allow(clippy::too_many_arguments)]
50    pub fn new(
51        id: &str,
52        protocol_system: &str,
53        protocol_type_name: &str,
54        chain: Chain,
55        tokens: Vec<T>,
56        contract_addresses: Vec<Address>,
57        static_attributes: HashMap<AttrStoreKey, StoreVal>,
58        change: ChangeType,
59        creation_tx: TxHash,
60        created_at: NaiveDateTime,
61    ) -> Self {
62        Self {
63            id: id.to_string(),
64            protocol_system: protocol_system.to_string(),
65            protocol_type_name: protocol_type_name.to_string(),
66            chain,
67            tokens,
68            contract_addresses,
69            static_attributes,
70            change,
71            creation_tx,
72            created_at,
73        }
74    }
75}
76
77impl ProtocolComponent<Arc<Token>> {
78    pub fn get_token(&self, address: &Address) -> Option<Arc<Token>> {
79        self.tokens
80            .iter()
81            .find(|t| &t.address == address)
82            .map(Arc::clone)
83    }
84}
85
86impl DeepSizeOf for ProtocolComponent {
87    fn deep_size_of_children(&self, ctx: &mut Context) -> usize {
88        self.id.deep_size_of_children(ctx) +
89            self.protocol_system
90                .deep_size_of_children(ctx) +
91            self.protocol_type_name
92                .deep_size_of_children(ctx) +
93            self.chain.deep_size_of_children(ctx) +
94            self.tokens.deep_size_of_children(ctx) +
95            self.contract_addresses
96                .deep_size_of_children(ctx) +
97            self.static_attributes
98                .deep_size_of_children(ctx) +
99            self.change.deep_size_of_children(ctx) +
100            self.creation_tx
101                .deep_size_of_children(ctx)
102    }
103}
104
105#[derive(Debug, Clone, PartialEq)]
106pub struct ProtocolComponentState {
107    pub component_id: ComponentId,
108    pub attributes: HashMap<AttrStoreKey, StoreVal>,
109    // used during snapshots retrieval by the gateway
110    pub balances: HashMap<Address, Balance>,
111}
112
113impl ProtocolComponentState {
114    pub fn new(
115        component_id: &str,
116        attributes: HashMap<AttrStoreKey, StoreVal>,
117        balances: HashMap<Address, Balance>,
118    ) -> Self {
119        Self { component_id: component_id.to_string(), attributes, balances }
120    }
121
122    /// Applies state deltas to this state.
123    ///
124    /// This method assumes that the passed delta is "newer" than the current state.
125    pub fn apply_state_delta(
126        &mut self,
127        delta: &ProtocolComponentStateDelta,
128    ) -> Result<(), MergeError> {
129        if self.component_id != delta.component_id {
130            return Err(MergeError::IdMismatch(
131                "ProtocolComponentStates".to_string(),
132                self.component_id.clone(),
133                delta.component_id.clone(),
134            ));
135        }
136        self.attributes
137            .extend(delta.updated_attributes.clone());
138
139        self.attributes
140            .retain(|attr, _| !delta.deleted_attributes.contains(attr));
141
142        Ok(())
143    }
144
145    /// Applies balance deltas to this state.
146    ///
147    /// This method assumes that the passed delta is "newer" than the current state.
148    pub fn apply_balance_delta(
149        &mut self,
150        delta: &HashMap<Bytes, ComponentBalance>,
151    ) -> Result<(), MergeError> {
152        self.balances.extend(
153            delta
154                .iter()
155                .map(|(k, v)| (k.clone(), v.balance.clone())),
156        );
157
158        Ok(())
159    }
160}
161
162#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, DeepSizeOf)]
163pub struct ProtocolComponentStateDelta {
164    pub component_id: ComponentId,
165    pub updated_attributes: HashMap<AttrStoreKey, StoreVal>,
166    pub deleted_attributes: HashSet<AttrStoreKey>,
167    /// Attribute names first introduced with `ChangeType::Creation` in this delta.
168    /// Skipped during serialization — this is an indexer-internal hint, not part of the public
169    /// delta format.
170    #[serde(skip)]
171    pub created_attributes: HashSet<AttrStoreKey>,
172}
173
174impl ProtocolComponentStateDelta {
175    pub fn new(
176        component_id: &str,
177        updated_attributes: HashMap<AttrStoreKey, StoreVal>,
178        deleted_attributes: HashSet<AttrStoreKey>,
179    ) -> Self {
180        Self {
181            component_id: component_id.to_string(),
182            updated_attributes,
183            deleted_attributes,
184            created_attributes: HashSet::new(),
185        }
186    }
187
188    /// Merges this update with another one.
189    ///
190    /// The method combines two `ProtocolComponentStateDelta` instances if they are for the same
191    /// protocol component.
192    ///
193    /// NB: It is assumed that `other` is a more recent update than `self` is and the two are
194    /// combined accordingly.
195    ///
196    /// # Errors
197    /// This method will return `CoreError::MergeError` if any of the above
198    /// conditions is violated.
199    pub fn merge(&mut self, other: ProtocolComponentStateDelta) -> Result<(), MergeError> {
200        if self.component_id != other.component_id {
201            return Err(MergeError::IdMismatch(
202                "ProtocolComponentStateDeltas".to_string(),
203                self.component_id.clone(),
204                other.component_id.clone(),
205            ));
206        }
207        for attr in &other.deleted_attributes {
208            self.updated_attributes.remove(attr);
209            self.created_attributes.remove(attr);
210        }
211        for attr in other.updated_attributes.keys() {
212            self.deleted_attributes.remove(attr);
213        }
214        self.updated_attributes
215            .extend(other.updated_attributes);
216        self.deleted_attributes
217            .extend(other.deleted_attributes);
218        self.created_attributes
219            .extend(other.created_attributes);
220        Ok(())
221    }
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, DeepSizeOf)]
225pub struct ComponentBalance {
226    pub token: Address,
227    pub balance: Balance,
228    pub balance_float: f64,
229    pub modify_tx: TxHash,
230    pub component_id: ComponentId,
231}
232
233impl ComponentBalance {
234    pub fn new(
235        token: Address,
236        new_balance: Balance,
237        balance_float: f64,
238        modify_tx: TxHash,
239        component_id: &str,
240    ) -> Self {
241        Self {
242            token,
243            balance: new_balance,
244            balance_float,
245            modify_tx,
246            component_id: component_id.to_string(),
247        }
248    }
249}
250
251/// Token quality range filter
252///
253/// The quality range is considered inclusive and used as a filter, will be applied as such.
254#[derive(Debug, Clone)]
255pub struct QualityRange {
256    pub min: Option<i32>,
257    pub max: Option<i32>,
258}
259
260impl QualityRange {
261    pub fn new(min: i32, max: i32) -> Self {
262        Self { min: Some(min), max: Some(max) }
263    }
264
265    pub fn min_only(min: i32) -> Self {
266        Self { min: Some(min), max: None }
267    }
268
269    #[allow(non_snake_case)]
270    pub fn None() -> Self {
271        Self { min: None, max: None }
272    }
273}
274
275pub struct GetAmountOutParams {
276    pub amount_in: BigUint,
277    pub token_in: Bytes,
278    pub token_out: Bytes,
279    pub sender: Bytes,
280    pub receiver: Bytes,
281}
282
283impl From<dto::ProtocolStateDelta> for ProtocolComponentStateDelta {
284    fn from(value: dto::ProtocolStateDelta) -> Self {
285        Self {
286            component_id: value.component_id,
287            updated_attributes: value.updated_attributes,
288            deleted_attributes: value.deleted_attributes,
289            created_attributes: HashSet::new(),
290        }
291    }
292}
293
294impl From<dto::ComponentBalance> for ComponentBalance {
295    fn from(value: dto::ComponentBalance) -> Self {
296        Self {
297            token: value.token,
298            balance: value.balance,
299            balance_float: value.balance_float,
300            modify_tx: value.modify_tx,
301            component_id: value.component_id,
302        }
303    }
304}
305
306impl From<dto::ProtocolComponent> for ProtocolComponent {
307    fn from(value: dto::ProtocolComponent) -> Self {
308        Self {
309            id: value.id,
310            protocol_system: value.protocol_system,
311            protocol_type_name: value.protocol_type_name,
312            chain: value.chain.into(),
313            tokens: value.tokens,
314            contract_addresses: value.contract_ids,
315            static_attributes: value.static_attributes,
316            change: value.change.into(),
317            creation_tx: value.creation_tx,
318            created_at: value.created_at,
319        }
320    }
321}
322
323impl From<dto::ResponseProtocolState> for ProtocolComponentState {
324    fn from(value: dto::ResponseProtocolState) -> Self {
325        Self {
326            component_id: value.component_id,
327            attributes: value.attributes,
328            balances: value.balances,
329        }
330    }
331}
332
333#[cfg(test)]
334mod test {
335    use super::*;
336
337    fn create_state(id: String) -> ProtocolComponentStateDelta {
338        let attributes1: HashMap<String, Bytes> = vec![
339            ("reserve1".to_owned(), Bytes::from(1000u64).lpad(32, 0)),
340            ("reserve2".to_owned(), Bytes::from(500u64).lpad(32, 0)),
341            ("static_attribute".to_owned(), Bytes::from(1u64).lpad(32, 0)),
342        ]
343        .into_iter()
344        .collect();
345        ProtocolComponentStateDelta {
346            component_id: id,
347            updated_attributes: attributes1,
348            deleted_attributes: HashSet::new(),
349            ..Default::default()
350        }
351    }
352
353    #[test]
354    fn test_merge_protocol_state_updates() {
355        let mut state_1 = create_state("State1".to_owned());
356        state_1
357            .updated_attributes
358            .insert("to_be_removed".to_owned(), Bytes::from(1u64).lpad(32, 0));
359        state_1.deleted_attributes = vec!["to_add_back".to_owned()]
360            .into_iter()
361            .collect();
362
363        let attributes2: HashMap<String, Bytes> = vec![
364            ("reserve1".to_owned(), Bytes::from(900u64).lpad(32, 0)),
365            ("reserve2".to_owned(), Bytes::from(550u64).lpad(32, 0)),
366            ("new_attribute".to_owned(), Bytes::from(1u64).lpad(32, 0)),
367            ("to_add_back".to_owned(), Bytes::from(200u64).lpad(32, 0)),
368        ]
369        .into_iter()
370        .collect();
371        let del_attributes2: HashSet<String> = vec!["to_be_removed".to_owned()]
372            .into_iter()
373            .collect();
374        let mut state_2 = create_state("State1".to_owned());
375        state_2.updated_attributes = attributes2;
376        state_2.deleted_attributes = del_attributes2;
377
378        let res = state_1.merge(state_2);
379
380        assert!(res.is_ok());
381        let expected_attributes: HashMap<String, Bytes> = vec![
382            ("reserve1".to_owned(), Bytes::from(900u64).lpad(32, 0)),
383            ("reserve2".to_owned(), Bytes::from(550u64).lpad(32, 0)),
384            ("static_attribute".to_owned(), Bytes::from(1u64).lpad(32, 0)),
385            ("new_attribute".to_owned(), Bytes::from(1u64).lpad(32, 0)),
386            ("to_add_back".to_owned(), Bytes::from(200u64).lpad(32, 0)),
387        ]
388        .into_iter()
389        .collect();
390        assert_eq!(state_1.updated_attributes, expected_attributes);
391        let expected_del_attributes: HashSet<String> = vec!["to_be_removed".to_owned()]
392            .into_iter()
393            .collect();
394        assert_eq!(state_1.deleted_attributes, expected_del_attributes);
395    }
396
397    #[test]
398    fn test_merge_protocol_state_update_wrong_id() {
399        let mut state1 = create_state("State1".to_owned());
400        let state2 = create_state("State2".to_owned());
401
402        let res = state1.merge(state2);
403
404        assert_eq!(
405            res,
406            Err(MergeError::IdMismatch(
407                "ProtocolComponentStateDeltas".to_string(),
408                "State1".to_string(),
409                "State2".to_string(),
410            ))
411        );
412    }
413}