tycho_common/models/
protocol.rs

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