tycho_simulation/rfq/protocols/liquorice/
all_pairs_state.rs1use 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#[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, "e.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 async fn request_signed_quote(
136 &self,
137 params: GetAmountOutParams,
138 ) -> Result<SignedQuote, SimulationError> {
139 let fill =
140 self.price_levels
141 .best_fill(¶ms.amount_in, ¶ms.token_in, ¶ms.token_out)?;
142 Ok(self
143 .client
144 .request_quote(¶ms, 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 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 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}