use std::{any::Any, collections::HashMap, fmt, sync::Arc};
use async_trait::async_trait;
use num_bigint::BigUint;
use serde::{Deserialize, Serialize};
use tycho_common::{
dto::ProtocolStateDelta,
models::{protocol::GetAmountOutParams, token::Token},
simulation::{
errors::{SimulationError, TransitionError},
indicatively_priced::{IndicativelyPriced, SignedQuote},
protocol_sim::{Balances, GetAmountOutResult, ProtocolSim},
},
Bytes,
};
use crate::rfq::protocols::{
hashflow::client::HashflowClient,
maker_price_levels::{AllMakerPriceLevels, MakerPriceLevels, MakerPricing},
};
#[derive(Clone, Serialize, Deserialize)]
pub struct HashflowAllPairsState {
pub price_levels: AllMakerPriceLevels,
pub client: Arc<HashflowClient>,
}
impl fmt::Debug for HashflowAllPairsState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("HashflowAllPairsState")
.field("price_levels", &self.price_levels)
.finish_non_exhaustive()
}
}
impl HashflowAllPairsState {
pub fn new(
price_levels: Vec<MakerPriceLevels>,
tokens: HashMap<Bytes, Token>,
client: HashflowClient,
) -> Result<Self, SimulationError> {
let rule = client.quote_rule();
let price_levels =
AllMakerPriceLevels::new(price_levels, tokens, MakerPricing::Hashflow, rule)?;
Ok(Self { price_levels, client: Arc::new(client) })
}
}
#[typetag::serde]
impl ProtocolSim for HashflowAllPairsState {
fn fee(&self) -> f64 {
0.0
}
fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
self.price_levels
.spot_price(&base.address, "e.address)
}
fn get_amount_out(
&self,
amount_in: BigUint,
token_in: &Token,
token_out: &Token,
) -> Result<GetAmountOutResult, SimulationError> {
let fill =
self.price_levels
.best_fill(&amount_in, &token_in.address, &token_out.address)?;
let new_state = Self {
price_levels: self
.price_levels
.with_used(&fill.maker_levels.market_maker),
client: self.client.clone(),
};
fill.result(151_000, Box::new(new_state))
}
fn get_limits(
&self,
sell_token: Bytes,
buy_token: Bytes,
) -> Result<(BigUint, BigUint), SimulationError> {
self.price_levels
.get_limits(&sell_token, &buy_token)
}
fn as_indicatively_priced(&self) -> Result<&dyn IndicativelyPriced, SimulationError> {
Ok(self)
}
fn delta_transition(
&mut self,
_delta: ProtocolStateDelta,
_tokens: &HashMap<Bytes, Token>,
_balances: &Balances,
) -> Result<(), TransitionError> {
Err(TransitionError::DecodeError("Not implemented".into()))
}
fn clone_box(&self) -> Box<dyn ProtocolSim> {
Box::new(self.clone())
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn eq(&self, other: &dyn ProtocolSim) -> bool {
let Some(other) = other
.as_any()
.downcast_ref::<HashflowAllPairsState>()
else {
return false;
};
self.price_levels == other.price_levels
}
}
#[async_trait]
impl IndicativelyPriced for HashflowAllPairsState {
async fn request_signed_quote(
&self,
params: GetAmountOutParams,
) -> Result<SignedQuote, SimulationError> {
let fill =
self.price_levels
.best_fill(¶ms.amount_in, ¶ms.token_in, ¶ms.token_out)?;
Ok(self
.client
.request_quote(¶ms, Some(&fill.maker_levels.market_maker))
.await?)
}
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use tokio::time::Duration;
use tycho_common::models::Chain;
use super::*;
use crate::rfq::{
models::{ComponentLayout, QuoteRule},
protocols::test_utils::{
maker_price_levels, mock_quote_server, quote_params, usdc, wbtc, weth, weth_amount,
HASHFLOW_QUOTE_RESPONSE,
},
};
fn client(quote_rule: QuoteRule, quote_endpoint: String) -> HashflowClient {
HashflowClient::new(
Chain::Ethereum,
HashSet::new(),
0.0,
HashSet::new(),
"".to_string(),
"".to_string(),
Duration::from_secs(0),
Duration::from_secs(1),
)
.unwrap()
.with_component_layout(ComponentLayout::AllPairs)
.with_quote_rule(quote_rule)
.with_quote_endpoint(quote_endpoint)
}
fn test_state(quote_rule: QuoteRule, quote_endpoint: String) -> HashflowAllPairsState {
HashflowAllPairsState::new(
vec![
maker_price_levels("test_mm", &weth(), &usdc(), &[(0.5, 3000.0), (1.5, 3000.0)]),
maker_price_levels("test_mm_2", &weth(), &usdc(), &[(0.5, 3010.0)]),
maker_price_levels("test_mm", &wbtc(), &usdc(), &[(1.0, 65000.0)]),
],
HashMap::from([
(weth().address, weth()),
(usdc().address, usdc()),
(wbtc().address, wbtc()),
]),
client(quote_rule, quote_endpoint),
)
.unwrap()
}
#[test]
fn get_amount_out_marks_the_maker_used() {
let state = test_state(QuoteRule::OncePerMaker, String::new());
let result = state
.get_amount_out(weth_amount(0.5), &weth(), &usdc())
.unwrap();
assert_eq!(result.amount, BigUint::from(1_505_000_000u64));
assert_eq!(result.gas, BigUint::from(151_000u64));
let new_state = result
.new_state
.as_any()
.downcast_ref::<HashflowAllPairsState>()
.unwrap();
assert_eq!(
new_state.price_levels,
state
.price_levels
.with_used("test_mm_2")
);
}
#[tokio::test]
async fn request_signed_quote_names_the_picked_maker() {
let (addr, request_log) = mock_quote_server(0, HASHFLOW_QUOTE_RESPONSE).await;
let state = HashflowAllPairsState::new(
vec![
maker_price_levels("test_mm", &weth(), &wbtc(), &[(1.0, 0.05)]),
maker_price_levels("test_mm_2", &weth(), &wbtc(), &[(1.0, 0.051)]),
],
HashMap::from([(weth().address, weth()), (wbtc().address, wbtc())]),
client(QuoteRule::OncePerMaker, format!("http://127.0.0.1:{}/rfq", addr.port())),
)
.unwrap();
state
.request_signed_quote(quote_params())
.await
.unwrap();
let after_first = HashflowAllPairsState {
price_levels: state
.price_levels
.with_used("test_mm_2"),
client: state.client.clone(),
};
after_first
.request_signed_quote(quote_params())
.await
.unwrap();
let requests = request_log.lock().unwrap();
assert!(requests[0].contains("\"marketMakers\":[\"test_mm_2\"]"), "{}", requests[0]);
assert!(requests[1].contains("\"marketMakers\":[\"test_mm\"]"), "{}", requests[1]);
}
}