use std::collections::HashMap;
use tycho_client::feed::synchronizer::ComponentWithState;
use tycho_common::{models::token::Token, Bytes};
use super::{all_pairs_state::LiquoriceAllPairsState, client_builder::LiquoriceClientBuilder};
use crate::{
protocol::{
errors::InvalidSnapshotError,
models::{DecoderContext, TryFromWithBlock},
},
rfq::{
constants::get_liquorice_auth,
models::{ComponentLayout, TimestampHeader},
protocols::{
component::{decode_all_pairs_component, DecodedAllPairs},
maker_price_levels::MakerPriceLevels,
},
},
};
impl TryFromWithBlock<ComponentWithState, TimestampHeader> for LiquoriceAllPairsState {
type Error = InvalidSnapshotError;
async fn try_from_with_header(
snapshot: ComponentWithState,
_timestamp_header: TimestampHeader,
_account_balances: &HashMap<Bytes, HashMap<Bytes, Bytes>>,
all_tokens: &HashMap<Bytes, Token>,
_decoder_context: &DecoderContext,
) -> Result<Self, Self::Error> {
let DecodedAllPairs { books: price_levels, tokens, quote_rule } =
decode_all_pairs_component::<MakerPriceLevels>(&snapshot, all_tokens)?;
let auth = get_liquorice_auth().map_err(|e| {
InvalidSnapshotError::ValueError(format!("Failed to get Liquorice authentication: {e}"))
})?;
let mut builder =
LiquoriceClientBuilder::new(snapshot.component.chain, auth.solver, auth.key)
.tokens(tokens.keys().cloned().collect())
.component_layout(ComponentLayout::AllPairs);
if let Some(quote_rule) = quote_rule {
builder = builder.quote_rule(quote_rule);
}
let client = builder.build().map_err(|e| {
InvalidSnapshotError::MissingAttribute(format!("Couldn't create LiquoriceClient: {e}"))
})?;
LiquoriceAllPairsState::new(price_levels, tokens, client)
.map_err(|e| InvalidSnapshotError::ValueError(e.to_string()))
}
}
#[cfg(test)]
mod tests {
use std::env;
use num_bigint::BigUint;
use tycho_common::simulation::{errors::SimulationError, protocol_sim::ProtocolSim};
use super::*;
use crate::rfq::{
models::QuoteRule,
protocols::test_utils::{all_pairs_snapshot, decode, usdc, wbtc, weth},
};
#[tokio::test]
async fn test_decodes_price_levels() {
env::set_var("LIQUORICE_USER", "test_solver");
env::set_var("LIQUORICE_KEY", "test_key");
let price_levels = serde_json::json!([
{
"mm": "test_market_maker",
"base_token": wbtc().address, "quote_token": usdc().address,
"levels": [{ "q": "1.5", "p": "65000.0" }, { "q": "2.0", "p": "64950.0" }]
},
{
"mm": "mm_b",
"base_token": wbtc().address, "quote_token": usdc().address,
"levels": [{ "q": "0.5", "p": "65100.0" }]
},
{
"mm": "test_market_maker",
"base_token": weth().address, "quote_token": usdc().address,
"levels": [{ "q": "10", "p": "3000.0" }]
}
]);
let (snapshot, tokens) =
all_pairs_snapshot("rfq:liquorice", &[wbtc(), usdc(), weth()], &price_levels);
let state = decode::<LiquoriceAllPairsState>(snapshot, &tokens)
.await
.unwrap();
let wbtc_price_levels = state
.price_levels
.pair_price_levels(&wbtc().address, &usdc().address);
assert_eq!(wbtc_price_levels.len(), 2);
assert_eq!(wbtc_price_levels[0].market_maker, "mm_b");
assert_eq!(wbtc_price_levels[1].levels[0].quantity, 1.5);
assert_eq!(wbtc_price_levels[1].levels[0].price, 65000.0);
}
#[tokio::test]
async fn test_decodes_once_per_venue_attribute() {
env::set_var("LIQUORICE_USER", "test_solver");
env::set_var("LIQUORICE_KEY", "test_key");
let price_levels = serde_json::json!([
{
"mm": "mm_b",
"base_token": wbtc().address, "quote_token": usdc().address,
"levels": [{ "q": "0.5", "p": "65100.0" }]
},
{
"mm": "test_market_maker",
"base_token": weth().address, "quote_token": usdc().address,
"levels": [{ "q": "10", "p": "3000.0" }]
}
]);
let (mut snapshot, tokens) =
all_pairs_snapshot("rfq:liquorice", &[wbtc(), usdc(), weth()], &price_levels);
snapshot
.component
.static_attributes
.insert(
QuoteRule::ATTRIBUTE.to_string(),
QuoteRule::OncePerVenue
.as_str()
.as_bytes()
.into(),
);
let state = decode::<LiquoriceAllPairsState>(snapshot, &tokens)
.await
.unwrap();
let after_swap = state
.get_amount_out(BigUint::from(50_000_000u64), &wbtc(), &usdc())
.unwrap()
.new_state;
let result = after_swap.get_amount_out(
BigUint::from(1_000_000_000_000_000_000u64),
&weth(),
&usdc(),
);
assert!(
matches!(result, Err(SimulationError::RecoverableError(message)) if message.contains("already quoted in this route"))
);
}
}