1use std::{
2 collections::{HashMap, HashSet},
3 str::FromStr,
4};
5
6use alloy::primitives::{utils::keccak256, Address, U256};
7use async_trait::async_trait;
8use futures::stream::BoxStream;
9use num_bigint::BigUint;
10use reqwest::Client;
11use serde::{Deserialize, Serialize};
12use tokio::time::{interval, timeout, Duration};
13use tracing::{error, info, warn};
14use tycho_common::{
15 models::{protocol::GetAmountOutParams, Chain},
16 simulation::indicatively_priced::SignedQuote,
17 Bytes,
18};
19
20use crate::{
21 evm::protocol::u256_num::biguint_to_u256,
22 rfq::{
23 client::RFQClient,
24 errors::RFQError,
25 models::{ComponentLayout, QuoteRule, TimestampHeader},
26 protocols::{
27 component,
28 hashflow::models::{
29 HashflowChain, HashflowMarketMakerLevels, HashflowMarketMakersResponse,
30 HashflowPriceLevelsResponse, HashflowQuoteRequest, HashflowQuoteResponse,
31 HashflowRFQ, HashflowRFQOptions,
32 },
33 maker_price_levels::{self, MakerLevelsWithTvl, MakerPriceLevels},
34 },
35 },
36 tycho_client::feed::synchronizer::{ComponentWithState, StateSyncMessage},
37 tycho_common::models::protocol::{ProtocolComponent, ProtocolComponentState},
38};
39
40#[derive(Clone, Debug, Serialize, Deserialize)]
41pub struct HashflowClient {
42 chain: Chain,
43 price_levels_endpoint: String,
44 market_makers_endpoint: String,
45 quote_endpoint: String,
46 tokens: HashSet<Bytes>,
48 tvl: f64,
50 #[serde(skip_serializing, default)]
51 auth_key: String,
52 #[serde(skip_serializing, default)]
53 auth_user: String,
54 quote_tokens: HashSet<Bytes>,
56 poll_time: Duration,
57 quote_timeout: Duration,
58 #[serde(default = "default_protocol_system")]
59 protocol_system: String,
60 #[serde(default)]
61 component_layout: ComponentLayout,
62 #[serde(default = "HashflowClient::default_quote_rule")]
64 quote_rule: QuoteRule,
65}
66
67fn default_protocol_system() -> String {
68 HashflowClient::PROTOCOL_SYSTEM.to_string()
69}
70
71impl HashflowClient {
72 pub const PROTOCOL_SYSTEM: &'static str = "rfq:hashflow";
73 pub const FALLBACK_PROTOCOL_SYSTEM: &'static str = "fallback:rfq:hashflow";
75 pub const DEFAULT_QUOTE_RULE: QuoteRule = QuoteRule::OncePerMaker;
76
77 pub(super) fn via_fallback_router(mut self) -> Self {
78 self.protocol_system = Self::FALLBACK_PROTOCOL_SYSTEM.to_string();
79 self
80 }
81
82 #[allow(clippy::too_many_arguments)]
83 pub fn new(
84 chain: Chain,
85 tokens: HashSet<Bytes>,
86 tvl: f64,
87 quote_tokens: HashSet<Bytes>,
88 auth_user: String,
89 auth_key: String,
90 poll_time: Duration,
91 quote_timeout: Duration,
92 ) -> Result<Self, RFQError> {
93 Ok(Self {
94 chain,
95 price_levels_endpoint: "https://api.hashflow.com/taker/v3/price-levels".to_string(),
96 market_makers_endpoint: "https://api.hashflow.com/taker/v3/market-makers".to_string(),
97 quote_endpoint: "https://api.hashflow.com/taker/v3/rfq".to_string(),
98 tokens,
99 tvl,
100 auth_key,
101 auth_user,
102 quote_tokens,
103 poll_time,
104 quote_timeout,
105 protocol_system: Self::PROTOCOL_SYSTEM.to_string(),
106 component_layout: ComponentLayout::PerPair,
107 quote_rule: Self::DEFAULT_QUOTE_RULE,
108 })
109 }
110
111 fn default_quote_rule() -> QuoteRule {
112 Self::DEFAULT_QUOTE_RULE
113 }
114
115 pub(crate) fn with_component_layout(mut self, component_layout: ComponentLayout) -> Self {
116 self.component_layout = component_layout;
117 self
118 }
119
120 pub(crate) fn with_quote_rule(mut self, quote_rule: QuoteRule) -> Self {
121 self.quote_rule = quote_rule;
122 self
123 }
124
125 fn stream_name(&self) -> &'static str {
128 match self.component_layout {
129 ComponentLayout::PerPair => "hashflow",
130 ComponentLayout::AllPairs => "hashflow_all_pairs",
131 }
132 }
133
134 pub fn quote_rule(&self) -> QuoteRule {
135 self.quote_rule
136 }
137
138 #[cfg(test)]
139 pub(crate) fn with_quote_endpoint(mut self, quote_endpoint: String) -> Self {
140 self.quote_endpoint = quote_endpoint;
141 self
142 }
143
144 fn normalize_tvl(
147 &self,
148 raw_tvl: f64,
149 quote_token: Bytes,
150 levels_by_mm: &HashMap<String, Vec<HashflowMarketMakerLevels>>,
151 ) -> Result<f64, RFQError> {
152 if self.quote_tokens.contains("e_token) {
154 return Ok(raw_tvl);
155 }
156
157 for approved_quote_token in &self.quote_tokens {
160 for mm_levels_inner in levels_by_mm.values() {
161 for quote_mm_level in mm_levels_inner {
162 if quote_mm_level.pair.base_token == quote_token &&
164 quote_mm_level.pair.quote_token == *approved_quote_token
165 {
166 if let Some(price) = quote_mm_level.get_price(1.0) {
167 return Ok(raw_tvl * price);
168 }
169 }
170 }
171 }
172 }
173
174 Ok(0.0)
176 }
177
178 fn create_component_with_state(
179 &self,
180 component_id: String,
181 tokens: Vec<Bytes>,
182 mm_name: &str,
183 mm_level: &HashflowMarketMakerLevels,
184 tvl: f64,
185 ) -> ComponentWithState {
186 let protocol_component = ProtocolComponent {
187 id: component_id.clone(),
188 protocol_system: self.protocol_system.clone(),
189 protocol_type_name: "hashflow_pool".to_string(),
190 chain: self.chain,
191 tokens,
192 contract_addresses: vec![], ..Default::default()
194 };
195
196 let mut attributes = HashMap::new();
197
198 if !mm_level.levels.is_empty() {
200 let levels_json = serde_json::to_string(&mm_level.levels).unwrap_or_default();
201 attributes.insert("levels".to_string(), levels_json.as_bytes().to_vec().into());
202 }
203 attributes.insert("mm".to_string(), mm_name.as_bytes().to_vec().into());
204
205 ComponentWithState {
206 state: ProtocolComponentState::new(&component_id, attributes, HashMap::new()),
207 component: protocol_component,
208 component_tvl: Some(tvl),
209 entrypoints: vec![],
210 }
211 }
212
213 fn levels_above_tvl_threshold<'a>(
216 &self,
217 levels_by_mm: &'a HashMap<String, Vec<HashflowMarketMakerLevels>>,
218 ) -> Result<Vec<MakerLevelsWithTvl<'a, HashflowMarketMakerLevels>>, RFQError> {
219 let mut kept = Vec::new();
220 for (mm_name, mm_levels) in levels_by_mm.iter() {
221 for mm_level in mm_levels {
222 let base_token = &mm_level.pair.base_token;
223 let quote_token = &mm_level.pair.quote_token;
224 if !(self.tokens.contains(base_token) && self.tokens.contains(quote_token)) {
225 continue;
226 }
227 let normalized_tvl = self.normalize_tvl(
228 mm_level.calculate_tvl(),
229 mm_level.pair.quote_token.clone(),
230 levels_by_mm,
231 )?;
232 if normalized_tvl < self.tvl {
233 info!(
234 "Filtering out component {} due to low TVL: {:.2} < {:.2}",
235 pair_component_id(mm_level),
236 normalized_tvl,
237 self.tvl
238 );
239 continue;
240 }
241 kept.push(MakerLevelsWithTvl {
242 market_maker: mm_name,
243 levels: mm_level,
244 tvl: normalized_tvl,
245 });
246 }
247 }
248 Ok(kept)
249 }
250
251 fn pair_components(
254 &self,
255 levels_by_mm: &HashMap<String, Vec<HashflowMarketMakerLevels>>,
256 ) -> Result<HashMap<String, ComponentWithState>, RFQError> {
257 let mut new_components = HashMap::new();
258 for MakerLevelsWithTvl { market_maker: mm_name, levels: mm_level, tvl } in
259 self.levels_above_tvl_threshold(levels_by_mm)?
260 {
261 let component_id = pair_component_id(mm_level);
262 let tokens = vec![mm_level.pair.base_token.clone(), mm_level.pair.quote_token.clone()];
263 let component_with_state = self.create_component_with_state(
264 component_id.clone(),
265 tokens,
266 mm_name,
267 mm_level,
268 tvl,
269 );
270 new_components.insert(component_id, component_with_state);
271 }
272 Ok(new_components)
273 }
274
275 fn all_pairs_component(
278 &self,
279 levels_by_mm: &HashMap<String, Vec<HashflowMarketMakerLevels>>,
280 ) -> Result<Option<ComponentWithState>, RFQError> {
281 let mut price_levels = Vec::new();
282 for MakerLevelsWithTvl { market_maker: mm_name, levels: mm_level, tvl } in
283 self.levels_above_tvl_threshold(levels_by_mm)?
284 {
285 let maker_levels = MakerPriceLevels {
286 market_maker: mm_name.to_string(),
287 base_token: mm_level.pair.base_token.clone(),
288 quote_token: mm_level.pair.quote_token.clone(),
289 levels: mm_level.levels.clone(),
290 };
291 price_levels.push((maker_levels, tvl));
292 }
293 maker_price_levels::all_pairs_component(
294 &self.protocol_system,
295 "hashflow_pool",
296 self.chain,
297 price_levels,
298 self.quote_rule,
299 )
300 }
301
302 pub(crate) async fn request_quote(
306 &self,
307 params: &GetAmountOutParams,
308 market_maker: Option<&str>,
309 ) -> Result<SignedQuote, RFQError> {
310 let hashflow_chain = HashflowChain::from(self.chain);
311 let effective_trader = Bytes::from(Address::random().to_vec());
316 let quote_request = HashflowQuoteRequest {
317 source: self.auth_user.clone(),
318 base_chain: hashflow_chain.clone(),
319 quote_chain: hashflow_chain,
320 rfqs: vec![HashflowRFQ {
321 base_token: params.token_in.to_string(),
322 quote_token: params.token_out.to_string(),
323 base_token_amount: Some(params.amount_in.to_string()),
324 quote_token_amount: None,
325 trader: params.receiver.to_string(),
326 effective_trader: Some(effective_trader.to_string()),
327 market_makers: market_maker.map(|mm| vec![mm.to_string()]),
328 options: market_maker
329 .map(|_| HashflowRFQOptions { do_not_retry_with_other_makers: true }),
330 }],
331 calldata: false,
332 };
333 self.send_quote_request(params, "e_request, &effective_trader)
334 .await
335 }
336
337 async fn fetch_market_makers(&mut self) -> Result<Vec<String>, RFQError> {
338 let query_params = vec![
339 ("source", self.auth_user.clone()),
340 ("baseChainType", "evm".to_string()),
341 ("baseChainId", self.chain.id().to_string()),
342 ];
343
344 let http_client = Client::new();
345 let request = http_client
346 .get(&self.market_makers_endpoint)
347 .query(&query_params)
348 .header("accept", "application/json")
349 .header("Authorization", &self.auth_key);
350
351 let response = request.send().await.map_err(|e| {
352 RFQError::ConnectionError(format!("Failed to fetch market makers: {e}"))
353 })?;
354
355 if !response.status().is_success() {
356 return Err(RFQError::ConnectionError(format!(
357 "HTTP error {}: {}",
358 response.status(),
359 response
360 .text()
361 .await
362 .unwrap_or_default()
363 )));
364 }
365
366 let mm_response: HashflowMarketMakersResponse = response.json().await.map_err(|e| {
367 RFQError::ParsingError(format!("Failed to parse market makers response: {e}"))
368 })?;
369
370 info!(
371 "Fetched {} market makers: {:?}",
372 mm_response.market_makers.len(),
373 mm_response.market_makers
374 );
375
376 Ok(mm_response.market_makers)
377 }
378
379 async fn fetch_price_levels(
380 &self,
381 market_makers: &Vec<String>,
382 ) -> Result<HashMap<String, Vec<HashflowMarketMakerLevels>>, RFQError> {
383 let mut query_params = vec![
384 ("source", self.auth_user.clone()),
385 ("baseChainType", "evm".to_string()),
386 ("baseChainId", self.chain.id().to_string()),
387 ];
388
389 for mm in market_makers {
391 query_params.push(("marketMakers[]", mm.clone()));
392 }
393
394 let http_client = Client::new();
395 let request = http_client
396 .get(&self.price_levels_endpoint)
397 .query(&query_params)
398 .header("accept", "application/json")
399 .header("Authorization", &self.auth_key);
400
401 let response = request
402 .send()
403 .await
404 .map_err(|e| RFQError::ConnectionError(format!("Failed to fetch price levels: {e}")))?;
405
406 if !response.status().is_success() {
407 return Err(RFQError::ConnectionError(format!(
408 "HTTP error {}: {}",
409 response.status(),
410 response
411 .text()
412 .await
413 .unwrap_or_default()
414 )));
415 }
416
417 let price_response: HashflowPriceLevelsResponse = response.json().await.map_err(|e| {
418 RFQError::ParsingError(format!("Failed to parse price levels response: {e}"))
419 })?;
420
421 if price_response.status != "success" {
422 let error = match price_response.error {
423 Some(error) => error.to_string(),
424 None => "no error details".to_string(),
425 };
426 return Err(RFQError::InvalidInput(format!("API returned error status: {error}")));
427 }
428
429 price_response
430 .levels
431 .ok_or_else(|| RFQError::ParsingError("API response missing levels".to_string()))
432 }
433
434 async fn send_quote_request(
435 &self,
436 params: &GetAmountOutParams,
437 quote_request: &HashflowQuoteRequest,
438 effective_trader: &Bytes,
439 ) -> Result<SignedQuote, RFQError> {
440 let url = self.quote_endpoint.clone();
441
442 let start_time = std::time::Instant::now();
443 const MAX_RETRIES: u32 = 3;
444 let mut last_error = None;
445
446 for attempt in 0..MAX_RETRIES {
447 let elapsed = start_time.elapsed();
449 if elapsed >= self.quote_timeout {
450 return Err(last_error.unwrap_or_else(|| {
451 RFQError::ConnectionError(format!(
452 "Hashflow quote request timed out after {} seconds",
453 self.quote_timeout.as_secs()
454 ))
455 }));
456 }
457
458 let remaining_time = self.quote_timeout - elapsed;
459
460 let http_client = Client::new();
461 let request = http_client
462 .post(&url)
463 .json("e_request)
464 .header("accept", "application/json")
465 .header("Authorization", &self.auth_key);
466
467 let response = match timeout(remaining_time, request.send()).await {
468 Ok(Ok(resp)) => resp,
469 Ok(Err(e)) => {
470 warn!(
471 "Hashflow quote request failed (attempt {}/{}): {}",
472 attempt + 1,
473 MAX_RETRIES,
474 e
475 );
476 last_error = Some(RFQError::ConnectionError(format!(
477 "Failed to send Hashflow quote request: {e}"
478 )));
479 if attempt < MAX_RETRIES - 1 {
480 tokio::time::sleep(Duration::from_millis(100)).await;
481 continue;
482 } else {
483 return Err(last_error.unwrap());
484 }
485 }
486 Err(_) => {
487 return Err(RFQError::ConnectionError(format!(
488 "Hashflow quote request timed out after {} seconds",
489 self.quote_timeout.as_secs()
490 )));
491 }
492 };
493
494 if response.status() != 200 {
495 let err_msg = match response.text().await {
496 Ok(text) => text,
497 Err(e) => {
498 warn!(
499 "Hashflow error response parsing failed (attempt {}/{}): {}",
500 attempt + 1,
501 MAX_RETRIES,
502 e
503 );
504 last_error = Some(RFQError::ParsingError(format!(
505 "Failed to read response text from Hashflow failed request: {e}"
506 )));
507 if attempt < MAX_RETRIES - 1 {
508 tokio::time::sleep(Duration::from_millis(100)).await;
509 continue;
510 } else {
511 return Err(last_error.unwrap());
512 }
513 }
514 };
515 last_error = Some(RFQError::FatalError(format!(
516 "Failed to send Hashflow quote request: {err_msg}",
517 )));
518 if attempt < MAX_RETRIES - 1 {
519 warn!(
520 "Hashflow returned non-200 status (attempt {}/{}): {}",
521 attempt + 1,
522 MAX_RETRIES,
523 err_msg
524 );
525 tokio::time::sleep(Duration::from_millis(100)).await;
526 continue;
527 } else {
528 return Err(last_error.unwrap());
529 }
530 }
531
532 let quote_response = match response
533 .json::<HashflowQuoteResponse>()
534 .await
535 {
536 Ok(resp) => resp,
537 Err(e) => {
538 warn!(
539 "Hashflow quote response parsing failed (attempt {}/{}): {}",
540 attempt + 1,
541 MAX_RETRIES,
542 e
543 );
544 last_error = Some(RFQError::ParsingError(format!(
545 "Failed to parse Hashflow quote response: {e}"
546 )));
547 if attempt < MAX_RETRIES - 1 {
548 tokio::time::sleep(Duration::from_millis(100)).await;
549 continue;
550 } else {
551 return Err(last_error.unwrap());
552 }
553 }
554 };
555
556 match quote_response.status.as_str() {
557 "success" => {
558 if let Some(quotes) = quote_response.quotes {
559 if quotes.is_empty() {
560 return Err(RFQError::QuoteNotFound(format!(
561 "Hashflow quote not found for {} {} ->{}",
562 params.amount_in, params.token_in, params.token_out,
563 )));
564 }
565 let quote = quotes[0].clone();
567 quote.validate(params, effective_trader)?;
568
569 let mut quote_attributes: HashMap<String, Bytes> = HashMap::new();
570 quote_attributes.insert("pool".to_string(), quote.quote_data.pool);
571 if let Some(external_account) = quote.quote_data.external_account {
572 quote_attributes
573 .insert("external_account".to_string(), external_account);
574 } else {
575 quote_attributes.insert(
576 "external_account".to_string(),
577 Bytes::from_str(&Address::ZERO.to_string()).map_err(|_| {
578 RFQError::ParsingError(
579 "Failed to parse zero address".to_string(),
580 )
581 })?,
582 );
583 }
584 quote_attributes.insert("trader".to_string(), quote.quote_data.trader);
585 quote_attributes
586 .insert("effective_trader".to_string(), effective_trader.clone());
587 quote_attributes
588 .insert("base_token".to_string(), quote.quote_data.base_token);
589 quote_attributes
590 .insert("quote_token".to_string(), quote.quote_data.quote_token);
591 quote_attributes.insert(
592 "base_token_amount".to_string(),
593 Bytes::from(
594 biguint_to_u256(
595 &BigUint::from_str("e.quote_data.base_token_amount)
596 .map_err(|_| {
597 RFQError::ParsingError(format!(
598 "Failed to parse base token amount: {}",
599 quote.quote_data.base_token_amount
600 ))
601 })?,
602 )
603 .to_be_bytes::<32>()
604 .to_vec(),
605 ),
606 );
607 quote_attributes.insert(
608 "quote_token_amount".to_string(),
609 Bytes::from(
610 biguint_to_u256(
611 &BigUint::from_str("e.quote_data.quote_token_amount)
612 .map_err(|_| {
613 RFQError::ParsingError(format!(
614 "Failed to parse quote token amount: {}",
615 quote.quote_data.quote_token_amount
616 ))
617 })?,
618 )
619 .to_be_bytes::<32>()
620 .to_vec(),
621 ),
622 );
623 quote_attributes.insert(
624 "quote_expiry".to_string(),
625 Bytes::from(
626 U256::from(quote.quote_data.quote_expiry)
627 .to_be_bytes::<32>()
628 .to_vec(),
629 ),
630 );
631 quote_attributes.insert(
632 "nonce".to_string(),
633 Bytes::from(
634 U256::from(quote.quote_data.nonce)
635 .to_be_bytes::<32>()
636 .to_vec(),
637 ),
638 );
639 quote_attributes.insert("tx_id".to_string(), quote.quote_data.tx_id);
640 quote_attributes.insert("signature".to_string(), quote.signature);
641
642 let signed_quote = SignedQuote {
643 base_token: params.token_in.clone(),
644 quote_token: params.token_out.clone(),
645 amount_in: BigUint::from_str("e.quote_data.base_token_amount)
646 .map_err(|_| {
647 RFQError::ParsingError(format!(
648 "Failed to parse amount in string: {}",
649 quote.quote_data.base_token_amount
650 ))
651 })?,
652 amount_out: BigUint::from_str("e.quote_data.quote_token_amount)
653 .map_err(|_| {
654 RFQError::ParsingError(format!(
655 "Failed to parse amount out string: {}",
656 quote.quote_data.quote_token_amount
657 ))
658 })?,
659 quote_attributes,
660 };
661 return Ok(signed_quote);
662 } else {
663 return Err(RFQError::QuoteNotFound(format!(
664 "Hashflow quote not found for {} {} ->{}",
665 params.amount_in, params.token_in, params.token_out,
666 )));
667 }
668 }
669 "fail" => {
670 let Some(error) = quote_response.error else {
671 return Err(RFQError::FatalError(
672 "Hashflow API error: request failed without an error".to_string(),
673 ));
674 };
675 return Err(RFQError::FatalError(format!("Hashflow API error: {error}")));
676 }
677 _ => {
678 return Err(RFQError::FatalError(
679 "Hashflow API error: Unknown status".to_string(),
680 ));
681 }
682 }
683 }
684
685 Err(last_error.unwrap_or_else(|| {
686 RFQError::ConnectionError("Hashflow quote request failed after retries".to_string())
687 }))
688 }
689}
690
691fn pair_component_id(mm_level: &HashflowMarketMakerLevels) -> String {
693 let pair_str = format!(
694 "hashflow_{}/{}",
695 hex::encode(&mm_level.pair.base_token),
696 hex::encode(&mm_level.pair.quote_token)
697 );
698 format!("{}", keccak256(pair_str.as_bytes()))
699}
700
701#[async_trait]
702impl RFQClient for HashflowClient {
703 fn component_layout(&self) -> ComponentLayout {
704 self.component_layout
705 }
706
707 fn stream(
708 &self,
709 ) -> BoxStream<'static, Result<(String, StateSyncMessage<TimestampHeader>), RFQError>> {
710 let mut client = self.clone();
711
712 Box::pin(async_stream::stream! {
713 let mut current_components: HashMap<String, ComponentWithState> = HashMap::new();
714 let mut ticker = interval(client.poll_time);
715
716 info!("Starting Hashflow price levels polling every {} seconds", client.poll_time.as_secs());
717 info!("TVL threshold: {:.2}", client.tvl);
718
719 loop {
720 ticker.tick().await;
721
722 let market_makers;
723 match client.fetch_market_makers().await {
724 Ok(mms) => {
725 market_makers = mms;
726 info!("Successfully fetched market makers");
727 }
728 Err(e) => {
729 info!("Failed to fetch market makers: {}", e);
730 continue;
731 }
732 }
733
734 let levels_by_mm = match client.fetch_price_levels(&market_makers).await {
735 Ok(levels_by_mm) => levels_by_mm,
736 Err(e) => {
737 error!("Failed to fetch price levels from Hashflow API: {}", e);
738 continue;
739 }
740 };
741 info!("Fetched price levels from {} market makers", levels_by_mm.len());
742 let components = match client.component_layout {
743 ComponentLayout::PerPair => client.pair_components(&levels_by_mm)?,
744 ComponentLayout::AllPairs => match client.all_pairs_component(&levels_by_mm) {
745 Ok(component) => component
746 .into_iter()
747 .map(|component| (component.component.id.clone(), component))
748 .collect(),
749 Err(e) => {
750 error!("Failed to build the Hashflow component: {}", e);
751 continue;
752 }
753 },
754 };
755 let timestamp = component::unix_timestamp()?;
756 let msg = component::poll_message(&mut current_components, components, timestamp);
757 yield Ok((client.stream_name().to_string(), msg));
758 }
759 })
760 }
761
762 async fn request_binding_quote(
763 &self,
764 params: &GetAmountOutParams,
765 ) -> Result<SignedQuote, RFQError> {
766 self.request_quote(params, None).await
767 }
768}
769
770#[cfg(test)]
771mod tests {
772 use std::{env, str::FromStr, time::Duration};
773
774 use dotenv::dotenv;
775 use futures::StreamExt;
776 use rstest::rstest;
777 use tokio::time::timeout;
778
779 use super::*;
780 use crate::rfq::{
781 constants::get_hashflow_auth,
782 models::PriceLevel,
783 protocols::{
784 component::{BOOKS_ATTRIBUTE, SWAP_DIRECTIONS_ATTRIBUTE},
785 hashflow::{client_builder::HashflowClientBuilder, models::HashflowPair},
786 test_utils::{
787 effective_trader_of, mock_quote_server, quote_params, read_request_body,
788 HASHFLOW_QUOTE_RESPONSE,
789 },
790 },
791 };
792
793 #[test]
794 fn test_normalize_tvl_same_quote_token() {
795 let client = create_test_client();
796 let levels = HashMap::new();
797
798 let result = client.normalize_tvl(
800 1000.0,
801 Bytes::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap(),
802 &levels,
803 );
804 assert!(result.is_ok());
805 assert_eq!(result.unwrap(), 1000.0);
806 }
807
808 #[test]
809 fn test_normalize_tvl_different_quote_token() {
810 let client = create_test_client();
811 let mut levels = HashMap::new();
812 let weth = Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap();
813 let usdc = Bytes::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap();
814
815 let eth_usdc_level = HashflowMarketMakerLevels {
817 pair: HashflowPair { base_token: weth.clone(), quote_token: usdc },
818 levels: vec![PriceLevel { quantity: 1.0, price: 3000.0 } ],
819 };
820
821 levels.insert("test_mm".to_string(), vec![eth_usdc_level]);
822
823 let result = client.normalize_tvl(2.0, weth, &levels);
825 assert!(result.is_ok());
826 assert_eq!(result.unwrap(), 6000.0);
828 }
829
830 #[test]
831 fn test_fallback_router_labels_components() {
832 let weth = Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap();
833 let usdc = Bytes::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap();
834 let level = HashflowMarketMakerLevels {
835 pair: HashflowPair { base_token: weth.clone(), quote_token: usdc.clone() },
836 levels: vec![PriceLevel { quantity: 1.0, price: 3000.0 }],
837 };
838 let builder = || HashflowClientBuilder::new(Chain::Ethereum, String::new(), String::new());
839 let label = |client: HashflowClient| {
840 client
841 .create_component_with_state(
842 String::from("hashflow"),
843 vec![weth.clone(), usdc.clone()],
844 "test_mm",
845 &level,
846 0.0,
847 )
848 .component
849 .protocol_system
850 };
851
852 assert_eq!(label(builder().build().unwrap()), HashflowClient::PROTOCOL_SYSTEM);
853 assert_eq!(
854 label(
855 builder()
856 .with_fallback_router()
857 .build()
858 .unwrap()
859 ),
860 HashflowClient::FALLBACK_PROTOCOL_SYSTEM
861 );
862 assert_eq!(
863 HashflowClient::FALLBACK_PROTOCOL_SYSTEM,
864 tycho_execution::encoding::evm::HASHFLOW_FALLBACK_PROTOCOL_SYSTEM
865 );
866 }
867
868 #[test]
869 fn test_normalize_tvl_no_conversion_available() {
870 let client = create_test_client();
871 let levels = HashMap::new();
872 let result = client.normalize_tvl(
873 1000.0,
874 Bytes::from_str("0x1234567890123456789012345678901234567890").unwrap(),
875 &levels,
876 );
877 assert!(result.is_ok());
878 assert_eq!(result.unwrap(), 0.0);
879 }
880
881 fn create_test_client() -> HashflowClient {
882 let quote_tokens = HashSet::from([
883 Bytes::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap(), Bytes::from_str("0xdAC17F958D2ee523a2206206994597C13D831ec7").unwrap(), ]);
886
887 HashflowClient::new(
888 Chain::Ethereum,
889 HashSet::new(),
890 1.0,
891 quote_tokens,
892 "test_user".to_string(),
893 "test_key".to_string(),
894 Duration::from_secs(5),
895 Duration::from_secs(5),
896 )
897 .unwrap()
898 }
899
900 #[rstest]
901 #[case::per_pair(ComponentLayout::PerPair)]
902 #[case::all_pairs(ComponentLayout::AllPairs)]
903 #[tokio::test]
904 #[ignore] async fn test_hashflow_api_polling(#[case] layout: ComponentLayout) {
906 dotenv().expect("Missing .env file");
907 let auth = get_hashflow_auth().unwrap();
908
909 let wbtc = Bytes::from_str("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599").unwrap();
910 let weth = Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap();
911
912 let tokens = HashSet::from([wbtc, weth.clone()]);
913
914 let quote_tokens = HashSet::from([
915 Bytes::from_str("0xa0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap(), Bytes::from_str("0xdac17f958d2ee523a2206206994597c13d831ec7").unwrap(), ]);
918
919 let client = HashflowClient::new(
920 Chain::Ethereum,
921 tokens,
922 1.0, quote_tokens,
924 auth.user,
925 auth.key,
926 Duration::from_secs(1),
927 Duration::from_secs(5),
928 )
929 .unwrap()
930 .with_component_layout(layout);
931
932 let mut stream = client.stream();
933
934 let result = timeout(Duration::from_secs(10), async {
935 let mut message_count = 0;
936 let max_messages = 3;
937 let mut total_components_received = 0;
938
939 while let Some(result) = stream.next().await {
940 match result {
941 Ok((component_id, msg)) => {
942 println!("Received message with ID: {component_id}");
943
944 assert!(!component_id.is_empty());
945 assert_eq!(component_id, client.stream_name());
946 assert!(msg.header.timestamp > 0);
947
948 let snapshot = &msg.snapshots;
949 total_components_received += snapshot.states.len();
950
951 println!("Received {} components in this message (Total so far: {})",
952 snapshot.states.len(), total_components_received);
953
954 if layout == ComponentLayout::AllPairs {
955 assert!(snapshot.states.len() <= 1, "one all-pairs component");
956 }
957 for (id, component_with_state) in &snapshot.states {
958 let attributes = &component_with_state.state.attributes;
959 match layout {
960 ComponentLayout::PerPair => {
961 assert!(!attributes["levels"].is_empty());
962 assert!(!attributes["mm"].is_empty());
963 }
964 ComponentLayout::AllPairs => {
965 let expected_id = component::component_id(
966 HashflowClient::PROTOCOL_SYSTEM,
967 client.chain,
968 );
969 assert_eq!(id, &expected_id);
970 let price_levels: Vec<MakerPriceLevels> =
971 serde_json::from_slice(&attributes[BOOKS_ATTRIBUTE]).unwrap();
972 assert!(!price_levels.is_empty());
973 let directions = &component_with_state.component.static_attributes[SWAP_DIRECTIONS_ATTRIBUTE];
974 assert_eq!(directions.len() % 40, 0);
975 }
976 }
977 if let Some(tvl) = component_with_state.component_tvl {
978 assert!(tvl >= 1.0);
979 println!("Component {id} TVL: ${tvl:.2}");
980 }
981 }
982
983 message_count += 1;
984 if message_count >= max_messages {
985 break;
986 }
987 }
988 Err(e) => {
989 panic!("Stream error: {e}");
990 }
991 }
992 }
993
994 assert!(message_count > 0, "Should have received at least one message");
995 assert!(total_components_received >= 1, "Should have received at least 1 component with $1 TVL threshold");
996 println!("Successfully received {message_count} messages with {total_components_received} total components");
997 })
998 .await;
999
1000 match result {
1001 Ok(_) => println!("Test completed successfully"),
1002 Err(_) => panic!("Test timed out - no messages received within 5 seconds"),
1003 }
1004 }
1005
1006 #[tokio::test]
1007 #[ignore] async fn test_request_binding_quote() {
1009 let wbtc = Bytes::from_str("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599").unwrap();
1010 let weth = Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap();
1011
1012 let auth_user = String::from("propellerheads");
1013 dotenv().expect("Missing .env file");
1014 let auth_key = env::var("HASHFLOW_KEY").unwrap();
1015
1016 let client = HashflowClient::new(
1017 Chain::Ethereum,
1018 HashSet::from_iter(vec![weth.clone(), wbtc.clone()]),
1019 10.0,
1020 HashSet::new(),
1021 auth_user,
1022 auth_key,
1023 Duration::from_secs(0),
1024 Duration::from_secs(5),
1025 )
1026 .unwrap();
1027
1028 let router = Bytes::from_str("0xfD0b31d2E955fA55e3fa641Fe90e08b677188d35").unwrap();
1029
1030 let params = GetAmountOutParams {
1031 amount_in: BigUint::from(1_000000000000000000u64),
1032 token_in: weth.clone(),
1033 token_out: wbtc.clone(),
1034 sender: router.clone(),
1035 receiver: router.clone(),
1036 origin: None,
1037 };
1038 let quote = client
1039 .request_binding_quote(¶ms)
1040 .await
1041 .unwrap();
1042
1043 assert_eq!(quote.base_token, weth);
1044 assert_eq!(quote.quote_token, wbtc);
1045 assert_eq!(quote.amount_in, BigUint::from(1_000000000000000000u64));
1046
1047 assert!(quote.amount_out > BigUint::from(3000000u64));
1049
1050 assert_eq!(quote.quote_attributes.len(), 12);
1051 let expected_attributes = [
1052 "pool",
1053 "external_account",
1054 "trader",
1055 "effective_trader",
1056 "base_token",
1057 "quote_token",
1058 "base_token_amount",
1059 "quote_token_amount",
1060 "quote_expiry",
1061 "nonce",
1062 "tx_id",
1063 "signature",
1064 ];
1065 for attr in expected_attributes {
1066 assert!(
1067 quote
1068 .quote_attributes
1069 .contains_key(attr),
1070 "Missing attribute: {attr}"
1071 );
1072 }
1073 assert_eq!(
1074 quote
1075 .quote_attributes
1076 .get("trader")
1077 .unwrap(),
1078 &router
1079 );
1080 }
1081
1082 const QUOTE_RESPONSE_WITHOUT_EFFECTIVE_TRADER: &str = r#"{"status":"success","error":null,"rfqId":"test-rfq-id","internalRfqIds":null,"quotes":[{"quoteData":{"pool":"0x71D9750ECF0c5081FAE4E3EDC4253E52024b0B59","externalAccount":null,"trader":"0xfD0b31d2E955fA55e3fa641Fe90e08b677188d35","baseToken":"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2","baseTokenAmount":"1000000000000000000","quoteToken":"0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599","quoteTokenAmount":"3329502","quoteExpiry":1707847360,"nonce":1707844960943648659,"txid":"0x0000000000000000000000000000000000000000000000000000000000000001"},"signature":"0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12"}]}"#;
1083
1084 fn create_test_hashflow_client(
1085 quote_endpoint: String,
1086 quote_timeout: Duration,
1087 ) -> HashflowClient {
1088 let token_in = Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap();
1089 let token_out = Bytes::from_str("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599").unwrap();
1090
1091 HashflowClient {
1092 chain: Chain::Ethereum,
1093 price_levels_endpoint: "http://unused/price-levels".to_string(),
1094 market_makers_endpoint: "http://unused/market-makers".to_string(),
1095 quote_endpoint,
1096 tokens: HashSet::from([token_in, token_out]),
1097 tvl: 10.0,
1098 auth_key: "test_key".to_string(),
1099 auth_user: "test_user".to_string(),
1100 quote_tokens: HashSet::new(),
1101 poll_time: Duration::from_secs(0),
1102 quote_timeout,
1103 protocol_system: HashflowClient::PROTOCOL_SYSTEM.to_string(),
1104 component_layout: ComponentLayout::PerPair,
1105 quote_rule: QuoteRule::OncePerMaker,
1106 }
1107 }
1108
1109 #[tokio::test]
1110 async fn test_request_binding_quote_without_effective_trader() {
1111 let (addr, _) = mock_quote_server(0, QUOTE_RESPONSE_WITHOUT_EFFECTIVE_TRADER).await;
1114 let client = create_test_hashflow_client(
1115 format!("http://127.0.0.1:{}/rfq", addr.port()),
1116 Duration::from_secs(1),
1117 );
1118 let params = quote_params();
1119
1120 let err = client
1121 .request_binding_quote(¶ms)
1122 .await
1123 .unwrap_err();
1124
1125 assert!(format!("{err:?}").contains("Effective trader mismatch"));
1126 }
1127
1128 #[tokio::test]
1129 async fn test_request_binding_quote_field_mapping() {
1130 let (addr, request_log) = mock_quote_server(0, HASHFLOW_QUOTE_RESPONSE).await;
1133 let client = create_test_hashflow_client(
1134 format!("http://127.0.0.1:{}/rfq", addr.port()),
1135 Duration::from_secs(1),
1136 );
1137 let params = quote_params();
1138
1139 let first_quote = client
1140 .request_binding_quote(¶ms)
1141 .await
1142 .unwrap();
1143 client
1144 .request_binding_quote(¶ms)
1145 .await
1146 .unwrap();
1147
1148 let requests = request_log.lock().unwrap();
1149 assert_eq!(requests.len(), 2);
1150 for body in requests.iter() {
1151 assert!(
1152 body.contains(&format!("\"trader\":\"{}\"", params.receiver)),
1153 "trader is not the receiver: {body}"
1154 );
1155 }
1156 let first = effective_trader_of(&requests[0]);
1157 let second = effective_trader_of(&requests[1]);
1158 assert_eq!(first.len(), 42, "effective trader is not an address");
1159 assert_ne!(first, second, "effective traders are not unique per quote");
1160 assert_ne!(first, params.receiver.to_string(), "effective trader equals the trader");
1161 assert_eq!(
1162 first_quote
1163 .quote_attributes
1164 .get("effective_trader")
1165 .unwrap()
1166 .to_string(),
1167 first,
1168 "quote attributes do not carry the requested effective trader"
1169 );
1170 }
1171
1172 #[tokio::test]
1173 async fn test_request_quote_from_maker() {
1174 let (addr, request_log) = mock_quote_server(0, HASHFLOW_QUOTE_RESPONSE).await;
1175 let client = create_test_hashflow_client(
1176 format!("http://127.0.0.1:{}/rfq", addr.port()),
1177 Duration::from_secs(1),
1178 );
1179
1180 client
1181 .request_quote("e_params(), Some("mm1"))
1182 .await
1183 .unwrap();
1184
1185 let request = &request_log.lock().unwrap()[0];
1186 assert!(request.contains("\"marketMakers\":[\"mm1\"]"), "{request}");
1187 assert!(request.contains("\"doNotRetryWithOtherMakers\":true"), "{request}");
1188 }
1189
1190 #[tokio::test]
1191 async fn test_request_binding_quote_names_no_maker() {
1192 let (addr, request_log) = mock_quote_server(0, HASHFLOW_QUOTE_RESPONSE).await;
1193 let client = create_test_hashflow_client(
1194 format!("http://127.0.0.1:{}/rfq", addr.port()),
1195 Duration::from_secs(1),
1196 );
1197
1198 client
1199 .request_binding_quote("e_params())
1200 .await
1201 .unwrap();
1202
1203 let request = &request_log.lock().unwrap()[0];
1204 assert!(!request.contains("marketMakers"), "{request}");
1205 assert!(!request.contains("options"), "{request}");
1206 }
1207
1208 fn levels(pair: (&Bytes, &Bytes), levels: &[(f64, f64)]) -> HashflowMarketMakerLevels {
1209 HashflowMarketMakerLevels {
1210 pair: HashflowPair { base_token: pair.0.clone(), quote_token: pair.1.clone() },
1211 levels: levels
1212 .iter()
1213 .map(|&(quantity, price)| PriceLevel { quantity, price })
1214 .collect(),
1215 }
1216 }
1217
1218 fn all_pairs_test_client() -> (HashflowClient, Bytes, Bytes, Bytes) {
1220 let weth = Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap();
1221 let wbtc = Bytes::from_str("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599").unwrap();
1222 let usdc = Bytes::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap();
1223 let mut client =
1224 create_test_hashflow_client("http://unused/rfq".to_string(), Duration::from_secs(1));
1225 client.tokens = HashSet::from([weth.clone(), wbtc.clone(), usdc.clone()]);
1226 client.quote_tokens = HashSet::from([usdc.clone()]);
1227 client.tvl = 100.0;
1228 (client, weth, wbtc, usdc)
1229 }
1230
1231 #[test]
1232 fn test_all_pairs_component_keeps_books_above_tvl_threshold() {
1233 let (client, weth, wbtc, usdc) = all_pairs_test_client();
1234 let levels_by_mm = HashMap::from([
1235 (
1236 "mm_b".to_string(),
1237 vec![
1238 levels((&weth, &usdc), &[(1.0, 3000.0)]),
1239 levels((&wbtc, &usdc), &[(0.001, 65.0)]),
1240 ],
1241 ),
1242 ("mm_a".to_string(), vec![levels((&weth, &usdc), &[(2.0, 3000.0)])]),
1243 ]);
1244
1245 let component = client
1246 .all_pairs_component(&levels_by_mm)
1247 .unwrap()
1248 .expect("two makers' price levels clear the threshold");
1249
1250 assert_eq!(
1251 component.component.id,
1252 component::component_id(HashflowClient::PROTOCOL_SYSTEM, client.chain)
1253 );
1254 assert_eq!(
1255 component.component_tvl,
1256 Some(9000.0),
1257 "the WBTC price levels are below the threshold"
1258 );
1259 let mut expected_tokens = vec![weth.clone(), usdc.clone()];
1260 expected_tokens.sort();
1261 assert_eq!(component.component.tokens, expected_tokens);
1262 let mut expected_directions = weth.to_vec();
1263 expected_directions.extend_from_slice(&usdc);
1264 assert_eq!(
1265 component.component.static_attributes[SWAP_DIRECTIONS_ATTRIBUTE].to_vec(),
1266 expected_directions
1267 );
1268 assert_eq!(
1269 component.component.static_attributes[QuoteRule::ATTRIBUTE].as_ref(),
1270 b"once_per_maker"
1271 );
1272
1273 let price_levels: Vec<MakerPriceLevels> =
1274 serde_json::from_slice(&component.state.attributes[BOOKS_ATTRIBUTE]).unwrap();
1275 assert_eq!(price_levels.len(), 2);
1276 assert_eq!(price_levels[0].market_maker, "mm_a", "price levels are sorted by maker");
1277 assert_eq!(price_levels[0].levels[0].quantity, 2.0);
1278 assert_eq!(price_levels[1].market_maker, "mm_b");
1279 assert_eq!(price_levels[1].base_token, weth);
1280 }
1281
1282 #[test]
1284 fn test_pair_components_keep_pairs_above_tvl_threshold() {
1285 let (client, weth, wbtc, usdc) = all_pairs_test_client();
1286 let levels_by_mm = HashMap::from([(
1287 "mm_a".to_string(),
1288 vec![
1289 levels((&weth, &usdc), &[(1.0, 3000.0)]),
1290 levels((&wbtc, &usdc), &[(0.001, 65.0)]),
1291 ],
1292 )]);
1293
1294 let components = client
1295 .pair_components(&levels_by_mm)
1296 .unwrap();
1297
1298 let id = format!(
1299 "{}",
1300 keccak256(format!("hashflow_{}/{}", hex::encode(&weth), hex::encode(&usdc)).as_bytes())
1301 );
1302 assert_eq!(components.keys().collect::<Vec<_>>(), [&id], "the WBTC pair is below TVL");
1303 let component = &components[&id];
1304 assert_eq!(component.component.tokens, [weth, usdc]);
1305 assert_eq!(component.component_tvl, Some(3000.0));
1306 assert!(component
1307 .component
1308 .static_attributes
1309 .is_empty());
1310 assert_eq!(component.state.attributes["mm"].as_ref(), b"mm_a");
1311 let levels: Vec<PriceLevel> =
1312 serde_json::from_slice(&component.state.attributes["levels"]).unwrap();
1313 assert_eq!(levels, [PriceLevel { quantity: 1.0, price: 3000.0 }]);
1314 }
1315
1316 #[tokio::test]
1317 async fn test_hashflow_quote_timeout() {
1318 let (addr, _) = mock_quote_server(500, HASHFLOW_QUOTE_RESPONSE).await;
1319
1320 let client_short_timeout = create_test_hashflow_client(
1322 format!("http://127.0.0.1:{}/rfq", addr.port()),
1323 Duration::from_millis(200),
1324 );
1325 let params = quote_params();
1326
1327 let start = std::time::Instant::now();
1329 let result = client_short_timeout
1330 .request_binding_quote(¶ms)
1331 .await;
1332 let elapsed = start.elapsed();
1333
1334 assert!(result.is_err());
1336 let err = result.unwrap_err();
1337 match err {
1338 RFQError::ConnectionError(msg) => {
1339 assert!(msg.contains("timed out"), "Expected timeout error, got: {}", msg);
1340 }
1341 _ => panic!("Expected ConnectionError, got: {:?}", err),
1342 }
1343 assert!(
1345 elapsed.as_millis() >= 200 && elapsed.as_millis() < 400,
1346 "Expected timeout around 200ms, got: {:?}",
1347 elapsed
1348 );
1349
1350 let client_long_timeout = create_test_hashflow_client(
1354 format!("http://127.0.0.1:{}/rfq", addr.port()),
1355 Duration::from_secs(1),
1356 );
1357
1358 let result = client_long_timeout
1360 .request_binding_quote(¶ms)
1361 .await;
1362
1363 assert!(result.is_ok(), "Expected success, got: {:?}", result);
1365 }
1366
1367 async fn create_retry_server() -> (std::net::SocketAddr, std::sync::Arc<std::sync::Mutex<u32>>)
1369 {
1370 use std::sync::{Arc, Mutex};
1371
1372 use tokio::{io::AsyncWriteExt, net::TcpListener};
1373
1374 let request_count = Arc::new(Mutex::new(0u32));
1375 let request_count_clone = request_count.clone();
1376
1377 let listener = TcpListener::bind("127.0.0.1:0")
1378 .await
1379 .unwrap();
1380 let addr = listener.local_addr().unwrap();
1381
1382 tokio::spawn(async move {
1383 while let Ok((mut stream, _)) = listener.accept().await {
1384 let count_clone = request_count_clone.clone();
1385 tokio::spawn(async move {
1386 *count_clone.lock().unwrap() += 1;
1387 let count = *count_clone.lock().unwrap();
1388 println!("Mock server: Received request #{count}");
1389
1390 let body = read_request_body(&mut stream).await;
1391 if count <= 2 {
1392 let response = "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 21\r\n\r\nInternal Server Error";
1393 let _ = stream
1394 .write_all(response.as_bytes())
1395 .await;
1396 } else {
1397 let json_response = HASHFLOW_QUOTE_RESPONSE
1398 .replace("{{EFFECTIVE_TRADER}}", &effective_trader_of(&body));
1399 let response = format!(
1400 "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
1401 json_response.len(),
1402 json_response
1403 );
1404 let _ = stream
1405 .write_all(response.as_bytes())
1406 .await;
1407 }
1408 let _ = stream.flush().await;
1409 let _ = stream.shutdown().await;
1410 });
1411 }
1412 });
1413
1414 tokio::time::sleep(Duration::from_millis(50)).await;
1415 (addr, request_count)
1416 }
1417
1418 #[tokio::test]
1419 async fn test_hashflow_quote_retry_on_bad_response() {
1420 let (addr, request_count) = create_retry_server().await;
1421
1422 let client = create_test_hashflow_client(
1423 format!("http://127.0.0.1:{}/rfq", addr.port()),
1424 Duration::from_secs(5),
1425 );
1426 let params = quote_params();
1427 let result = client
1428 .request_binding_quote(¶ms)
1429 .await;
1430
1431 assert!(result.is_ok(), "Expected success after retries, got: {:?}", result);
1432 let quote = result.unwrap();
1433
1434 assert_eq!(quote.amount_in, BigUint::from(1_000000000000000000u64));
1436 assert_eq!(quote.amount_out, BigUint::from(3329502u64));
1437
1438 let final_count = *request_count.lock().unwrap();
1440 assert_eq!(final_count, 3, "Expected 3 requests, got {}", final_count);
1441 }
1442
1443 #[test]
1444 fn test_hashflow_client_serialize_deserialize_roundtrip() {
1445 let token_in = Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap();
1446 let token_out = Bytes::from_str("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599").unwrap();
1447 let quote_token = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
1448
1449 let original = HashflowClient {
1450 chain: Chain::Ethereum,
1451 price_levels_endpoint: "https://api.hashflow.com/price_levels".to_string(),
1452 market_makers_endpoint: "https://api.hashflow.com/market_makers".to_string(),
1453 quote_endpoint: "https://api.hashflow.com/quote".to_string(),
1454 tokens: HashSet::from([token_in.clone(), token_out.clone()]),
1455 tvl: 50.5,
1456 auth_key: "secret_key".to_string(),
1457 auth_user: "secret_user".to_string(),
1458 quote_tokens: HashSet::from([quote_token.clone()]),
1459 poll_time: Duration::from_secs(10),
1460 quote_timeout: Duration::from_millis(5500),
1461 protocol_system: HashflowClient::PROTOCOL_SYSTEM.to_string(),
1462 component_layout: ComponentLayout::PerPair,
1463 quote_rule: QuoteRule::OncePerMaker,
1464 };
1465
1466 let serialized = serde_json::to_string(&original).unwrap();
1467 let deserialized: HashflowClient = serde_json::from_str(&serialized).unwrap();
1468
1469 assert_eq!(deserialized.chain, original.chain);
1471 assert_eq!(deserialized.price_levels_endpoint, original.price_levels_endpoint);
1472 assert_eq!(deserialized.market_makers_endpoint, original.market_makers_endpoint);
1473 assert_eq!(deserialized.quote_endpoint, original.quote_endpoint);
1474 assert_eq!(deserialized.tokens, original.tokens);
1475 assert_eq!(deserialized.tvl, original.tvl);
1476 assert_eq!(deserialized.quote_tokens, original.quote_tokens);
1477 assert_eq!(deserialized.poll_time, original.poll_time);
1478 assert_eq!(deserialized.quote_timeout, original.quote_timeout);
1479
1480 assert_eq!(deserialized.auth_key, "");
1482 assert_eq!(deserialized.auth_user, "");
1483 assert_ne!(deserialized.auth_key, original.auth_key);
1484 assert_ne!(deserialized.auth_user, original.auth_user);
1485 }
1486
1487 #[test]
1488 fn test_hashflow_client_deserialize_with_credentials() {
1489 let json = r#"{
1492 "chain": "ethereum",
1493 "price_levels_endpoint": "https://api.hashflow.com/price_levels",
1494 "market_makers_endpoint": "https://api.hashflow.com/market_makers",
1495 "quote_endpoint": "https://api.hashflow.com/quote",
1496 "tokens": [],
1497 "tvl": 10.0,
1498 "auth_key": "provided_key",
1499 "auth_user": "provided_user",
1500 "quote_tokens": [],
1501 "poll_time": {"secs": 10, "nanos": 0},
1502 "quote_timeout": {"secs": 30, "nanos": 0}
1503 }"#;
1504
1505 let client: HashflowClient = serde_json::from_str(json).unwrap();
1506
1507 assert_eq!(client.auth_key, "provided_key");
1509 assert_eq!(client.auth_user, "provided_user");
1510 }
1511}