Skip to main content

tycho_simulation/rfq/protocols/liquorice/
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    liquorice::client::LiquoriceClient,
19    maker_price_levels::{AllMakerPriceLevels, MakerPriceLevels, MakerPricing},
20};
21
22/// Liquorice'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 LiquoriceAllPairsState {
29    pub price_levels: AllMakerPriceLevels,
30    pub client: Arc<LiquoriceClient>,
31}
32
33impl fmt::Debug for LiquoriceAllPairsState {
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        f.debug_struct("LiquoriceAllPairsState")
36            .field("price_levels", &self.price_levels)
37            .finish_non_exhaustive()
38    }
39}
40
41impl LiquoriceAllPairsState {
42    pub fn new(
43        price_levels: Vec<MakerPriceLevels>,
44        tokens: HashMap<Bytes, Token>,
45        client: LiquoriceClient,
46    ) -> Result<Self, SimulationError> {
47        let rule = client.quote_rule();
48        let price_levels =
49            AllMakerPriceLevels::new(price_levels, tokens, MakerPricing::Liquorice, rule)?;
50        Ok(Self { price_levels, client: Arc::new(client) })
51    }
52}
53
54#[typetag::serde]
55impl ProtocolSim for LiquoriceAllPairsState {
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(134_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::<LiquoriceAllPairsState>()
121        else {
122            return false;
123        };
124        self.price_levels == other.price_levels
125    }
126}
127
128#[async_trait]
129impl IndicativelyPriced for LiquoriceAllPairsState {
130    /// Takes the level of the market maker `get_amount_out` picks on this state for the same
131    /// amount, and no other: another maker's level 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            LIQUORICE_QUOTE_RESPONSE,
162        },
163    };
164
165    fn client(quote_rule: QuoteRule, quote_endpoint: String) -> LiquoriceClient {
166        LiquoriceClient::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            300,
176        )
177        .unwrap()
178        .with_component_layout(ComponentLayout::AllPairs)
179        .with_quote_rule(quote_rule)
180        .with_quote_endpoint(quote_endpoint)
181    }
182
183    /// `test_mm` holds 7 WETH, `test_mm_2` 1 WETH at a lower price.
184    fn test_state(quote_rule: QuoteRule) -> LiquoriceAllPairsState {
185        LiquoriceAllPairsState::new(
186            vec![
187                maker_price_levels(
188                    "test_mm",
189                    &weth(),
190                    &usdc(),
191                    &[(0.5, 3000.0), (1.5, 3000.0), (5.0, 2999.0)],
192                ),
193                maker_price_levels("test_mm_2", &weth(), &usdc(), &[(1.0, 2998.0)]),
194            ],
195            HashMap::from([(weth().address, weth()), (usdc().address, usdc())]),
196            client(quote_rule, String::new()),
197        )
198        .unwrap()
199    }
200
201    #[test]
202    fn get_amount_out_marks_the_maker_used() {
203        let state = test_state(QuoteRule::OncePerMaker);
204        let result = state
205            .get_amount_out(weth_amount(1.0), &weth(), &usdc())
206            .unwrap();
207        assert_eq!(result.amount, BigUint::from(3_000_000_000u64));
208        assert_eq!(result.gas, BigUint::from(134_000u64));
209        let new_state = result
210            .new_state
211            .as_any()
212            .downcast_ref::<LiquoriceAllPairsState>()
213            .unwrap();
214        assert_eq!(new_state.price_levels, state.price_levels.with_used("test_mm"));
215    }
216
217    #[tokio::test]
218    async fn request_signed_quote_takes_the_picked_makers_level() {
219        let (addr, _) = mock_quote_server(0, LIQUORICE_QUOTE_RESPONSE).await;
220        // The mock quote holds one level, from `test-maker`, for 1 WETH -> WBTC.
221        let state = LiquoriceAllPairsState::new(
222            vec![
223                maker_price_levels("test-maker", &weth(), &wbtc(), &[(1.0, 0.051)]),
224                maker_price_levels("other", &weth(), &wbtc(), &[(1.0, 0.05)]),
225            ],
226            HashMap::from([(weth().address, weth()), (wbtc().address, wbtc())]),
227            client(QuoteRule::OncePerMaker, format!("http://127.0.0.1:{}/rfq", addr.port())),
228        )
229        .unwrap();
230        let quote = state
231            .request_signed_quote(quote_params())
232            .await
233            .unwrap();
234        assert_eq!(quote.amount_out, BigUint::from(3329502u64));
235
236        let after_first = LiquoriceAllPairsState {
237            price_levels: state
238                .price_levels
239                .with_used("test-maker"),
240            client: state.client.clone(),
241        };
242        let missing = after_first
243            .request_signed_quote(quote_params())
244            .await
245            .unwrap_err();
246        assert!(
247            matches!(missing, SimulationError::FatalError(msg) if msg.contains("quote not found"))
248        );
249    }
250}