tycho_simulation/rfq/protocols/hashflow/
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::HashflowAllPairsState, client_builder::HashflowClientBuilder};
7use crate::{
8 protocol::{
9 errors::InvalidSnapshotError,
10 models::{DecoderContext, TryFromWithBlock},
11 },
12 rfq::{
13 constants::get_hashflow_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 HashflowAllPairsState {
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_hashflow_auth().map_err(|e| {
36 InvalidSnapshotError::ValueError(format!("Failed to get Hashflow authentication: {e}"))
37 })?;
38 let mut builder = HashflowClientBuilder::new(snapshot.component.chain, auth.user, auth.key)
39 .tokens(tokens.keys().cloned().collect())
40 .component_layout(ComponentLayout::AllPairs);
41 if let Some(quote_rule) = quote_rule {
42 builder = builder.quote_rule(quote_rule);
43 }
44 let client = builder.build().map_err(|e| {
45 InvalidSnapshotError::MissingAttribute(format!("Couldn't create HashflowClient: {e}"))
46 })?;
47
48 HashflowAllPairsState::new(price_levels, tokens, client)
49 .map_err(|e| InvalidSnapshotError::ValueError(e.to_string()))
50 }
51}
52
53#[cfg(test)]
54mod tests {
55 use std::env;
56
57 use num_bigint::BigUint;
58 use tycho_common::simulation::{errors::SimulationError, protocol_sim::ProtocolSim};
59
60 use super::*;
61 use crate::rfq::{
62 models::QuoteRule,
63 protocols::test_utils::{all_pairs_snapshot, decode, usdc, wbtc, weth},
64 };
65
66 #[tokio::test]
67 async fn test_decodes_price_levels() {
68 env::set_var("HASHFLOW_USER", "test_user");
70 env::set_var("HASHFLOW_KEY", "test_key");
71 let price_levels = serde_json::json!([
72 {
73 "mm": "test_market_maker",
74 "base_token": wbtc().address, "quote_token": usdc().address,
75 "levels": [{ "q": "1.5", "p": "65000.0" }, { "q": "2.0", "p": "64950.0" }]
76 },
77 {
78 "mm": "mm_b",
79 "base_token": wbtc().address, "quote_token": usdc().address,
80 "levels": [{ "q": "0.5", "p": "65100.0" }]
81 },
82 {
83 "mm": "test_market_maker",
84 "base_token": weth().address, "quote_token": usdc().address,
85 "levels": [{ "q": "10", "p": "3000.0" }]
86 }
87 ]);
88 let (snapshot, tokens) =
89 all_pairs_snapshot("rfq:hashflow", &[wbtc(), usdc(), weth()], &price_levels);
90 let state = decode::<HashflowAllPairsState>(snapshot, &tokens)
91 .await
92 .unwrap();
93
94 let wbtc_price_levels = state
95 .price_levels
96 .pair_price_levels(&wbtc().address, &usdc().address);
97 assert_eq!(wbtc_price_levels.len(), 2);
98 assert_eq!(wbtc_price_levels[0].market_maker, "mm_b");
99 assert_eq!(wbtc_price_levels[1].levels[0].quantity, 1.5);
100 assert_eq!(wbtc_price_levels[1].levels[0].price, 65000.0);
101 assert_eq!(
102 state
103 .price_levels
104 .pair_price_levels(&weth().address, &usdc().address)
105 .len(),
106 1
107 );
108 }
109
110 #[tokio::test]
113 async fn test_decodes_once_per_venue_attribute() {
114 env::set_var("HASHFLOW_USER", "test_user");
115 env::set_var("HASHFLOW_KEY", "test_key");
116 let price_levels = serde_json::json!([
117 {
118 "mm": "mm_b",
119 "base_token": wbtc().address, "quote_token": usdc().address,
120 "levels": [{ "q": "0.5", "p": "65100.0" }]
121 },
122 {
123 "mm": "test_market_maker",
124 "base_token": weth().address, "quote_token": usdc().address,
125 "levels": [{ "q": "10", "p": "3000.0" }]
126 }
127 ]);
128 let (mut snapshot, tokens) =
129 all_pairs_snapshot("rfq:hashflow", &[wbtc(), usdc(), weth()], &price_levels);
130 snapshot
131 .component
132 .static_attributes
133 .insert(
134 QuoteRule::ATTRIBUTE.to_string(),
135 QuoteRule::OncePerVenue
136 .as_str()
137 .as_bytes()
138 .into(),
139 );
140
141 let state = decode::<HashflowAllPairsState>(snapshot, &tokens)
142 .await
143 .unwrap();
144 let after_swap = state
145 .get_amount_out(BigUint::from(50_000_000u64), &wbtc(), &usdc())
146 .unwrap()
147 .new_state;
148
149 let result = after_swap.get_amount_out(
150 BigUint::from(1_000_000_000_000_000_000u64),
151 &weth(),
152 &usdc(),
153 );
154 assert!(
155 matches!(result, Err(SimulationError::RecoverableError(message)) if message.contains("already quoted in this route"))
156 );
157 }
158}