Skip to main content

tycho_simulation/rfq/protocols/hashflow/
all_pairs_state.rs

1use std::{any::Any, collections::HashMap, fmt, sync::Arc};
2
3use async_trait::async_trait;
4use num_bigint::BigUint;
5use serde::{Deserialize, Serialize};
6use tycho_common::{
7    dto::ProtocolStateDelta,
8    models::{protocol::GetAmountOutParams, token::Token},
9    simulation::{
10        errors::{SimulationError, TransitionError},
11        indicatively_priced::{IndicativelyPriced, SignedQuote},
12        protocol_sim::{Balances, GetAmountOutResult, ProtocolSim},
13    },
14    Bytes,
15};
16
17use crate::rfq::protocols::{
18    hashflow::client::HashflowClient,
19    maker_price_levels::{AllMakerPriceLevels, MakerPriceLevels, MakerPricing},
20};
21
22/// Hashflow's liquidity on one chain: every market maker's levels on every pair it quotes.
23///
24/// A swap takes its quote from one market maker and marks that maker used in the state it
25/// returns. A maker's second quote does not account for its first fill, so by default a later
26/// swap on that state goes to another maker.
27#[derive(Clone, Serialize, Deserialize)]
28pub struct HashflowAllPairsState {
29    pub price_levels: AllMakerPriceLevels,
30    pub client: Arc<HashflowClient>,
31}
32
33impl fmt::Debug for HashflowAllPairsState {
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        f.debug_struct("HashflowAllPairsState")
36            .field("price_levels", &self.price_levels)
37            .finish_non_exhaustive()
38    }
39}
40
41impl HashflowAllPairsState {
42    pub fn new(
43        price_levels: Vec<MakerPriceLevels>,
44        tokens: HashMap<Bytes, Token>,
45        client: HashflowClient,
46    ) -> Result<Self, SimulationError> {
47        let rule = client.quote_rule();
48        let price_levels =
49            AllMakerPriceLevels::new(price_levels, tokens, MakerPricing::Hashflow, rule)?;
50        Ok(Self { price_levels, client: Arc::new(client) })
51    }
52}
53
54#[typetag::serde]
55impl ProtocolSim for HashflowAllPairsState {
56    fn fee(&self) -> f64 {
57        0.0
58    }
59
60    fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
61        self.price_levels
62            .spot_price(&base.address, &quote.address)
63    }
64
65    fn get_amount_out(
66        &self,
67        amount_in: BigUint,
68        token_in: &Token,
69        token_out: &Token,
70    ) -> Result<GetAmountOutResult, SimulationError> {
71        let fill =
72            self.price_levels
73                .best_fill(&amount_in, &token_in.address, &token_out.address)?;
74        let new_state = Self {
75            price_levels: self
76                .price_levels
77                .with_used(&fill.maker_levels.market_maker),
78            client: self.client.clone(),
79        };
80        fill.result(151_000, Box::new(new_state))
81    }
82
83    fn get_limits(
84        &self,
85        sell_token: Bytes,
86        buy_token: Bytes,
87    ) -> Result<(BigUint, BigUint), SimulationError> {
88        self.price_levels
89            .get_limits(&sell_token, &buy_token)
90    }
91
92    fn as_indicatively_priced(&self) -> Result<&dyn IndicativelyPriced, SimulationError> {
93        Ok(self)
94    }
95
96    fn delta_transition(
97        &mut self,
98        _delta: ProtocolStateDelta,
99        _tokens: &HashMap<Bytes, Token>,
100        _balances: &Balances,
101    ) -> Result<(), TransitionError> {
102        Err(TransitionError::DecodeError("Not implemented".into()))
103    }
104
105    fn clone_box(&self) -> Box<dyn ProtocolSim> {
106        Box::new(self.clone())
107    }
108
109    fn as_any(&self) -> &dyn Any {
110        self
111    }
112
113    fn as_any_mut(&mut self) -> &mut dyn Any {
114        self
115    }
116
117    fn eq(&self, other: &dyn ProtocolSim) -> bool {
118        let Some(other) = other
119            .as_any()
120            .downcast_ref::<HashflowAllPairsState>()
121        else {
122            return false;
123        };
124        self.price_levels == other.price_levels
125    }
126}
127
128#[async_trait]
129impl IndicativelyPriced for HashflowAllPairsState {
130    /// Asks the market maker `get_amount_out` picks on this state for the same amount, and no
131    /// other: a fallback maker could be one the route already fills against.
132    ///
133    /// Call it on the state the swap was simulated on, with the simulated amount. On the state
134    /// that swap returned, its maker is used, so the request names another maker or fails.
135    async fn request_signed_quote(
136        &self,
137        params: GetAmountOutParams,
138    ) -> Result<SignedQuote, SimulationError> {
139        let fill =
140            self.price_levels
141                .best_fill(&params.amount_in, &params.token_in, &params.token_out)?;
142        Ok(self
143            .client
144            .request_quote(&params, Some(&fill.maker_levels.market_maker))
145            .await?)
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use std::collections::HashSet;
152
153    use tokio::time::Duration;
154    use tycho_common::models::Chain;
155
156    use super::*;
157    use crate::rfq::{
158        models::{ComponentLayout, QuoteRule},
159        protocols::test_utils::{
160            maker_price_levels, mock_quote_server, quote_params, usdc, wbtc, weth, weth_amount,
161            HASHFLOW_QUOTE_RESPONSE,
162        },
163    };
164
165    fn client(quote_rule: QuoteRule, quote_endpoint: String) -> HashflowClient {
166        HashflowClient::new(
167            Chain::Ethereum,
168            HashSet::new(),
169            0.0,
170            HashSet::new(),
171            "".to_string(),
172            "".to_string(),
173            Duration::from_secs(0),
174            Duration::from_secs(1),
175        )
176        .unwrap()
177        .with_component_layout(ComponentLayout::AllPairs)
178        .with_quote_rule(quote_rule)
179        .with_quote_endpoint(quote_endpoint)
180    }
181
182    /// `test_mm_2` pays more for 0.5 WETH; `test_mm` also quotes WBTC.
183    fn test_state(quote_rule: QuoteRule, quote_endpoint: String) -> HashflowAllPairsState {
184        HashflowAllPairsState::new(
185            vec![
186                maker_price_levels("test_mm", &weth(), &usdc(), &[(0.5, 3000.0), (1.5, 3000.0)]),
187                maker_price_levels("test_mm_2", &weth(), &usdc(), &[(0.5, 3010.0)]),
188                maker_price_levels("test_mm", &wbtc(), &usdc(), &[(1.0, 65000.0)]),
189            ],
190            HashMap::from([
191                (weth().address, weth()),
192                (usdc().address, usdc()),
193                (wbtc().address, wbtc()),
194            ]),
195            client(quote_rule, quote_endpoint),
196        )
197        .unwrap()
198    }
199
200    #[test]
201    fn get_amount_out_marks_the_maker_used() {
202        let state = test_state(QuoteRule::OncePerMaker, String::new());
203        let result = state
204            .get_amount_out(weth_amount(0.5), &weth(), &usdc())
205            .unwrap();
206        assert_eq!(result.amount, BigUint::from(1_505_000_000u64));
207        assert_eq!(result.gas, BigUint::from(151_000u64));
208        let new_state = result
209            .new_state
210            .as_any()
211            .downcast_ref::<HashflowAllPairsState>()
212            .unwrap();
213        assert_eq!(
214            new_state.price_levels,
215            state
216                .price_levels
217                .with_used("test_mm_2")
218        );
219    }
220
221    #[tokio::test]
222    async fn request_signed_quote_names_the_picked_maker() {
223        let (addr, request_log) = mock_quote_server(0, HASHFLOW_QUOTE_RESPONSE).await;
224        // The mock quote is for 1 WETH -> WBTC, as `quote_params` asks.
225        let state = HashflowAllPairsState::new(
226            vec![
227                maker_price_levels("test_mm", &weth(), &wbtc(), &[(1.0, 0.05)]),
228                maker_price_levels("test_mm_2", &weth(), &wbtc(), &[(1.0, 0.051)]),
229            ],
230            HashMap::from([(weth().address, weth()), (wbtc().address, wbtc())]),
231            client(QuoteRule::OncePerMaker, format!("http://127.0.0.1:{}/rfq", addr.port())),
232        )
233        .unwrap();
234        state
235            .request_signed_quote(quote_params())
236            .await
237            .unwrap();
238        let after_first = HashflowAllPairsState {
239            price_levels: state
240                .price_levels
241                .with_used("test_mm_2"),
242            client: state.client.clone(),
243        };
244        after_first
245            .request_signed_quote(quote_params())
246            .await
247            .unwrap();
248
249        let requests = request_log.lock().unwrap();
250        assert!(requests[0].contains("\"marketMakers\":[\"test_mm_2\"]"), "{}", requests[0]);
251        assert!(requests[1].contains("\"marketMakers\":[\"test_mm\"]"), "{}", requests[1]);
252    }
253}