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    /// The end user's EOA (`tx.origin`), for RFQ providers that ask for it. `None` lets the
282    /// provider use its own default.
283    pub origin: Option<Bytes>,
284}
285
286impl From<dto::ProtocolStateDelta> for ProtocolComponentStateDelta {
287    fn from(value: dto::ProtocolStateDelta) -> Self {
288        Self {
289            component_id: value.component_id,
290            updated_attributes: value.updated_attributes,
291            deleted_attributes: value.deleted_attributes,
292            created_attributes: HashSet::new(),
293        }
294    }
295}
296
297impl From<dto::ComponentBalance> for ComponentBalance {
298    fn from(value: dto::ComponentBalance) -> Self {
299        Self {
300            token: value.token,
301            balance: value.balance,
302            balance_float: value.balance_float,
303            modify_tx: value.modify_tx,
304            component_id: value.component_id,
305        }
306    }
307}
308
309impl From<dto::ProtocolComponent> for ProtocolComponent {
310    fn from(value: dto::ProtocolComponent) -> Self {
311        Self {
312            id: value.id,
313            protocol_system: value.protocol_system,
314            protocol_type_name: value.protocol_type_name,
315            chain: value.chain.into(),
316            tokens: value.tokens,
317            contract_addresses: value.contract_ids,
318            static_attributes: value.static_attributes,
319            change: value.change.into(),
320            creation_tx: value.creation_tx,
321            created_at: value.created_at,
322        }
323    }
324}
325
326impl From<dto::ResponseProtocolState> for ProtocolComponentState {
327    fn from(value: dto::ResponseProtocolState) -> Self {
328        Self {
329            component_id: value.component_id,
330            attributes: value.attributes,
331            balances: value.balances,
332        }
333    }
334}
335
336#[cfg(test)]
337mod test {
338    use super::*;
339
340    fn create_state(id: String) -> ProtocolComponentStateDelta {
341        let attributes1: HashMap<String, Bytes> = vec![
342            ("reserve1".to_owned(), Bytes::from(1000u64).lpad(32, 0)),
343            ("reserve2".to_owned(), Bytes::from(500u64).lpad(32, 0)),
344            ("static_attribute".to_owned(), Bytes::from(1u64).lpad(32, 0)),
345        ]
346        .into_iter()
347        .collect();
348        ProtocolComponentStateDelta {
349            component_id: id,
350            updated_attributes: attributes1,
351            deleted_attributes: HashSet::new(),
352            ..Default::default()
353        }
354    }
355
356    #[test]
357    fn test_merge_protocol_state_updates() {
358        let mut state_1 = create_state("State1".to_owned());
359        state_1
360            .updated_attributes
361            .insert("to_be_removed".to_owned(), Bytes::from(1u64).lpad(32, 0));
362        state_1.deleted_attributes = vec!["to_add_back".to_owned()]
363            .into_iter()
364            .collect();
365
366        let attributes2: HashMap<String, Bytes> = vec![
367            ("reserve1".to_owned(), Bytes::from(900u64).lpad(32, 0)),
368            ("reserve2".to_owned(), Bytes::from(550u64).lpad(32, 0)),
369            ("new_attribute".to_owned(), Bytes::from(1u64).lpad(32, 0)),
370            ("to_add_back".to_owned(), Bytes::from(200u64).lpad(32, 0)),
371        ]
372        .into_iter()
373        .collect();
374        let del_attributes2: HashSet<String> = vec!["to_be_removed".to_owned()]
375            .into_iter()
376            .collect();
377        let mut state_2 = create_state("State1".to_owned());
378        state_2.updated_attributes = attributes2;
379        state_2.deleted_attributes = del_attributes2;
380
381        let res = state_1.merge(state_2);
382
383        assert!(res.is_ok());
384        let expected_attributes: HashMap<String, Bytes> = vec![
385            ("reserve1".to_owned(), Bytes::from(900u64).lpad(32, 0)),
386            ("reserve2".to_owned(), Bytes::from(550u64).lpad(32, 0)),
387            ("static_attribute".to_owned(), Bytes::from(1u64).lpad(32, 0)),
388            ("new_attribute".to_owned(), Bytes::from(1u64).lpad(32, 0)),
389            ("to_add_back".to_owned(), Bytes::from(200u64).lpad(32, 0)),
390        ]
391        .into_iter()
392        .collect();
393        assert_eq!(state_1.updated_attributes, expected_attributes);
394        let expected_del_attributes: HashSet<String> = vec!["to_be_removed".to_owned()]
395            .into_iter()
396            .collect();
397        assert_eq!(state_1.deleted_attributes, expected_del_attributes);
398    }
399
400    #[test]
401    fn test_merge_protocol_state_update_wrong_id() {
402        let mut state1 = create_state("State1".to_owned());
403        let state2 = create_state("State2".to_owned());
404
405        let res = state1.merge(state2);
406
407        assert_eq!(
408            res,
409            Err(MergeError::IdMismatch(
410                "ProtocolComponentStateDeltas".to_string(),
411                "State1".to_string(),
412                "State2".to_string(),
413            ))
414        );
415    }
416}