1use std::{any::Any, collections::HashMap, fmt};
2
3use async_trait::async_trait;
4use num_bigint::BigUint;
5use num_traits::{FromPrimitive, Pow, ToPrimitive};
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::{
18 client::RFQClient,
19 protocols::hashflow::{client::HashflowClient, models::HashflowMarketMakerLevels},
20};
21
22#[derive(Clone)]
23pub struct HashflowState {
24 pub base_token: Token,
25 pub quote_token: Token,
26 pub levels: HashflowMarketMakerLevels,
27 pub market_maker: String,
28 pub client: HashflowClient,
29}
30
31impl fmt::Debug for HashflowState {
32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 f.debug_struct("HashflowState")
34 .field("base_token", &self.base_token)
35 .field("quote_token", &self.quote_token)
36 .field("market_maker", &self.market_maker)
37 .finish_non_exhaustive()
38 }
39}
40
41impl HashflowState {
42 pub fn new(
43 base_token: Token,
44 quote_token: Token,
45 levels: HashflowMarketMakerLevels,
46 market_maker: String,
47 client: HashflowClient,
48 ) -> Self {
49 Self { base_token, quote_token, levels, market_maker, client }
50 }
51
52 fn valid_direction_guard(
53 &self,
54 token_address_in: &Bytes,
55 token_address_out: &Bytes,
56 ) -> Result<(), SimulationError> {
57 if !(token_address_in == &self.base_token.address &&
59 token_address_out == &self.quote_token.address)
60 {
61 Err(SimulationError::InvalidInput(
62 format!("Invalid token addresses. Got in={token_address_in}, out={token_address_out}, expected in={}, out={}", self.base_token.address, self.quote_token.address),
63 None,
64 ))
65 } else {
66 Ok(())
67 }
68 }
69
70 fn valid_levels_guard(&self) -> Result<(), SimulationError> {
71 if self.levels.levels.is_empty() {
72 return Err(SimulationError::RecoverableError("No liquidity".into()));
73 }
74 Ok(())
75 }
76}
77
78impl ProtocolSim for HashflowState {
79 fn fee(&self) -> f64 {
80 todo!()
81 }
82
83 fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
84 self.valid_direction_guard(&base.address, "e.address)?;
85
86 self.levels
88 .levels
89 .first()
90 .ok_or(SimulationError::RecoverableError("No liquidity".into()))
91 .map(|level| level.price)
92 }
93
94 fn get_amount_out(
95 &self,
96 amount_in: BigUint,
97 token_in: &Token,
98 token_out: &Token,
99 ) -> Result<GetAmountOutResult, SimulationError> {
100 self.valid_direction_guard(&token_in.address, &token_out.address)?;
101 self.valid_levels_guard()?;
102
103 let amount_in = amount_in.to_f64().ok_or_else(|| {
104 SimulationError::RecoverableError("Can't convert amount in to f64".into())
105 })? / 10f64.powi(token_in.decimals as i32);
106
107 let min_amount = self.levels.levels[0].quantity;
109 if amount_in < min_amount {
110 return Err(SimulationError::RecoverableError(format!(
111 "Amount below minimum. Input amount: {amount_in}, min amount: {min_amount}"
112 )));
113 }
114
115 let (amount_out, remaining_amount_in) = self
117 .levels
118 .get_amount_out_from_levels(amount_in);
119
120 let res = GetAmountOutResult {
121 amount: BigUint::from_f64(amount_out * 10f64.powi(token_out.decimals as i32))
122 .ok_or_else(|| {
123 SimulationError::RecoverableError("Can't convert amount out to BigUInt".into())
124 })?,
125 gas: BigUint::from(134_000u64), new_state: self.clone_box(), };
128
129 if remaining_amount_in > 0.0 {
130 return Err(SimulationError::InvalidInput(
131 format!("Pool has not enough liquidity to support complete swap. Input amount: {amount_in}, consumed amount: {}", amount_in-remaining_amount_in),
132 Some(res)));
133 }
134
135 Ok(res)
136 }
137
138 fn get_limits(
139 &self,
140 sell_token: Bytes,
141 buy_token: Bytes,
142 ) -> Result<(BigUint, BigUint), SimulationError> {
143 self.valid_direction_guard(&sell_token, &buy_token)?;
144 self.valid_levels_guard()?;
145
146 let sell_decimals = self.base_token.decimals;
147 let buy_decimals = self.quote_token.decimals;
148 let (total_sell_amount, total_buy_amount) =
149 self.levels
150 .levels
151 .iter()
152 .fold((0.0, 0.0), |(sell_sum, buy_sum), level| {
153 (sell_sum + level.quantity, buy_sum + level.quantity * level.price)
154 });
155
156 let sell_limit =
157 BigUint::from((total_sell_amount * 10_f64.pow(sell_decimals as f64)) as u128);
158 let buy_limit = BigUint::from((total_buy_amount * 10_f64.pow(buy_decimals as f64)) as u128);
159
160 Ok((sell_limit, buy_limit))
161 }
162
163 fn as_indicatively_priced(&self) -> Result<&dyn IndicativelyPriced, SimulationError> {
164 Ok(self)
165 }
166
167 fn delta_transition(
168 &mut self,
169 _delta: ProtocolStateDelta,
170 _tokens: &HashMap<Bytes, Token>,
171 _balances: &Balances,
172 ) -> Result<(), TransitionError<String>> {
173 todo!()
174 }
175
176 fn clone_box(&self) -> Box<dyn ProtocolSim> {
177 Box::new(self.clone())
178 }
179
180 fn as_any(&self) -> &dyn Any {
181 self
182 }
183
184 fn as_any_mut(&mut self) -> &mut dyn Any {
185 self
186 }
187
188 fn eq(&self, other: &dyn ProtocolSim) -> bool {
189 if let Some(other_state) = other
190 .as_any()
191 .downcast_ref::<HashflowState>()
192 {
193 self.base_token == other_state.base_token &&
194 self.quote_token == other_state.quote_token &&
195 self.levels == other_state.levels
196 } else {
197 false
198 }
199 }
200}
201
202#[async_trait]
203impl IndicativelyPriced for HashflowState {
204 async fn request_signed_quote(
205 &self,
206 params: GetAmountOutParams,
207 ) -> Result<SignedQuote, SimulationError> {
208 Ok(self
209 .client
210 .request_binding_quote(¶ms)
211 .await?)
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use std::{collections::HashSet, str::FromStr};
218
219 use tokio::time::Duration;
220 use tycho_common::models::Chain;
221
222 use super::*;
223 use crate::rfq::protocols::hashflow::models::{HashflowPair, HashflowPriceLevel};
224
225 fn wbtc() -> Token {
226 Token::new(
227 &hex::decode("2260fac5e5542a773aa44fbcfedf7c193bc2c599")
228 .unwrap()
229 .into(),
230 "WBTC",
231 8,
232 0,
233 &[Some(10_000)],
234 Chain::Ethereum,
235 100,
236 )
237 }
238
239 fn usdc() -> Token {
240 Token::new(
241 &hex::decode("a0b86991c6218a76c1d19d4a2e9eb0ce3606eb48")
242 .unwrap()
243 .into(),
244 "USDC",
245 6,
246 0,
247 &[Some(10_000)],
248 Chain::Ethereum,
249 100,
250 )
251 }
252
253 fn weth() -> Token {
254 Token::new(
255 &Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(),
256 "WETH",
257 18,
258 0,
259 &[],
260 Default::default(),
261 100,
262 )
263 }
264
265 fn empty_hashflow_client() -> HashflowClient {
266 HashflowClient::new(
267 Chain::Ethereum,
268 HashSet::new(),
269 0.0,
270 HashSet::new(),
271 "".to_string(),
272 "".to_string(),
273 Duration::from_secs(0),
274 Duration::from_secs(30),
275 )
276 .unwrap()
277 }
278
279 fn create_test_hashflow_state() -> HashflowState {
280 HashflowState {
281 base_token: weth(),
282 quote_token: usdc(),
283 levels: HashflowMarketMakerLevels {
284 pair: HashflowPair {
285 base_token: Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2")
286 .unwrap(),
287 quote_token: Bytes::from_str("0xa0b86991c6218a76c1d19d4a2e9eb0ce3606eb48")
288 .unwrap(),
289 },
290 levels: vec![
291 HashflowPriceLevel { quantity: 0.5, price: 3000.0 },
292 HashflowPriceLevel { quantity: 1.5, price: 3000.0 },
293 HashflowPriceLevel { quantity: 5.0, price: 2999.0 },
294 ],
295 },
296 market_maker: "test_mm".to_string(),
297 client: empty_hashflow_client(),
298 }
299 }
300
301 mod spot_price {
302 use super::*;
303
304 #[test]
305 fn returns_best_price() {
306 let state = create_test_hashflow_state();
307 let price = state
308 .spot_price(&state.base_token, &state.quote_token)
309 .unwrap();
310 assert_eq!(price, 3000.0);
312 }
313
314 #[test]
315 fn returns_invalid_input_error() {
316 let state = create_test_hashflow_state();
317 let result = state.spot_price(&wbtc(), &usdc());
318 assert!(result.is_err());
319 if let Err(SimulationError::InvalidInput(msg, _)) = result {
320 assert!(msg.contains("Invalid token addresses"));
321 } else {
322 panic!("Expected InvalidInput");
323 }
324 }
325
326 #[test]
327 fn returns_no_liquidity_error() {
328 let mut state = create_test_hashflow_state();
329 state.levels.levels.clear();
330 let result = state.spot_price(&state.base_token, &state.quote_token);
331 assert!(result.is_err());
332 if let Err(SimulationError::RecoverableError(msg)) = result {
333 assert_eq!(msg, "No liquidity");
334 } else {
335 panic!("Expected RecoverableError");
336 }
337 }
338 }
339
340 mod get_amount_out {
341 use super::*;
342
343 #[test]
344 fn wbtc_to_usdc() {
345 let state = create_test_hashflow_state();
346
347 let amount_out_result = state
351 .get_amount_out(
352 BigUint::from_str("1500000000000000000").unwrap(), &weth(),
354 &usdc(),
355 )
356 .unwrap();
357
358 assert_eq!(amount_out_result.amount, BigUint::from_str("4500000000").unwrap()); assert_eq!(amount_out_result.gas, BigUint::from(134_000u64));
361 }
362
363 #[test]
364 fn usdc_to_wbtc() {
365 let state = create_test_hashflow_state();
366
367 let result = state.get_amount_out(
372 BigUint::from_str("10000000000").unwrap(), &usdc(),
374 &weth(),
375 );
376
377 assert!(result.is_err());
378 if let Err(SimulationError::InvalidInput(msg, ..)) = result {
379 assert!(msg.contains("Invalid token addresses"));
380 } else {
381 panic!("Expected InvalidInput");
382 }
383 }
384
385 #[test]
386 fn below_minimum() {
387 let state = create_test_hashflow_state();
388
389 let result = state.get_amount_out(
391 BigUint::from_str("250000000000000000").unwrap(), &weth(),
393 &usdc(),
394 );
395
396 assert!(result.is_err());
397 if let Err(SimulationError::RecoverableError(msg)) = result {
398 assert!(msg.contains("Amount below minimum"));
399 } else {
400 panic!("Expected RecoverableError");
401 }
402 }
403
404 #[test]
405 fn insufficient_liquidity() {
406 let state = create_test_hashflow_state();
407
408 let result = state.get_amount_out(
410 BigUint::from_str("8000000000000000000").unwrap(), &weth(),
412 &usdc(),
413 );
414
415 assert!(result.is_err());
416 if let Err(SimulationError::InvalidInput(msg, _)) = result {
417 assert!(msg.contains("Pool has not enough liquidity"));
418 } else {
419 panic!("Expected InvalidInput");
420 }
421 }
422
423 #[test]
424 fn invalid_token_pair() {
425 let state = create_test_hashflow_state();
426
427 let result = state.get_amount_out(
429 BigUint::from_str("100000000").unwrap(), &wbtc(),
431 &usdc(),
432 );
433
434 assert!(result.is_err());
435 if let Err(SimulationError::InvalidInput(msg, ..)) = result {
436 assert!(msg.contains("Invalid token addresses"));
437 } else {
438 panic!("Expected InvalidInput");
439 }
440 }
441
442 #[test]
443 fn no_liquidity() {
444 let mut state = create_test_hashflow_state();
445 state.levels.levels = vec![]; let result = state.get_amount_out(
448 BigUint::from_str("1000000000000000000").unwrap(), &weth(),
450 &usdc(),
451 );
452
453 assert!(result.is_err());
454 if let Err(SimulationError::RecoverableError(msg)) = result {
455 assert_eq!(msg, "No liquidity");
456 } else {
457 panic!("Expected RecoverableError");
458 }
459 }
460 }
461
462 mod get_limits {
463 use super::*;
464
465 #[test]
466 fn valid_limits() {
467 let state = create_test_hashflow_state();
468 let (sell_limit, buy_limit) = state
469 .get_limits(state.base_token.address.clone(), state.quote_token.address.clone())
470 .unwrap();
471
472 assert_eq!(sell_limit, BigUint::from((7.0 * 10f64.powi(18)) as u128));
475 assert_eq!(buy_limit, BigUint::from((20995.0 * 10f64.powi(6)) as u128));
476 }
477
478 #[test]
479 fn invalid_token_pair() {
480 let state = create_test_hashflow_state();
481 let result =
482 state.get_limits(wbtc().address.clone(), state.quote_token.address.clone());
483 assert!(result.is_err());
484 if let Err(SimulationError::InvalidInput(msg, _)) = result {
485 assert!(msg.contains("Invalid token addresses"));
486 } else {
487 panic!("Expected InvalidInput");
488 }
489 }
490
491 #[test]
492 fn no_liquidity() {
493 let mut state = create_test_hashflow_state();
494 state.levels.levels = vec![];
495 let result = state
496 .get_limits(state.base_token.address.clone(), state.quote_token.address.clone());
497 assert!(result.is_err());
498 if let Err(SimulationError::RecoverableError(msg)) = result {
499 assert_eq!(msg, "No liquidity");
500 } else {
501 panic!("Expected RecoverableError");
502 }
503 }
504 }
505}