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