tycho_common/models/
protocol.rs

1use std::collections::{hash_map::Entry, HashMap, HashSet};
2
3use chrono::NaiveDateTime;
4use serde::{Deserialize, Serialize};
5use tracing::warn;
6
7use crate::{
8    models::{
9        blockchain::Transaction, Address, AttrStoreKey, Balance, Chain, ChangeType, ComponentId,
10        MergeError, StoreVal, TxHash,
11    },
12    Bytes,
13};
14
15/// `ProtocolComponent` provides detailed descriptions of a component of a protocol,
16/// for example, swap pools that enables the exchange of two tokens.
17///
18/// A `ProtocolComponent` can be associated with an `Account`, and it has an identifier (`id`) that
19/// can be either the on-chain address or a custom one. It belongs to a specific `ProtocolSystem`
20/// and has a `ProtocolTypeID` that associates it with a `ProtocolType` that describes its behaviour
21/// e.g., swap, lend, bridge. The component is associated with a specific `Chain` and holds
22/// information about tradable tokens, related contract IDs, and static attributes.
23///
24/// Every values of a `ProtocolComponent` must be static, they can't ever be changed after creation.
25/// The dynamic values associated to a component must be given using `ProtocolComponentState`.
26#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
27pub struct ProtocolComponent {
28    pub id: ComponentId,
29    pub protocol_system: String,
30    pub protocol_type_name: String,
31    pub chain: Chain,
32    pub tokens: Vec<Address>,
33    pub contract_addresses: Vec<Address>,
34    pub static_attributes: HashMap<AttrStoreKey, StoreVal>,
35    pub change: ChangeType,
36    pub creation_tx: TxHash,
37    pub created_at: NaiveDateTime,
38}
39
40impl ProtocolComponent {
41    #[allow(clippy::too_many_arguments)]
42    pub fn new(
43        id: &str,
44        protocol_system: &str,
45        protocol_type_name: &str,
46        chain: Chain,
47        tokens: Vec<Address>,
48        contract_addresses: Vec<Address>,
49        static_attributes: HashMap<AttrStoreKey, StoreVal>,
50        change: ChangeType,
51        creation_tx: TxHash,
52        created_at: NaiveDateTime,
53    ) -> Self {
54        Self {
55            id: id.to_string(),
56            protocol_system: protocol_system.to_string(),
57            protocol_type_name: protocol_type_name.to_string(),
58            chain,
59            tokens,
60            contract_addresses,
61            static_attributes,
62            change,
63            creation_tx,
64            created_at,
65        }
66    }
67}
68
69#[derive(Debug, Clone, PartialEq)]
70pub struct ProtocolComponentState {
71    pub component_id: ComponentId,
72    pub attributes: HashMap<AttrStoreKey, StoreVal>,
73    // used during snapshots retrieval by the gateway
74    pub balances: HashMap<Address, Balance>,
75}
76
77impl ProtocolComponentState {
78    pub fn new(
79        component_id: &str,
80        attributes: HashMap<AttrStoreKey, StoreVal>,
81        balances: HashMap<Address, Balance>,
82    ) -> Self {
83        Self { component_id: component_id.to_string(), attributes, balances }
84    }
85
86    /// Applies state deltas to this state.
87    ///
88    /// This method assumes that the passed delta is "newer" than the current state.
89    pub fn apply_state_delta(
90        &mut self,
91        delta: &ProtocolComponentStateDelta,
92    ) -> Result<(), MergeError> {
93        if self.component_id != delta.component_id {
94            return Err(MergeError::IdMismatch(
95                "ProtocolComponentStates".to_string(),
96                self.component_id.clone(),
97                delta.component_id.clone(),
98            ));
99        }
100        self.attributes
101            .extend(delta.updated_attributes.clone());
102
103        self.attributes
104            .retain(|attr, _| !delta.deleted_attributes.contains(attr));
105
106        Ok(())
107    }
108
109    /// Applies balance deltas to this state.
110    ///
111    /// This method assumes that the passed delta is "newer" than the current state.
112    pub fn apply_balance_delta(
113        &mut self,
114        delta: &HashMap<Bytes, ComponentBalance>,
115    ) -> Result<(), MergeError> {
116        self.balances.extend(
117            delta
118                .iter()
119                .map(|(k, v)| (k.clone(), v.balance.clone())),
120        );
121
122        Ok(())
123    }
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
127pub struct ProtocolComponentStateDelta {
128    pub component_id: ComponentId,
129    pub updated_attributes: HashMap<AttrStoreKey, StoreVal>,
130    pub deleted_attributes: HashSet<AttrStoreKey>,
131}
132
133impl ProtocolComponentStateDelta {
134    pub fn new(
135        component_id: &str,
136        updated_attributes: HashMap<AttrStoreKey, StoreVal>,
137        deleted_attributes: HashSet<AttrStoreKey>,
138    ) -> Self {
139        Self { component_id: component_id.to_string(), updated_attributes, deleted_attributes }
140    }
141
142    /// Merges this update with another one.
143    ///
144    /// The method combines two `ProtocolComponentStateDelta` instances if they are for the same
145    /// protocol component.
146    ///
147    /// NB: It is assumed that `other` is a more recent update than `self` is and the two are
148    /// combined accordingly.
149    ///
150    /// # Errors
151    /// This method will return `CoreError::MergeError` if any of the above
152    /// conditions is violated.
153    pub fn merge(&mut self, other: ProtocolComponentStateDelta) -> Result<(), MergeError> {
154        if self.component_id != other.component_id {
155            return Err(MergeError::IdMismatch(
156                "ProtocolComponentStateDeltas".to_string(),
157                self.component_id.clone(),
158                other.component_id.clone(),
159            ));
160        }
161        for attr in &other.deleted_attributes {
162            self.updated_attributes.remove(attr);
163        }
164        for attr in other.updated_attributes.keys() {
165            self.deleted_attributes.remove(attr);
166        }
167        self.updated_attributes
168            .extend(other.updated_attributes);
169        self.deleted_attributes
170            .extend(other.deleted_attributes);
171        Ok(())
172    }
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
176pub struct ComponentBalance {
177    pub token: Address,
178    pub balance: Balance,
179    pub balance_float: f64,
180    pub modify_tx: TxHash,
181    pub component_id: ComponentId,
182}
183
184impl ComponentBalance {
185    pub fn new(
186        token: Address,
187        new_balance: Balance,
188        balance_float: f64,
189        modify_tx: TxHash,
190        component_id: &str,
191    ) -> Self {
192        Self {
193            token,
194            balance: new_balance,
195            balance_float,
196            modify_tx,
197            component_id: component_id.to_string(),
198        }
199    }
200}
201
202/// Token quality range filter
203///
204/// The quality range is considered inclusive and used as a filter, will be applied as such.
205#[derive(Debug, Clone)]
206pub struct QualityRange {
207    pub min: Option<i32>,
208    pub max: Option<i32>,
209}
210
211impl QualityRange {
212    pub fn new(min: i32, max: i32) -> Self {
213        Self { min: Some(min), max: Some(max) }
214    }
215
216    pub fn min_only(min: i32) -> Self {
217        Self { min: Some(min), max: None }
218    }
219
220    #[allow(non_snake_case)]
221    pub fn None() -> Self {
222        Self { min: None, max: None }
223    }
224}
225
226/// Updates grouped by their respective transaction.
227#[derive(Debug, Clone, PartialEq, Default)]
228pub struct ProtocolChangesWithTx {
229    pub new_protocol_components: HashMap<ComponentId, ProtocolComponent>,
230    pub protocol_states: HashMap<ComponentId, ProtocolComponentStateDelta>,
231    pub balance_changes: HashMap<ComponentId, HashMap<Bytes, ComponentBalance>>,
232    pub tx: Transaction,
233}
234
235impl ProtocolChangesWithTx {
236    /// Merges this update with another one.
237    ///
238    /// The method combines two `ProtocolStatesWithTx` instances under certain
239    /// conditions:
240    /// - The block from which both updates came should be the same. If the updates are from
241    ///   different blocks, the method will return an error.
242    /// - The transactions for each of the updates should be distinct. If they come from the same
243    ///   transaction, the method will return an error.
244    /// - The order of the transaction matters. The transaction from `other` must have occurred
245    ///   later than the self transaction. If the self transaction has a higher index than `other`,
246    ///   the method will return an error.
247    ///
248    /// The merged update keeps the transaction of `other`.
249    ///
250    /// # Errors
251    /// This method will return an error if any of the above conditions is violated.
252    pub fn merge(&mut self, other: ProtocolChangesWithTx) -> Result<(), MergeError> {
253        if self.tx.block_hash != other.tx.block_hash {
254            return Err(MergeError::BlockMismatch(
255                "ProtocolChangesWithTx".to_string(),
256                self.tx.block_hash.clone(),
257                other.tx.block_hash,
258            ));
259        }
260        if self.tx.hash == other.tx.hash {
261            return Err(MergeError::SameTransaction(
262                "ProtocolChangesWithTx".to_string(),
263                other.tx.hash,
264            ));
265        }
266        if self.tx.index > other.tx.index {
267            return Err(MergeError::TransactionOrderError(
268                "ProtocolChangesWithTx".to_string(),
269                self.tx.index,
270                other.tx.index,
271            ));
272        }
273        self.tx = other.tx;
274        // Merge protocol states
275        for (key, value) in other.protocol_states {
276            match self.protocol_states.entry(key) {
277                Entry::Occupied(mut entry) => {
278                    entry.get_mut().merge(value)?;
279                }
280                Entry::Vacant(entry) => {
281                    entry.insert(value);
282                }
283            }
284        }
285
286        // Merge token balances
287        for (component_id, balance_changes) in other.balance_changes {
288            let token_balances = self
289                .balance_changes
290                .entry(component_id)
291                .or_default();
292            for (token, balance) in balance_changes {
293                token_balances.insert(token, balance);
294            }
295        }
296
297        // Merge new protocol components
298        // Log a warning if a new protocol component for the same id already exists, because this
299        // should never happen.
300        for (key, value) in other.new_protocol_components {
301            match self.new_protocol_components.entry(key) {
302                Entry::Occupied(mut entry) => {
303                    warn!(
304                        "Overwriting new protocol component for id {} with a new one. This should never happen! Please check logic",
305                        entry.get().id
306                    );
307                    entry.insert(value);
308                }
309                Entry::Vacant(entry) => {
310                    entry.insert(value);
311                }
312            }
313        }
314
315        Ok(())
316    }
317}
318
319#[cfg(test)]
320mod test {
321    use rstest::rstest;
322
323    use super::*;
324    use crate::models::blockchain::fixtures as block_fixtures;
325
326    const HASH_256_0: &str = "0x0000000000000000000000000000000000000000000000000000000000000000";
327    const HASH_256_1: &str = "0x0000000000000000000000000000000000000000000000000000000000000001";
328
329    fn create_state(id: String) -> ProtocolComponentStateDelta {
330        let attributes1: HashMap<String, Bytes> = vec![
331            ("reserve1".to_owned(), Bytes::from(1000u64).lpad(32, 0)),
332            ("reserve2".to_owned(), Bytes::from(500u64).lpad(32, 0)),
333            ("static_attribute".to_owned(), Bytes::from(1u64).lpad(32, 0)),
334        ]
335        .into_iter()
336        .collect();
337        ProtocolComponentStateDelta {
338            component_id: id,
339            updated_attributes: attributes1,
340            deleted_attributes: HashSet::new(),
341        }
342    }
343
344    #[test]
345    fn test_merge_protocol_state_updates() {
346        let mut state_1 = create_state("State1".to_owned());
347        state_1
348            .updated_attributes
349            .insert("to_be_removed".to_owned(), Bytes::from(1u64).lpad(32, 0));
350        state_1.deleted_attributes = vec!["to_add_back".to_owned()]
351            .into_iter()
352            .collect();
353
354        let attributes2: HashMap<String, Bytes> = vec![
355            ("reserve1".to_owned(), Bytes::from(900u64).lpad(32, 0)),
356            ("reserve2".to_owned(), Bytes::from(550u64).lpad(32, 0)),
357            ("new_attribute".to_owned(), Bytes::from(1u64).lpad(32, 0)),
358            ("to_add_back".to_owned(), Bytes::from(200u64).lpad(32, 0)),
359        ]
360        .into_iter()
361        .collect();
362        let del_attributes2: HashSet<String> = vec!["to_be_removed".to_owned()]
363            .into_iter()
364            .collect();
365        let mut state_2 = create_state("State1".to_owned());
366        state_2.updated_attributes = attributes2;
367        state_2.deleted_attributes = del_attributes2;
368
369        let res = state_1.merge(state_2);
370
371        assert!(res.is_ok());
372        let expected_attributes: HashMap<String, Bytes> = vec![
373            ("reserve1".to_owned(), Bytes::from(900u64).lpad(32, 0)),
374            ("reserve2".to_owned(), Bytes::from(550u64).lpad(32, 0)),
375            ("static_attribute".to_owned(), Bytes::from(1u64).lpad(32, 0)),
376            ("new_attribute".to_owned(), Bytes::from(1u64).lpad(32, 0)),
377            ("to_add_back".to_owned(), Bytes::from(200u64).lpad(32, 0)),
378        ]
379        .into_iter()
380        .collect();
381        assert_eq!(state_1.updated_attributes, expected_attributes);
382        let expected_del_attributes: HashSet<String> = vec!["to_be_removed".to_owned()]
383            .into_iter()
384            .collect();
385        assert_eq!(state_1.deleted_attributes, expected_del_attributes);
386    }
387
388    fn protocol_state_with_tx() -> ProtocolChangesWithTx {
389        let state_1 = create_state("State1".to_owned());
390        let state_2 = create_state("State2".to_owned());
391        let states: HashMap<String, ProtocolComponentStateDelta> =
392            vec![(state_1.component_id.clone(), state_1), (state_2.component_id.clone(), state_2)]
393                .into_iter()
394                .collect();
395        ProtocolChangesWithTx {
396            protocol_states: states,
397            tx: block_fixtures::transaction01(),
398            ..Default::default()
399        }
400    }
401
402    #[test]
403    fn test_merge_protocol_state_update_with_tx() {
404        let mut base_state = protocol_state_with_tx();
405
406        let new_attributes: HashMap<String, Bytes> = vec![
407            ("reserve1".to_owned(), Bytes::from(600u64).lpad(32, 0)),
408            ("new_attribute".to_owned(), Bytes::from(10u64).lpad(32, 0)),
409        ]
410        .into_iter()
411        .collect();
412        let new_tx = block_fixtures::create_transaction(HASH_256_1, HASH_256_0, 11);
413        let new_states: HashMap<String, ProtocolComponentStateDelta> = vec![(
414            "State1".to_owned(),
415            ProtocolComponentStateDelta {
416                component_id: "State1".to_owned(),
417                updated_attributes: new_attributes,
418                deleted_attributes: HashSet::new(),
419            },
420        )]
421        .into_iter()
422        .collect();
423
424        let tx_update =
425            ProtocolChangesWithTx { protocol_states: new_states, tx: new_tx, ..Default::default() };
426
427        let res = base_state.merge(tx_update);
428
429        assert!(res.is_ok());
430        assert_eq!(base_state.protocol_states.len(), 2);
431        let expected_attributes: HashMap<String, Bytes> = vec![
432            ("reserve1".to_owned(), Bytes::from(600u64).lpad(32, 0)),
433            ("reserve2".to_owned(), Bytes::from(500u64).lpad(32, 0)),
434            ("static_attribute".to_owned(), Bytes::from(1u64).lpad(32, 0)),
435            ("new_attribute".to_owned(), Bytes::from(10u64).lpad(32, 0)),
436        ]
437        .into_iter()
438        .collect();
439        assert_eq!(
440            base_state
441                .protocol_states
442                .get("State1")
443                .unwrap()
444                .updated_attributes,
445            expected_attributes
446        );
447    }
448
449    #[rstest]
450    #[case::diff_block(
451    block_fixtures::create_transaction(HASH_256_1, HASH_256_1, 11),
452    Err(MergeError::BlockMismatch(
453        "ProtocolChangesWithTx".to_string(),
454        Bytes::zero(32),
455        HASH_256_1.into(),
456    ))
457    )]
458    #[case::same_tx(
459    block_fixtures::create_transaction(HASH_256_0, HASH_256_0, 11),
460    Err(MergeError::SameTransaction(
461        "ProtocolChangesWithTx".to_string(),
462        Bytes::zero(32),
463    ))
464    )]
465    #[case::lower_idx(
466    block_fixtures::create_transaction(HASH_256_1, HASH_256_0, 1),
467    Err(MergeError::TransactionOrderError(
468        "ProtocolChangesWithTx".to_string(),
469        10,
470        1,
471    ))
472    )]
473    fn test_merge_pool_state_update_with_tx_errors(
474        #[case] tx: Transaction,
475        #[case] exp: Result<(), MergeError>,
476    ) {
477        let mut base_state = protocol_state_with_tx();
478
479        let mut new_state = protocol_state_with_tx();
480        new_state.tx = tx;
481
482        let res = base_state.merge(new_state);
483
484        assert_eq!(res, exp);
485    }
486
487    #[test]
488    fn test_merge_protocol_state_update_wrong_id() {
489        let mut state1 = create_state("State1".to_owned());
490        let state2 = create_state("State2".to_owned());
491
492        let res = state1.merge(state2);
493
494        assert_eq!(
495            res,
496            Err(MergeError::IdMismatch(
497                "ProtocolComponentStateDeltas".to_string(),
498                "State1".to_string(),
499                "State2".to_string(),
500            ))
501        );
502    }
503}