Skip to main content

tycho_simulation/rfq/protocols/
component.rs

1use 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
26/// State attribute: the venue's books as JSON.
27pub const BOOKS_ATTRIBUTE: &str = "books";
28/// Static attribute: the directions the venue quotes, 40 bytes each (token in, then token out).
29pub const SWAP_DIRECTIONS_ATTRIBUTE: &str = "swap_directions";
30
31/// The id of a venue's component on `chain`. One per chain, the same across polls.
32pub 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/// The all-pairs component for one poll. Its `tokens` are every token a direction names.
60#[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
100/// What an all-pairs component carries.
101pub struct DecodedAllPairs<B> {
102    pub books: Vec<B>,
103    pub tokens: HashMap<Bytes, Token>,
104    /// `None` when the component carries no `quote_rule` attribute; the builder default applies.
105    pub quote_rule: Option<QuoteRule>,
106}
107
108/// A missing `books` attribute is a venue with no books. Every token the component names must
109/// be in `all_tokens`. Fails on a per-pair component, which has no `swap_directions` attribute.
110pub 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
147/// Seconds since the UNIX epoch.
148pub 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
155/// The stream message for one poll: every component in `components`, and the removal of every
156/// component the stream emitted before that `components` lacks.
157pub 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}