1use std::{
2 collections::{BTreeSet, HashMap},
3 time::SystemTime,
4};
5
6use alloy::primitives::utils::keccak256;
7use serde::{de::DeserializeOwned, Serialize};
8use tracing::{debug, warn};
9use tycho_client::feed::synchronizer::{ComponentWithState, Snapshot, StateSyncMessage};
10use tycho_common::{
11 models::{
12 protocol::{ProtocolComponent, ProtocolComponentState},
13 token::Token,
14 Chain,
15 },
16 Bytes,
17};
18
19use crate::{
20 protocol::errors::InvalidSnapshotError,
21 rfq::{
22 errors::RFQError,
23 models::{QuoteRule, TimestampHeader},
24 },
25};
26
27pub const BOOKS_ATTRIBUTE: &str = "books";
29pub const SWAP_DIRECTIONS_ATTRIBUTE: &str = "swap_directions";
31
32pub fn component_id(protocol_system: &str, chain: Chain) -> String {
34 keccak256(format!("{protocol_system}_{}", chain.id()).as_bytes()).to_string()
35}
36
37pub fn encode_swap_directions(directions: &BTreeSet<(Bytes, Bytes)>) -> Bytes {
38 let mut encoded = Vec::with_capacity(directions.len() * 40);
39 for (token_in, token_out) in directions {
40 encoded.extend_from_slice(token_in);
41 encoded.extend_from_slice(token_out);
42 }
43 encoded.into()
44}
45
46pub fn decode_swap_directions(attribute: &[u8]) -> Result<Vec<(Bytes, Bytes)>, String> {
47 let (directions, rest) = attribute.as_chunks::<40>();
48 if !rest.is_empty() {
49 return Err(format!(
50 "Swap directions attribute holds {} bytes, not a multiple of 40",
51 attribute.len()
52 ));
53 }
54 Ok(directions
55 .iter()
56 .map(|direction| (Bytes::from(&direction[..20]), Bytes::from(&direction[20..])))
57 .collect())
58}
59
60#[allow(clippy::too_many_arguments)]
62pub fn all_pairs_component<B: Serialize>(
63 protocol_system: &str,
64 protocol_type_name: &str,
65 chain: Chain,
66 swap_directions: &BTreeSet<(Bytes, Bytes)>,
67 books: &[B],
68 tvl: f64,
69 quote_rule: QuoteRule,
70) -> Result<ComponentWithState, RFQError> {
71 let mut tokens = BTreeSet::new();
72 for (token_in, token_out) in swap_directions {
73 tokens.insert(token_in.clone());
74 tokens.insert(token_out.clone());
75 }
76 let books = serde_json::to_vec(books)
77 .map_err(|e| RFQError::ParsingError(format!("Failed to serialize books: {e}")))?;
78 let id = component_id(protocol_system, chain);
79 let component = ProtocolComponent {
80 id: id.clone(),
81 protocol_system: protocol_system.to_string(),
82 protocol_type_name: protocol_type_name.to_string(),
83 chain,
84 tokens: tokens.into_iter().collect(),
85 contract_addresses: vec![],
86 static_attributes: HashMap::from([
87 (SWAP_DIRECTIONS_ATTRIBUTE.to_string(), encode_swap_directions(swap_directions)),
88 (QuoteRule::ATTRIBUTE.to_string(), quote_rule.as_str().as_bytes().into()),
89 ]),
90 ..Default::default()
91 };
92 let attributes = HashMap::from([(BOOKS_ATTRIBUTE.to_string(), books.into())]);
93 Ok(ComponentWithState {
94 state: ProtocolComponentState::new(&id, attributes, HashMap::new()),
95 component,
96 component_tvl: Some(tvl),
97 entrypoints: vec![],
98 })
99}
100
101pub struct DecodedAllPairs<B> {
103 pub books: Vec<B>,
104 pub tokens: HashMap<Bytes, Token>,
105 pub quote_rule: Option<QuoteRule>,
107}
108
109pub fn decode_all_pairs_component<B: DeserializeOwned>(
116 snapshot: &ComponentWithState,
117 all_tokens: &HashMap<Bytes, Token>,
118) -> Result<DecodedAllPairs<B>, InvalidSnapshotError> {
119 let component = &snapshot.component;
120 if !component
121 .static_attributes
122 .contains_key(SWAP_DIRECTIONS_ATTRIBUTE)
123 {
124 return Err(InvalidSnapshotError::MissingAttribute(format!(
125 "Component {} of {} has no {SWAP_DIRECTIONS_ATTRIBUTE} attribute, so its client \
126 streams per pair. Build the client with ComponentLayout::AllPairs, or register the \
127 per-pair state for it.",
128 component.id, component.protocol_system
129 )));
130 }
131 let mut tokens = HashMap::new();
132 for address in &component.tokens {
133 let Some(token) = all_tokens.get(address) else {
134 debug!(
135 "Component {} of {} names token {address}, which the decoder does not know. \
136 Dropping the token and the books that name it.",
137 component.id, component.protocol_system
138 );
139 continue;
140 };
141 tokens.insert(address.clone(), token.clone());
142 }
143 let books = match snapshot
144 .state
145 .attributes
146 .get(BOOKS_ATTRIBUTE)
147 {
148 Some(books) => decode_books(books, component)?,
149 None => Vec::new(),
150 };
151 let quote_rule = QuoteRule::from_attributes(&component.static_attributes)
152 .map_err(InvalidSnapshotError::ValueError)?;
153 Ok(DecodedAllPairs { books, tokens, quote_rule })
154}
155
156fn decode_books<B: DeserializeOwned>(
157 books: &[u8],
158 component: &ProtocolComponent,
159) -> Result<Vec<B>, InvalidSnapshotError> {
160 let books: Vec<serde_json::Value> = serde_json::from_slice(books)
161 .map_err(|e| InvalidSnapshotError::ValueError(format!("Invalid books JSON: {e}")))?;
162 let mut decoded = Vec::with_capacity(books.len());
163 for (index, book) in books.into_iter().enumerate() {
164 match serde_json::from_value(book) {
165 Ok(book) => decoded.push(book),
166 Err(e) => warn!(
167 "Component {} of {} has an invalid book at index {index}: {e}. Skipping it.",
168 component.id, component.protocol_system
169 ),
170 }
171 }
172 Ok(decoded)
173}
174
175pub fn unix_timestamp() -> Result<u64, RFQError> {
177 SystemTime::now()
178 .duration_since(SystemTime::UNIX_EPOCH)
179 .map(|elapsed| elapsed.as_secs())
180 .map_err(|_| RFQError::ParsingError("SystemTime before UNIX EPOCH!".into()))
181}
182
183pub fn poll_message(
186 current: &mut HashMap<String, ComponentWithState>,
187 components: HashMap<String, ComponentWithState>,
188 timestamp: u64,
189) -> StateSyncMessage<TimestampHeader> {
190 let mut removed_components = HashMap::new();
191 for (id, component) in current.iter() {
192 if !components.contains_key(id) {
193 removed_components.insert(id.clone(), component.component.clone());
194 }
195 }
196 *current = components.clone();
197 sync_message(components, removed_components, timestamp)
198}
199
200fn sync_message(
201 states: HashMap<String, ComponentWithState>,
202 removed_components: HashMap<String, ProtocolComponent>,
203 timestamp: u64,
204) -> StateSyncMessage<TimestampHeader> {
205 StateSyncMessage {
206 header: TimestampHeader { timestamp },
207 snapshots: Snapshot { states, vm_storage: HashMap::new() },
208 deltas: None,
209 removed_components,
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216 use crate::rfq::protocols::test_utils::{all_pairs_snapshot, usdc, weth};
217
218 #[test]
219 fn swap_directions_round_trip() {
220 let directions =
221 BTreeSet::from([(weth().address, usdc().address), (usdc().address, weth().address)]);
222 let encoded = encode_swap_directions(&directions);
223 assert_eq!(encoded.len(), 80);
224 let decoded: BTreeSet<_> = decode_swap_directions(&encoded)
225 .unwrap()
226 .into_iter()
227 .collect();
228 assert_eq!(decoded, directions);
229 }
230
231 #[test]
232 fn swap_directions_length_not_a_multiple_of_40() {
233 let mut attribute =
234 encode_swap_directions(&BTreeSet::from([(weth().address, usdc().address)])).to_vec();
235 attribute.push(0);
236 let result = decode_swap_directions(&attribute);
237 assert!(matches!(result, Err(message) if message.contains("not a multiple of 40")));
238 }
239
240 #[test]
241 fn component_tokens_are_the_directions_tokens() {
242 let directions = BTreeSet::from([(weth().address, usdc().address)]);
243 let component = all_pairs_component(
244 "rfq:test",
245 "test_pool",
246 Chain::Ethereum,
247 &directions,
248 &["book"],
249 100.0,
250 QuoteRule::OncePerVenue,
251 )
252 .unwrap();
253
254 let mut expected_tokens = vec![weth().address, usdc().address];
255 expected_tokens.sort();
256 assert_eq!(component.component.tokens, expected_tokens);
257 assert_eq!(component.component.id, component_id("rfq:test", Chain::Ethereum));
258 assert_eq!(component.state.component_id, component.component.id);
259 assert_eq!(component.component_tvl, Some(100.0));
260 assert_eq!(
261 component.component.static_attributes[QuoteRule::ATTRIBUTE].as_ref(),
262 b"once_per_venue"
263 );
264 assert_eq!(
265 component.component.static_attributes[SWAP_DIRECTIONS_ATTRIBUTE],
266 encode_swap_directions(&directions)
267 );
268 let books: Vec<String> =
269 serde_json::from_slice(&component.state.attributes[BOOKS_ATTRIBUTE]).unwrap();
270 assert_eq!(books, ["book"]);
271 }
272
273 #[test]
274 fn decode_all_pairs_component_skips_invalid_book() {
275 let (snapshot, tokens) =
276 all_pairs_snapshot("rfq:test", &[weth(), usdc()], &serde_json::json!(["a", 7, "b"]));
277 let decoded = decode_all_pairs_component::<String>(&snapshot, &tokens).unwrap();
278 assert_eq!(decoded.books, ["a", "b"]);
279 }
280
281 #[test]
282 fn decode_all_pairs_component_invalid_books_json() {
283 let (mut snapshot, tokens) = all_pairs_snapshot("rfq:test", &[weth(), usdc()], &["book"]);
284 snapshot
285 .state
286 .attributes
287 .insert(BOOKS_ATTRIBUTE.to_string(), b"invalid json".into());
288 let result = decode_all_pairs_component::<String>(&snapshot, &tokens);
289 assert!(
290 matches!(result, Err(InvalidSnapshotError::ValueError(msg)) if msg.contains("Invalid books JSON"))
291 );
292 }
293
294 #[test]
295 fn decode_all_pairs_component_rejects_per_pair_component() {
296 let (mut snapshot, tokens) = all_pairs_snapshot("rfq:test", &[weth(), usdc()], &["book"]);
297 snapshot
298 .component
299 .static_attributes
300 .remove(SWAP_DIRECTIONS_ATTRIBUTE);
301 let result = decode_all_pairs_component::<String>(&snapshot, &tokens);
302 assert!(
303 matches!(result, Err(InvalidSnapshotError::MissingAttribute(msg)) if msg.contains("AllPairs"))
304 );
305 }
306
307 #[test]
308 fn poll_message_removes_dropped_components() {
309 let pair = |id: &str| {
310 let mut component = all_pairs_component(
311 "rfq:test",
312 "test_pool",
313 Chain::Ethereum,
314 &BTreeSet::from([(weth().address, usdc().address)]),
315 &["book"],
316 100.0,
317 QuoteRule::OncePerVenue,
318 )
319 .unwrap();
320 component.component.id = id.to_string();
321 (id.to_string(), component)
322 };
323 let mut current = HashMap::new();
324
325 let first = poll_message(&mut current, HashMap::from([pair("a"), pair("b")]), 1);
326 assert_eq!(first.snapshots.states.len(), 2);
327 assert!(first.removed_components.is_empty());
328
329 let second = poll_message(&mut current, HashMap::from([pair("b")]), 2);
330 assert_eq!(
331 second
332 .snapshots
333 .states
334 .keys()
335 .collect::<Vec<_>>(),
336 ["b"]
337 );
338 assert_eq!(
339 second
340 .removed_components
341 .keys()
342 .collect::<Vec<_>>(),
343 ["a"]
344 );
345 }
346}