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        DeltaError, 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<(), DeltaError> {
93        if self.component_id != delta.component_id {
94            return Err(DeltaError::IdMismatch(
95                self.component_id.clone(),
96                delta.component_id.clone(),
97            ));
98        }
99        self.attributes
100            .extend(delta.updated_attributes.clone());
101
102        self.attributes
103            .retain(|attr, _| !delta.deleted_attributes.contains(attr));
104
105        Ok(())
106    }
107
108    /// Applies balance deltas to this state.
109    ///
110    /// This method assumes that the passed delta is "newer" than the current state.
111    pub fn apply_balance_delta(
112        &mut self,
113        delta: &HashMap<Bytes, ComponentBalance>,
114    ) -> Result<(), DeltaError> {
115        self.balances.extend(
116            delta
117                .iter()
118                .map(|(k, v)| (k.clone(), v.balance.clone())),
119        );
120
121        Ok(())
122    }
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
126pub struct ProtocolComponentStateDelta {
127    pub component_id: ComponentId,
128    pub updated_attributes: HashMap<AttrStoreKey, StoreVal>,
129    pub deleted_attributes: HashSet<AttrStoreKey>,
130}
131
132impl ProtocolComponentStateDelta {
133    pub fn new(
134        component_id: &str,
135        updated_attributes: HashMap<AttrStoreKey, StoreVal>,
136        deleted_attributes: HashSet<AttrStoreKey>,
137    ) -> Self {
138        Self { component_id: component_id.to_string(), updated_attributes, deleted_attributes }
139    }
140
141    /// Merges this update with another one.
142    ///
143    /// The method combines two `ProtocolComponentStateDelta` instances if they are for the same
144    /// protocol component.
145    ///
146    /// NB: It is assumed that `other` is a more recent update than `self` is and the two are
147    /// combined accordingly.
148    ///
149    /// # Errors
150    /// This method will return `CoreError::MergeError` if any of the above
151    /// conditions is violated.
152    pub fn merge(&mut self, other: ProtocolComponentStateDelta) -> Result<(), String> {
153        if self.component_id != other.component_id {
154            return Err(format!(
155                "Can't merge ProtocolStates from differing identities; Expected {}, got {}",
156                self.component_id, other.component_id
157            ));
158        }
159        for attr in &other.deleted_attributes {
160            self.updated_attributes.remove(attr);
161        }
162        for attr in other.updated_attributes.keys() {
163            self.deleted_attributes.remove(attr);
164        }
165        self.updated_attributes
166            .extend(other.updated_attributes);
167        self.deleted_attributes
168            .extend(other.deleted_attributes);
169        Ok(())
170    }
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
174pub struct ComponentBalance {
175    pub token: Address,
176    pub balance: Balance,
177    pub balance_float: f64,
178    pub modify_tx: TxHash,
179    pub component_id: ComponentId,
180}
181
182impl ComponentBalance {
183    pub fn new(
184        token: Address,
185        new_balance: Balance,
186        balance_float: f64,
187        modify_tx: TxHash,
188        component_id: &str,
189    ) -> Self {
190        Self {
191            token,
192            balance: new_balance,
193            balance_float,
194            modify_tx,
195            component_id: component_id.to_string(),
196        }
197    }
198}
199
200/// Token quality range filter
201///
202/// The quality range is considered inclusive and used as a filter, will be applied as such.
203#[derive(Debug, Clone)]
204pub struct QualityRange {
205    pub min: Option<i32>,
206    pub max: Option<i32>,
207}
208
209impl QualityRange {
210    pub fn new(min: i32, max: i32) -> Self {
211        Self { min: Some(min), max: Some(max) }
212    }
213
214    pub fn min_only(min: i32) -> Self {
215        Self { min: Some(min), max: None }
216    }
217
218    #[allow(non_snake_case)]
219    pub fn None() -> Self {
220        Self { min: None, max: None }
221    }
222}
223
224/// Updates grouped by their respective transaction.
225#[derive(Debug, Clone, PartialEq, Default)]
226pub struct ProtocolChangesWithTx {
227    pub new_protocol_components: HashMap<ComponentId, ProtocolComponent>,
228    pub protocol_states: HashMap<ComponentId, ProtocolComponentStateDelta>,
229    pub balance_changes: HashMap<ComponentId, HashMap<Bytes, ComponentBalance>>,
230    pub tx: Transaction,
231}
232
233impl ProtocolChangesWithTx {
234    /// Merges this update with another one.
235    ///
236    /// The method combines two `ProtocolStatesWithTx` instances under certain
237    /// conditions:
238    /// - The block from which both updates came should be the same. If the updates are from
239    ///   different blocks, the method will return an error.
240    /// - The transactions for each of the updates should be distinct. If they come from the same
241    ///   transaction, the method will return an error.
242    /// - The order of the transaction matters. The transaction from `other` must have occurred
243    ///   later than the self transaction. If the self transaction has a higher index than `other`,
244    ///   the method will return an error.
245    ///
246    /// The merged update keeps the transaction of `other`.
247    ///
248    /// # Errors
249    /// This method will return an error if any of the above conditions is violated.
250    pub fn merge(&mut self, other: ProtocolChangesWithTx) -> Result<(), String> {
251        if self.tx.block_hash != other.tx.block_hash {
252            return Err(format!(
253                "Can't merge ProtocolStates from different blocks: {:x} != {:x}",
254                self.tx.block_hash, other.tx.block_hash,
255            ));
256        }
257        if self.tx.hash == other.tx.hash {
258            return Err(format!(
259                "Can't merge ProtocolStates from the same transaction: {:x}",
260                self.tx.hash
261            ));
262        }
263        if self.tx.index > other.tx.index {
264            return Err(format!(
265                "Can't merge ProtocolStates with lower transaction index: {} > {}",
266                self.tx.index, other.tx.index
267            ));
268        }
269        self.tx = other.tx;
270        // Merge protocol states
271        for (key, value) in other.protocol_states {
272            match self.protocol_states.entry(key) {
273                Entry::Occupied(mut entry) => {
274                    entry.get_mut().merge(value)?;
275                }
276                Entry::Vacant(entry) => {
277                    entry.insert(value);
278                }
279            }
280        }
281
282        // Merge token balances
283        for (component_id, balance_changes) in other.balance_changes {
284            let token_balances = self
285                .balance_changes
286                .entry(component_id)
287                .or_default();
288            for (token, balance) in balance_changes {
289                token_balances.insert(token, balance);
290            }
291        }
292
293        // Merge new protocol components
294        // Log a warning if a new protocol component for the same id already exists, because this
295        // should never happen.
296        for (key, value) in other.new_protocol_components {
297            match self.new_protocol_components.entry(key) {
298                Entry::Occupied(mut entry) => {
299                    warn!(
300                        "Overwriting new protocol component for id {} with a new one. This should never happen! Please check logic",
301                        entry.get().id
302                    );
303                    entry.insert(value);
304                }
305                Entry::Vacant(entry) => {
306                    entry.insert(value);
307                }
308            }
309        }
310
311        Ok(())
312    }
313}
314
315#[cfg(test)]
316mod test {
317    use rstest::rstest;
318
319    use super::*;
320    use crate::models::blockchain::fixtures as block_fixtures;
321
322    const HASH_256_0: &str = "0x0000000000000000000000000000000000000000000000000000000000000000";
323    const HASH_256_1: &str = "0x0000000000000000000000000000000000000000000000000000000000000001";
324
325    fn create_state(id: String) -> ProtocolComponentStateDelta {
326        let attributes1: HashMap<String, Bytes> = vec![
327            ("reserve1".to_owned(), Bytes::from(1000u64).lpad(32, 0)),
328            ("reserve2".to_owned(), Bytes::from(500u64).lpad(32, 0)),
329            ("static_attribute".to_owned(), Bytes::from(1u64).lpad(32, 0)),
330        ]
331        .into_iter()
332        .collect();
333        ProtocolComponentStateDelta {
334            component_id: id,
335            updated_attributes: attributes1,
336            deleted_attributes: HashSet::new(),
337        }
338    }
339
340    #[test]
341    fn test_merge_protocol_state_updates() {
342        let mut state_1 = create_state("State1".to_owned());
343        state_1
344            .updated_attributes
345            .insert("to_be_removed".to_owned(), Bytes::from(1u64).lpad(32, 0));
346        state_1.deleted_attributes = vec!["to_add_back".to_owned()]
347            .into_iter()
348            .collect();
349
350        let attributes2: HashMap<String, Bytes> = vec![
351            ("reserve1".to_owned(), Bytes::from(900u64).lpad(32, 0)),
352            ("reserve2".to_owned(), Bytes::from(550u64).lpad(32, 0)),
353            ("new_attribute".to_owned(), Bytes::from(1u64).lpad(32, 0)),
354            ("to_add_back".to_owned(), Bytes::from(200u64).lpad(32, 0)),
355        ]
356        .into_iter()
357        .collect();
358        let del_attributes2: HashSet<String> = vec!["to_be_removed".to_owned()]
359            .into_iter()
360            .collect();
361        let mut state_2 = create_state("State1".to_owned());
362        state_2.updated_attributes = attributes2;
363        state_2.deleted_attributes = del_attributes2;
364
365        let res = state_1.merge(state_2);
366
367        assert!(res.is_ok());
368        let expected_attributes: HashMap<String, Bytes> = vec![
369            ("reserve1".to_owned(), Bytes::from(900u64).lpad(32, 0)),
370            ("reserve2".to_owned(), Bytes::from(550u64).lpad(32, 0)),
371            ("static_attribute".to_owned(), Bytes::from(1u64).lpad(32, 0)),
372            ("new_attribute".to_owned(), Bytes::from(1u64).lpad(32, 0)),
373            ("to_add_back".to_owned(), Bytes::from(200u64).lpad(32, 0)),
374        ]
375        .into_iter()
376        .collect();
377        assert_eq!(state_1.updated_attributes, expected_attributes);
378        let expected_del_attributes: HashSet<String> = vec!["to_be_removed".to_owned()]
379            .into_iter()
380            .collect();
381        assert_eq!(state_1.deleted_attributes, expected_del_attributes);
382    }
383
384    fn protocol_state_with_tx() -> ProtocolChangesWithTx {
385        let state_1 = create_state("State1".to_owned());
386        let state_2 = create_state("State2".to_owned());
387        let states: HashMap<String, ProtocolComponentStateDelta> =
388            vec![(state_1.component_id.clone(), state_1), (state_2.component_id.clone(), state_2)]
389                .into_iter()
390                .collect();
391        ProtocolChangesWithTx {
392            protocol_states: states,
393            tx: block_fixtures::transaction01(),
394            ..Default::default()
395        }
396    }
397
398    #[test]
399    fn test_merge_protocol_state_update_with_tx() {
400        let mut base_state = protocol_state_with_tx();
401
402        let new_attributes: HashMap<String, Bytes> = vec![
403            ("reserve1".to_owned(), Bytes::from(600u64).lpad(32, 0)),
404            ("new_attribute".to_owned(), Bytes::from(10u64).lpad(32, 0)),
405        ]
406        .into_iter()
407        .collect();
408        let new_tx = block_fixtures::create_transaction(HASH_256_1, HASH_256_0, 11);
409        let new_states: HashMap<String, ProtocolComponentStateDelta> = vec![(
410            "State1".to_owned(),
411            ProtocolComponentStateDelta {
412                component_id: "State1".to_owned(),
413                updated_attributes: new_attributes,
414                deleted_attributes: HashSet::new(),
415            },
416        )]
417        .into_iter()
418        .collect();
419
420        let tx_update =
421            ProtocolChangesWithTx { protocol_states: new_states, tx: new_tx, ..Default::default() };
422
423        let res = base_state.merge(tx_update);
424
425        assert!(res.is_ok());
426        assert_eq!(base_state.protocol_states.len(), 2);
427        let expected_attributes: HashMap<String, Bytes> = vec![
428            ("reserve1".to_owned(), Bytes::from(600u64).lpad(32, 0)),
429            ("reserve2".to_owned(), Bytes::from(500u64).lpad(32, 0)),
430            ("static_attribute".to_owned(), Bytes::from(1u64).lpad(32, 0)),
431            ("new_attribute".to_owned(), Bytes::from(10u64).lpad(32, 0)),
432        ]
433        .into_iter()
434        .collect();
435        assert_eq!(
436            base_state
437                .protocol_states
438                .get("State1")
439                .unwrap()
440                .updated_attributes,
441            expected_attributes
442        );
443    }
444
445    #[rstest]
446    #[case::diff_block(
447    block_fixtures::create_transaction(HASH_256_1, HASH_256_1, 11),
448    Err(format ! ("Can't merge ProtocolStates from different blocks: {:x} != {}", Bytes::zero(32), HASH_256_1))
449    )]
450    #[case::same_tx(
451    block_fixtures::create_transaction(HASH_256_0, HASH_256_0, 11),
452    Err(format ! ("Can't merge ProtocolStates from the same transaction: {:x}", Bytes::zero(32)))
453    )]
454    #[case::lower_idx(
455    block_fixtures::create_transaction(HASH_256_1, HASH_256_0, 1),
456    Err("Can't merge ProtocolStates with lower transaction index: 10 > 1".to_owned())
457    )]
458    fn test_merge_pool_state_update_with_tx_errors(
459        #[case] tx: Transaction,
460        #[case] exp: Result<(), String>,
461    ) {
462        let mut base_state = protocol_state_with_tx();
463
464        let mut new_state = protocol_state_with_tx();
465        new_state.tx = tx;
466
467        let res = base_state.merge(new_state);
468
469        assert_eq!(res, exp);
470    }
471
472    #[test]
473    fn test_merge_protocol_state_update_wrong_id() {
474        let mut state1 = create_state("State1".to_owned());
475        let state2 = create_state("State2".to_owned());
476
477        let res = state1.merge(state2);
478
479        assert_eq!(
480            res,
481            Err(
482                "Can't merge ProtocolStates from differing identities; Expected State1, got State2"
483                    .to_owned()
484            )
485        );
486    }
487}