Skip to main content

tycho_simulation/rfq/protocols/native/
decoder.rs

1use std::collections::{HashMap, HashSet};
2
3use tycho_client::feed::synchronizer::ComponentWithState;
4use tycho_common::{models::token::Token, Bytes};
5
6use super::{client_builder::NativeClientBuilder, models::NativePriceData, state::NativeState};
7use crate::{
8    protocol::{
9        errors::InvalidSnapshotError,
10        models::{DecoderContext, TryFromWithBlock},
11    },
12    rfq::models::TimestampHeader,
13};
14
15impl TryFromWithBlock<ComponentWithState, TimestampHeader> for NativeState {
16    type Error = InvalidSnapshotError;
17
18    async fn try_from_with_header(
19        snapshot: ComponentWithState,
20        _timestamp_header: TimestampHeader,
21        _account_balances: &HashMap<Bytes, HashMap<Bytes, Bytes>>,
22        all_tokens: &HashMap<Bytes, Token>,
23        _decoder_context: &DecoderContext,
24    ) -> Result<Self, Self::Error> {
25        let state_attrs = snapshot.state.attributes;
26
27        if snapshot.component.tokens.len() != 2 {
28            return Err(InvalidSnapshotError::ValueError(
29                "Component must have 2 tokens (base and quote)".to_string(),
30            ));
31        }
32
33        let base_token_address = &snapshot.component.tokens[0];
34        let quote_token_address = &snapshot.component.tokens[1];
35
36        let base_token = all_tokens
37            .get(base_token_address)
38            .ok_or_else(|| {
39                InvalidSnapshotError::ValueError(format!(
40                    "Base token not found: {base_token_address}"
41                ))
42            })?
43            .clone();
44
45        let quote_token = all_tokens
46            .get(quote_token_address)
47            .ok_or_else(|| {
48                InvalidSnapshotError::ValueError(format!(
49                    "Quote token not found: {quote_token_address}"
50                ))
51            })?
52            .clone();
53
54        // Parse the Relay orderbook snapshot stored by the stream.
55        let book_data = state_attrs
56            .get("book")
57            .ok_or_else(|| InvalidSnapshotError::MissingAttribute("book".to_string()))?;
58
59        let book: NativePriceData = serde_json::from_slice(book_data)
60            .map_err(|e| InvalidSnapshotError::ValueError(format!("Invalid book JSON: {e}")))?;
61
62        let client_builder =
63            NativeClientBuilder::from_env(snapshot.component.chain).map_err(|e| {
64                InvalidSnapshotError::ValueError(format!(
65                    "Failed to get Native Relay authentication: {e}"
66                ))
67            })?;
68
69        let client = client_builder
70            .tokens(HashSet::from([base_token.address.clone(), quote_token.address.clone()]))
71            .build()
72            .map_err(|e| {
73                InvalidSnapshotError::MissingAttribute(format!("Couldn't create NativeClient: {e}"))
74            })?;
75
76        NativeState::new(base_token, quote_token, book, client)
77            .map_err(|e| InvalidSnapshotError::ValueError(e.to_string()))
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use std::{collections::HashMap, env};
84
85    use tycho_common::models::{
86        protocol::{ProtocolComponent, ProtocolComponentState},
87        Chain, ChangeType,
88    };
89
90    use super::*;
91    use crate::rfq::protocols::native::models::NativePriceLevel;
92
93    fn weth() -> Token {
94        Token::new(
95            &hex::decode("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2")
96                .unwrap()
97                .into(),
98            "WETH",
99            18,
100            0,
101            &[Some(10_000)],
102            Chain::Ethereum,
103            100,
104        )
105    }
106
107    fn usdc() -> Token {
108        Token::new(
109            &hex::decode("a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48")
110                .unwrap()
111                .into(),
112            "USDC",
113            6,
114            0,
115            &[Some(10_000)],
116            Chain::Ethereum,
117            100,
118        )
119    }
120
121    fn create_test_book() -> NativePriceData {
122        NativePriceData {
123            base_address: weth().address,
124            quote_address: usdc().address,
125            minimum_in_base: 0.0,
126            minimum_in_quote: 0.0,
127            minimum_out_base: 0.0,
128            minimum_out_quote: 0.0,
129            bids: vec![NativePriceLevel { price: 3000.0, quantity: 1.5 }],
130            asks: vec![NativePriceLevel { price: 3001.0, quantity: 2.0 }],
131        }
132    }
133
134    fn create_test_snapshot() -> (ComponentWithState, HashMap<Bytes, Token>) {
135        let weth_token = weth();
136        let usdc_token = usdc();
137        let book = create_test_book();
138
139        let mut tokens = HashMap::new();
140        tokens.insert(weth_token.address.clone(), weth_token.clone());
141        tokens.insert(usdc_token.address.clone(), usdc_token.clone());
142
143        let mut state_attributes = HashMap::new();
144
145        let book_json = serde_json::to_vec(&book).expect("Failed to serialize book");
146        state_attributes.insert("book".to_string(), book_json.into());
147
148        let snapshot = ComponentWithState {
149            state: ProtocolComponentState {
150                attributes: state_attributes,
151                component_id: "native_market_1".to_string(),
152                balances: HashMap::new(),
153            },
154            component: ProtocolComponent {
155                id: "native_market_1".to_string(),
156                protocol_system: "rfq:native".to_string(),
157                protocol_type_name: "native_relay_pool".to_string(),
158                chain: Chain::Ethereum,
159                tokens: vec![weth_token.address.clone(), usdc_token.address.clone()],
160                contract_addresses: Vec::new(),
161                static_attributes: HashMap::new(),
162                change: ChangeType::Creation,
163                creation_tx: Bytes::default(),
164                created_at: chrono::NaiveDateTime::default(),
165            },
166            component_tvl: Some(4500.0),
167            entrypoints: Vec::new(),
168        };
169
170        (snapshot, tokens)
171    }
172
173    #[tokio::test]
174    async fn test_try_from_with_header() {
175        env::set_var("NATIVE_API_KEY", "test-api-key");
176
177        let (snapshot, tokens) = create_test_snapshot();
178
179        let result = NativeState::try_from_with_header(
180            snapshot,
181            TimestampHeader { timestamp: 1703097600u64 },
182            &HashMap::new(),
183            &tokens,
184            &DecoderContext::new(),
185        )
186        .await
187        .expect("create state from snapshot");
188
189        assert_eq!(result.base_token.symbol, "WETH");
190        assert_eq!(result.quote_token.symbol, "USDC");
191        assert_eq!(result.book.bids.len(), 1);
192        assert_eq!(result.book.asks.len(), 1);
193        assert_eq!(result.book.bids[0].price, 3000.0);
194        assert_eq!(result.book.bids[0].quantity, 1.5);
195    }
196
197    #[tokio::test]
198    async fn test_try_from_missing_book() {
199        let (mut snapshot, tokens) = create_test_snapshot();
200        // Remove the book completely to simulate a missing attribute
201        snapshot.state.attributes.remove("book");
202
203        let result = NativeState::try_from_with_header(
204            snapshot,
205            TimestampHeader::default(),
206            &HashMap::new(),
207            &tokens,
208            &DecoderContext::new(),
209        )
210        .await;
211
212        assert!(matches!(
213            result.unwrap_err(),
214            InvalidSnapshotError::MissingAttribute(attribute) if attribute == "book"
215        ));
216    }
217
218    #[tokio::test]
219    async fn test_try_from_missing_token() {
220        let (mut snapshot, tokens) = create_test_snapshot();
221        // Remove the second token
222        snapshot.component.tokens.pop();
223
224        let result = NativeState::try_from_with_header(
225            snapshot,
226            TimestampHeader::default(),
227            &HashMap::new(),
228            &tokens,
229            &DecoderContext::new(),
230        )
231        .await;
232
233        assert!(matches!(result.unwrap_err(), InvalidSnapshotError::ValueError(_)));
234    }
235}