tycho_common/models/
protocol.rs

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