Skip to main content

tycho_simulation/rfq/protocols/hashflow/
decoder.rs

1use std::collections::{HashMap, HashSet};
2
3use tycho_client::feed::synchronizer::ComponentWithState;
4use tycho_common::{models::token::Token, Bytes};
5
6use super::{
7    client_builder::HashflowClientBuilder,
8    models::{HashflowMarketMakerLevels, HashflowPair, HashflowPriceLevel},
9    state::HashflowState,
10};
11use crate::{
12    protocol::{
13        errors::InvalidSnapshotError,
14        models::{DecoderContext, TryFromWithBlock},
15    },
16    rfq::{constants::get_hashflow_auth, models::TimestampHeader},
17};
18
19impl TryFromWithBlock<ComponentWithState, TimestampHeader> for HashflowState {
20    type Error = InvalidSnapshotError;
21
22    async fn try_from_with_header(
23        snapshot: ComponentWithState,
24        _timestamp_header: TimestampHeader,
25        _account_balances: &HashMap<Bytes, HashMap<Bytes, Bytes>>,
26        all_tokens: &HashMap<Bytes, Token>,
27        _decoder_context: &DecoderContext,
28    ) -> Result<Self, Self::Error> {
29        let state_attrs = snapshot.state.attributes;
30
31        if snapshot.component.tokens.len() != 2 {
32            return Err(InvalidSnapshotError::ValueError(
33                "Component must have 2 tokens (base and quote)".to_string(),
34            ));
35        }
36
37        let base_token_address = &snapshot.component.tokens[0];
38        let quote_token_address = &snapshot.component.tokens[1];
39
40        let base_token = all_tokens
41            .get(base_token_address)
42            .ok_or_else(|| {
43                InvalidSnapshotError::ValueError(format!(
44                    "Base token not found: {base_token_address}"
45                ))
46            })?
47            .clone();
48
49        let quote_token = all_tokens
50            .get(quote_token_address)
51            .ok_or_else(|| {
52                InvalidSnapshotError::ValueError(format!(
53                    "Quote token not found: {quote_token_address}"
54                ))
55            })?
56            .clone();
57
58        // Parse the price levels array from the component attributes.
59        // A missing levels key indicates no levels for this market maker.
60        let empty_levels_array: Bytes = "[]".as_bytes().to_vec().into();
61        let levels_data = state_attrs
62            .get("levels")
63            .unwrap_or(&empty_levels_array);
64
65        let price_levels: Vec<HashflowPriceLevel> = serde_json::from_slice(levels_data)
66            .map_err(|e| InvalidSnapshotError::ValueError(format!("Invalid levels JSON: {e}")))?;
67
68        let market_maker = state_attrs
69            .get("mm")
70            .ok_or_else(|| {
71                InvalidSnapshotError::MissingAttribute("mm attribute not found".to_string())
72            })
73            .and_then(|mm_bytes| {
74                String::from_utf8(mm_bytes.to_vec()).map_err(|_| {
75                    InvalidSnapshotError::ValueError("Invalid mm encoding".to_string())
76                })
77            })?;
78
79        // Create the HashflowMarketMakerLevels using token pair from component
80        let levels = HashflowMarketMakerLevels {
81            pair: HashflowPair {
82                base_token: base_token_address.clone(),
83                quote_token: quote_token_address.clone(),
84            },
85            levels: price_levels,
86        };
87
88        // Create HashFlow client with authentication from environment variables
89        let auth = get_hashflow_auth().map_err(|e| {
90            InvalidSnapshotError::ValueError(format!("Failed to get Hashflow authentication: {e}"))
91        })?;
92
93        let client = HashflowClientBuilder::new(snapshot.component.chain, auth.user, auth.key)
94            .tokens(HashSet::from([base_token_address.clone(), quote_token_address.clone()]))
95            .build()
96            .map_err(|e| {
97                InvalidSnapshotError::ValueError(format!("Couldn't create HashflowClient: {e}"))
98            })?;
99
100        Ok(HashflowState::new(base_token, quote_token, levels, market_maker, client))
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use std::env;
107
108    use tycho_common::models::{
109        protocol::{ProtocolComponent, ProtocolComponentState},
110        Chain, ChangeType,
111    };
112
113    use super::*;
114
115    fn wbtc() -> Token {
116        Token::new(
117            &hex::decode("2260fac5e5542a773aa44fbcfedf7c193bc2c599")
118                .unwrap()
119                .into(),
120            "WBTC",
121            8,
122            0,
123            &[Some(10_000)],
124            Chain::Ethereum,
125            100,
126        )
127    }
128
129    fn usdc() -> Token {
130        Token::new(
131            &hex::decode("a0b86991c6218a76c1d19d4a2e9eb0ce3606eb48")
132                .unwrap()
133                .into(),
134            "USDC",
135            6,
136            0,
137            &[Some(10_000)],
138            Chain::Ethereum,
139            100,
140        )
141    }
142
143    fn create_test_levels() -> serde_json::Value {
144        serde_json::json!([
145            {
146                "q": "1.5",
147                "p": "65000.0"
148            },
149            {
150                "q": "2.0",
151                "p": "64950.0"
152            },
153            {
154                "q": "0.5",
155                "p": "65100.0"
156            }
157        ])
158    }
159
160    fn create_test_snapshot() -> (ComponentWithState, HashMap<Bytes, Token>) {
161        let wbtc_token = wbtc();
162        let usdc_token = usdc();
163        let levels = create_test_levels();
164
165        let mut tokens = HashMap::new();
166        tokens.insert(wbtc_token.address.clone(), wbtc_token.clone());
167        tokens.insert(usdc_token.address.clone(), usdc_token.clone());
168
169        let mut state_attributes = HashMap::new();
170
171        // Serialize the levels to JSON
172        let levels_json = serde_json::to_vec(&levels).expect("Failed to serialize levels");
173        state_attributes.insert("levels".to_string(), levels_json.into());
174
175        // Add market maker name
176        state_attributes.insert(
177            "mm".to_string(),
178            "test_market_maker"
179                .as_bytes()
180                .to_vec()
181                .into(),
182        );
183
184        let snapshot = ComponentWithState {
185            state: ProtocolComponentState {
186                attributes: state_attributes,
187                component_id: "hashflow_wbtc_usdc".to_string(),
188                balances: HashMap::new(),
189            },
190            component: ProtocolComponent {
191                id: "hashflow_wbtc_usdc".to_string(),
192                protocol_system: "hashflow".to_string(),
193                protocol_type_name: "hashflow".to_string(),
194                chain: Chain::Ethereum,
195                tokens: vec![wbtc_token.address.clone(), usdc_token.address.clone()],
196                contract_addresses: Vec::new(),
197                static_attributes: HashMap::new(),
198                change: ChangeType::Creation,
199                creation_tx: Bytes::default(),
200                created_at: chrono::NaiveDateTime::default(),
201            },
202            component_tvl: None,
203            entrypoints: Vec::new(),
204        };
205
206        (snapshot, tokens)
207    }
208
209    #[tokio::test]
210    async fn test_try_from_with_header() {
211        env::set_var("HASHFLOW_USER", "test_user");
212        env::set_var("HASHFLOW_KEY", "test_key");
213
214        let (snapshot, tokens) = create_test_snapshot();
215
216        let result = HashflowState::try_from_with_header(
217            snapshot,
218            TimestampHeader { timestamp: 1703097600u64 },
219            &HashMap::new(),
220            &tokens,
221            &DecoderContext::new(),
222        )
223        .await
224        .expect("create state from snapshot");
225
226        assert_eq!(result.base_token.symbol, "WBTC");
227        assert_eq!(result.quote_token.symbol, "USDC");
228        assert_eq!(result.market_maker, "test_market_maker");
229        assert_eq!(result.levels.levels.len(), 3);
230        assert_eq!(result.levels.levels[0].quantity, 1.5);
231        assert_eq!(result.levels.levels[0].price, 65000.0);
232        assert_eq!(result.levels.levels[1].quantity, 2.0);
233        assert_eq!(result.levels.levels[1].price, 64950.0);
234        assert_eq!(result.levels.levels[2].quantity, 0.5);
235        assert_eq!(result.levels.levels[2].price, 65100.0);
236    }
237
238    #[tokio::test]
239    async fn test_try_from_missing_levels() {
240        env::set_var("HASHFLOW_USER", "test_user");
241        env::set_var("HASHFLOW_KEY", "test_key");
242
243        let (mut snapshot, tokens) = create_test_snapshot();
244        snapshot
245            .state
246            .attributes
247            .remove("levels");
248
249        let result = HashflowState::try_from_with_header(
250            snapshot,
251            TimestampHeader::default(),
252            &HashMap::new(),
253            &tokens,
254            &DecoderContext::new(),
255        )
256        .await
257        .expect("create state with missing levels should default to empty levels");
258
259        // Should succeed with empty levels
260        assert_eq!(result.base_token.symbol, "WBTC");
261        assert_eq!(result.quote_token.symbol, "USDC");
262        assert_eq!(result.levels.levels.len(), 0);
263    }
264
265    #[tokio::test]
266    async fn test_try_from_missing_token() {
267        env::set_var("HASHFLOW_USER", "test_user");
268        env::set_var("HASHFLOW_KEY", "test_key");
269
270        // Test missing second token (only one token in array)
271        let (mut snapshot, tokens) = create_test_snapshot();
272        snapshot.component.tokens.pop(); // Remove the second token
273
274        let result = HashflowState::try_from_with_header(
275            snapshot,
276            TimestampHeader::default(),
277            &HashMap::new(),
278            &tokens,
279            &DecoderContext::new(),
280        )
281        .await;
282
283        assert!(result.is_err());
284        assert!(matches!(result.unwrap_err(), InvalidSnapshotError::ValueError(_)));
285    }
286
287    #[tokio::test]
288    async fn test_try_from_too_many_tokens() {
289        env::set_var("HASHFLOW_USER", "test_user");
290        env::set_var("HASHFLOW_KEY", "test_key");
291
292        // Test with three tokens instead of two
293        let (mut snapshot, mut tokens) = create_test_snapshot();
294
295        let dai_token = Token::new(
296            &hex::decode("6b175474e89094c44da98b954eedeac495271d0f")
297                .unwrap()
298                .into(),
299            "DAI",
300            18,
301            0,
302            &[Some(10_000)],
303            Chain::Ethereum,
304            100,
305        );
306
307        tokens.insert(dai_token.address.clone(), dai_token.clone());
308        snapshot
309            .component
310            .tokens
311            .push(dai_token.address);
312
313        let result = HashflowState::try_from_with_header(
314            snapshot,
315            TimestampHeader::default(),
316            &HashMap::new(),
317            &tokens,
318            &DecoderContext::new(),
319        )
320        .await;
321
322        assert!(result.is_err());
323        assert!(matches!(result.unwrap_err(), InvalidSnapshotError::ValueError(_)));
324    }
325
326    #[tokio::test]
327    async fn test_try_from_invalid_levels_json() {
328        env::set_var("HASHFLOW_USER", "test_user");
329        env::set_var("HASHFLOW_KEY", "test_key");
330
331        let (mut snapshot, tokens) = create_test_snapshot();
332
333        // Insert invalid JSON for levels
334        snapshot.state.attributes.insert(
335            "levels".to_string(),
336            "invalid json"
337                .as_bytes()
338                .to_vec()
339                .into(),
340        );
341
342        let result = HashflowState::try_from_with_header(
343            snapshot,
344            TimestampHeader::default(),
345            &HashMap::new(),
346            &tokens,
347            &DecoderContext::new(),
348        )
349        .await;
350
351        assert!(result.is_err());
352        assert!(matches!(result.unwrap_err(), InvalidSnapshotError::ValueError(_)));
353    }
354
355    #[tokio::test]
356    async fn test_try_from_missing_mm() {
357        env::set_var("HASHFLOW_USER", "test_user");
358        env::set_var("HASHFLOW_KEY", "test_key");
359
360        let (mut snapshot, tokens) = create_test_snapshot();
361        snapshot.state.attributes.remove("mm");
362
363        let result = HashflowState::try_from_with_header(
364            snapshot,
365            TimestampHeader::default(),
366            &HashMap::new(),
367            &tokens,
368            &DecoderContext::new(),
369        )
370        .await;
371
372        assert!(result.is_err());
373        assert!(matches!(result.unwrap_err(), InvalidSnapshotError::MissingAttribute(_)));
374    }
375}