Skip to main content

tycho_simulation/rfq/protocols/hashflow/
all_pairs_decoder.rs

1use std::collections::HashMap;
2
3use tycho_client::feed::synchronizer::ComponentWithState;
4use tycho_common::{models::token::Token, Bytes};
5
6use super::{all_pairs_state::HashflowAllPairsState, client_builder::HashflowClientBuilder};
7use crate::{
8    protocol::{
9        errors::InvalidSnapshotError,
10        models::{DecoderContext, TryFromWithBlock},
11    },
12    rfq::{
13        constants::get_hashflow_auth,
14        models::{ComponentLayout, TimestampHeader},
15        protocols::{
16            component::{decode_all_pairs_component, DecodedAllPairs},
17            maker_price_levels::MakerPriceLevels,
18        },
19    },
20};
21
22impl TryFromWithBlock<ComponentWithState, TimestampHeader> for HashflowAllPairsState {
23    type Error = InvalidSnapshotError;
24
25    async fn try_from_with_header(
26        snapshot: ComponentWithState,
27        _timestamp_header: TimestampHeader,
28        _account_balances: &HashMap<Bytes, HashMap<Bytes, Bytes>>,
29        all_tokens: &HashMap<Bytes, Token>,
30        _decoder_context: &DecoderContext,
31    ) -> Result<Self, Self::Error> {
32        let DecodedAllPairs { books: price_levels, tokens, quote_rule } =
33            decode_all_pairs_component::<MakerPriceLevels>(&snapshot, all_tokens)?;
34
35        let auth = get_hashflow_auth().map_err(|e| {
36            InvalidSnapshotError::ValueError(format!("Failed to get Hashflow authentication: {e}"))
37        })?;
38        let mut builder = HashflowClientBuilder::new(snapshot.component.chain, auth.user, auth.key)
39            .tokens(tokens.keys().cloned().collect())
40            .component_layout(ComponentLayout::AllPairs);
41        if let Some(quote_rule) = quote_rule {
42            builder = builder.quote_rule(quote_rule);
43        }
44        let client = builder.build().map_err(|e| {
45            InvalidSnapshotError::MissingAttribute(format!("Couldn't create HashflowClient: {e}"))
46        })?;
47
48        HashflowAllPairsState::new(price_levels, tokens, client)
49            .map_err(|e| InvalidSnapshotError::ValueError(e.to_string()))
50    }
51}
52
53#[cfg(test)]
54mod tests {
55    use std::env;
56
57    use num_bigint::BigUint;
58    use tycho_common::simulation::{errors::SimulationError, protocol_sim::ProtocolSim};
59
60    use super::*;
61    use crate::rfq::{
62        models::QuoteRule,
63        protocols::test_utils::{all_pairs_snapshot, decode, usdc, wbtc, weth},
64    };
65
66    #[tokio::test]
67    async fn test_decodes_price_levels() {
68        // Two makers on WBTC/USDC and one of them on WETH/USDC.
69        env::set_var("HASHFLOW_USER", "test_user");
70        env::set_var("HASHFLOW_KEY", "test_key");
71        let price_levels = serde_json::json!([
72            {
73                "mm": "test_market_maker",
74                "base_token": wbtc().address, "quote_token": usdc().address,
75                "levels": [{ "q": "1.5", "p": "65000.0" }, { "q": "2.0", "p": "64950.0" }]
76            },
77            {
78                "mm": "mm_b",
79                "base_token": wbtc().address, "quote_token": usdc().address,
80                "levels": [{ "q": "0.5", "p": "65100.0" }]
81            },
82            {
83                "mm": "test_market_maker",
84                "base_token": weth().address, "quote_token": usdc().address,
85                "levels": [{ "q": "10", "p": "3000.0" }]
86            }
87        ]);
88        let (snapshot, tokens) =
89            all_pairs_snapshot("rfq:hashflow", &[wbtc(), usdc(), weth()], &price_levels);
90        let state = decode::<HashflowAllPairsState>(snapshot, &tokens)
91            .await
92            .unwrap();
93
94        let wbtc_price_levels = state
95            .price_levels
96            .pair_price_levels(&wbtc().address, &usdc().address);
97        assert_eq!(wbtc_price_levels.len(), 2);
98        assert_eq!(wbtc_price_levels[0].market_maker, "mm_b");
99        assert_eq!(wbtc_price_levels[1].levels[0].quantity, 1.5);
100        assert_eq!(wbtc_price_levels[1].levels[0].price, 65000.0);
101        assert_eq!(
102            state
103                .price_levels
104                .pair_price_levels(&weth().address, &usdc().address)
105                .len(),
106            1
107        );
108    }
109
110    /// `mm_b` fills the WBTC swap, and the WETH pair only `test_market_maker` quotes is then
111    /// refused — which only `once_per_venue` does.
112    #[tokio::test]
113    async fn test_decodes_once_per_venue_attribute() {
114        env::set_var("HASHFLOW_USER", "test_user");
115        env::set_var("HASHFLOW_KEY", "test_key");
116        let price_levels = serde_json::json!([
117            {
118                "mm": "mm_b",
119                "base_token": wbtc().address, "quote_token": usdc().address,
120                "levels": [{ "q": "0.5", "p": "65100.0" }]
121            },
122            {
123                "mm": "test_market_maker",
124                "base_token": weth().address, "quote_token": usdc().address,
125                "levels": [{ "q": "10", "p": "3000.0" }]
126            }
127        ]);
128        let (mut snapshot, tokens) =
129            all_pairs_snapshot("rfq:hashflow", &[wbtc(), usdc(), weth()], &price_levels);
130        snapshot
131            .component
132            .static_attributes
133            .insert(
134                QuoteRule::ATTRIBUTE.to_string(),
135                QuoteRule::OncePerVenue
136                    .as_str()
137                    .as_bytes()
138                    .into(),
139            );
140
141        let state = decode::<HashflowAllPairsState>(snapshot, &tokens)
142            .await
143            .unwrap();
144        let after_swap = state
145            .get_amount_out(BigUint::from(50_000_000u64), &wbtc(), &usdc())
146            .unwrap()
147            .new_state;
148
149        let result = after_swap.get_amount_out(
150            BigUint::from(1_000_000_000_000_000_000u64),
151            &weth(),
152            &usdc(),
153        );
154        assert!(
155            matches!(result, Err(SimulationError::RecoverableError(message)) if message.contains("already quoted in this route"))
156        );
157    }
158}