tycho_simulation/rfq/protocols/native/
decoder.rs1use 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 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 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 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}