tycho_simulation/rfq/protocols/liquorice/
all_pairs_decoder.rs1use std::collections::HashMap;
2
3use tycho_client::feed::synchronizer::ComponentWithState;
4use tycho_common::{models::token::Token, Bytes};
5
6use super::{all_pairs_state::LiquoriceAllPairsState, client_builder::LiquoriceClientBuilder};
7use crate::{
8 protocol::{
9 errors::InvalidSnapshotError,
10 models::{DecoderContext, TryFromWithBlock},
11 },
12 rfq::{
13 constants::get_liquorice_auth,
14 models::{ComponentLayout, TimestampHeader},
15 protocols::{
16 component::{decode_all_pairs_component, DecodedAllPairs},
17 maker_price_levels::MakerPriceLevels,
18 },
19 },
20};
21
22impl TryFromWithBlock<ComponentWithState, TimestampHeader> for LiquoriceAllPairsState {
23 type Error = InvalidSnapshotError;
24
25 async fn try_from_with_header(
26 snapshot: ComponentWithState,
27 _timestamp_header: TimestampHeader,
28 _account_balances: &HashMap<Bytes, HashMap<Bytes, Bytes>>,
29 all_tokens: &HashMap<Bytes, Token>,
30 _decoder_context: &DecoderContext,
31 ) -> Result<Self, Self::Error> {
32 let DecodedAllPairs { books: price_levels, tokens, quote_rule } =
33 decode_all_pairs_component::<MakerPriceLevels>(&snapshot, all_tokens)?;
34
35 let auth = get_liquorice_auth().map_err(|e| {
36 InvalidSnapshotError::ValueError(format!("Failed to get Liquorice authentication: {e}"))
37 })?;
38 let mut builder =
39 LiquoriceClientBuilder::new(snapshot.component.chain, auth.solver, auth.key)
40 .tokens(tokens.keys().cloned().collect())
41 .component_layout(ComponentLayout::AllPairs);
42 if let Some(quote_rule) = quote_rule {
43 builder = builder.quote_rule(quote_rule);
44 }
45 let client = builder.build().map_err(|e| {
46 InvalidSnapshotError::MissingAttribute(format!("Couldn't create LiquoriceClient: {e}"))
47 })?;
48
49 LiquoriceAllPairsState::new(price_levels, tokens, client)
50 .map_err(|e| InvalidSnapshotError::ValueError(e.to_string()))
51 }
52}
53
54#[cfg(test)]
55mod tests {
56 use std::env;
57
58 use num_bigint::BigUint;
59 use tycho_common::simulation::{errors::SimulationError, protocol_sim::ProtocolSim};
60
61 use super::*;
62 use crate::rfq::{
63 models::QuoteRule,
64 protocols::test_utils::{all_pairs_snapshot, decode, usdc, wbtc, weth},
65 };
66
67 #[tokio::test]
68 async fn test_decodes_price_levels() {
69 env::set_var("LIQUORICE_USER", "test_solver");
71 env::set_var("LIQUORICE_KEY", "test_key");
72 let price_levels = serde_json::json!([
73 {
74 "mm": "test_market_maker",
75 "base_token": wbtc().address, "quote_token": usdc().address,
76 "levels": [{ "q": "1.5", "p": "65000.0" }, { "q": "2.0", "p": "64950.0" }]
77 },
78 {
79 "mm": "mm_b",
80 "base_token": wbtc().address, "quote_token": usdc().address,
81 "levels": [{ "q": "0.5", "p": "65100.0" }]
82 },
83 {
84 "mm": "test_market_maker",
85 "base_token": weth().address, "quote_token": usdc().address,
86 "levels": [{ "q": "10", "p": "3000.0" }]
87 }
88 ]);
89 let (snapshot, tokens) =
90 all_pairs_snapshot("rfq:liquorice", &[wbtc(), usdc(), weth()], &price_levels);
91 let state = decode::<LiquoriceAllPairsState>(snapshot, &tokens)
92 .await
93 .unwrap();
94
95 let wbtc_price_levels = state
96 .price_levels
97 .pair_price_levels(&wbtc().address, &usdc().address);
98 assert_eq!(wbtc_price_levels.len(), 2);
99 assert_eq!(wbtc_price_levels[0].market_maker, "mm_b");
100 assert_eq!(wbtc_price_levels[1].levels[0].quantity, 1.5);
101 assert_eq!(wbtc_price_levels[1].levels[0].price, 65000.0);
102 }
103
104 #[tokio::test]
107 async fn test_decodes_once_per_venue_attribute() {
108 env::set_var("LIQUORICE_USER", "test_solver");
109 env::set_var("LIQUORICE_KEY", "test_key");
110 let price_levels = serde_json::json!([
111 {
112 "mm": "mm_b",
113 "base_token": wbtc().address, "quote_token": usdc().address,
114 "levels": [{ "q": "0.5", "p": "65100.0" }]
115 },
116 {
117 "mm": "test_market_maker",
118 "base_token": weth().address, "quote_token": usdc().address,
119 "levels": [{ "q": "10", "p": "3000.0" }]
120 }
121 ]);
122 let (mut snapshot, tokens) =
123 all_pairs_snapshot("rfq:liquorice", &[wbtc(), usdc(), weth()], &price_levels);
124 snapshot
125 .component
126 .static_attributes
127 .insert(
128 QuoteRule::ATTRIBUTE.to_string(),
129 QuoteRule::OncePerVenue
130 .as_str()
131 .as_bytes()
132 .into(),
133 );
134
135 let state = decode::<LiquoriceAllPairsState>(snapshot, &tokens)
136 .await
137 .unwrap();
138 let after_swap = state
139 .get_amount_out(BigUint::from(50_000_000u64), &wbtc(), &usdc())
140 .unwrap()
141 .new_state;
142
143 let result = after_swap.get_amount_out(
144 BigUint::from(1_000_000_000_000_000_000u64),
145 &weth(),
146 &usdc(),
147 );
148 assert!(
149 matches!(result, Err(SimulationError::RecoverableError(message)) if message.contains("already quoted in this route"))
150 );
151 }
152}