1use std::{
2 collections::{HashMap, HashSet},
3 str::FromStr,
4 time::SystemTime,
5};
6
7use alloy::primitives::{utils::keccak256, Address, U256};
8use async_trait::async_trait;
9use futures::stream::BoxStream;
10use num_bigint::BigUint;
11use reqwest::Client;
12use serde::{Deserialize, Serialize};
13use tokio::time::{interval, timeout, Duration};
14use tracing::{error, info, warn};
15use tycho_common::{
16 models::{protocol::GetAmountOutParams, Chain},
17 simulation::indicatively_priced::SignedQuote,
18 Bytes,
19};
20
21use crate::{
22 evm::protocol::u256_num::biguint_to_u256,
23 rfq::{
24 client::RFQClient,
25 errors::RFQError,
26 models::TimestampHeader,
27 protocols::hashflow::models::{
28 HashflowChain, HashflowMarketMakerLevels, HashflowMarketMakersResponse,
29 HashflowPriceLevelsResponse, HashflowQuoteRequest, HashflowQuoteResponse, HashflowRFQ,
30 },
31 },
32 tycho_client::feed::synchronizer::{ComponentWithState, Snapshot, StateSyncMessage},
33 tycho_common::models::protocol::{ProtocolComponent, ProtocolComponentState},
34};
35
36#[derive(Clone, Debug, Serialize, Deserialize)]
37pub struct HashflowClient {
38 chain: Chain,
39 price_levels_endpoint: String,
40 market_makers_endpoint: String,
41 quote_endpoint: String,
42 tokens: HashSet<Bytes>,
44 tvl: f64,
46 #[serde(skip_serializing, default)]
47 auth_key: String,
48 #[serde(skip_serializing, default)]
49 auth_user: String,
50 quote_tokens: HashSet<Bytes>,
52 poll_time: Duration,
53 quote_timeout: Duration,
54}
55
56impl HashflowClient {
57 pub const PROTOCOL_SYSTEM: &'static str = "rfq:hashflow";
58
59 #[allow(clippy::too_many_arguments)]
60 pub fn new(
61 chain: Chain,
62 tokens: HashSet<Bytes>,
63 tvl: f64,
64 quote_tokens: HashSet<Bytes>,
65 auth_user: String,
66 auth_key: String,
67 poll_time: Duration,
68 quote_timeout: Duration,
69 ) -> Result<Self, RFQError> {
70 Ok(Self {
71 chain,
72 price_levels_endpoint: "https://api.hashflow.com/taker/v3/price-levels".to_string(),
73 market_makers_endpoint: "https://api.hashflow.com/taker/v3/market-makers".to_string(),
74 quote_endpoint: "https://api.hashflow.com/taker/v3/rfq".to_string(),
75 tokens,
76 tvl,
77 auth_key,
78 auth_user,
79 quote_tokens,
80 poll_time,
81 quote_timeout,
82 })
83 }
84
85 fn normalize_tvl(
88 &self,
89 raw_tvl: f64,
90 quote_token: Bytes,
91 levels_by_mm: &HashMap<String, Vec<HashflowMarketMakerLevels>>,
92 ) -> Result<f64, RFQError> {
93 if self.quote_tokens.contains("e_token) {
95 return Ok(raw_tvl);
96 }
97
98 for approved_quote_token in &self.quote_tokens {
101 for mm_levels_inner in levels_by_mm.values() {
102 for quote_mm_level in mm_levels_inner {
103 if quote_mm_level.pair.base_token == quote_token &&
105 quote_mm_level.pair.quote_token == *approved_quote_token
106 {
107 if let Some(price) = quote_mm_level.get_price(1.0) {
108 return Ok(raw_tvl * price);
109 }
110 }
111 }
112 }
113 }
114
115 Ok(0.0)
117 }
118
119 fn create_component_with_state(
120 &self,
121 component_id: String,
122 tokens: Vec<Bytes>,
123 mm_name: &str,
124 mm_level: &HashflowMarketMakerLevels,
125 tvl: f64,
126 ) -> ComponentWithState {
127 let protocol_component = ProtocolComponent {
128 id: component_id.clone(),
129 protocol_system: Self::PROTOCOL_SYSTEM.to_string(),
130 protocol_type_name: "hashflow_pool".to_string(),
131 chain: self.chain,
132 tokens,
133 contract_addresses: vec![], ..Default::default()
135 };
136
137 let mut attributes = HashMap::new();
138
139 if !mm_level.levels.is_empty() {
141 let levels_json = serde_json::to_string(&mm_level.levels).unwrap_or_default();
142 attributes.insert("levels".to_string(), levels_json.as_bytes().to_vec().into());
143 }
144 attributes.insert("mm".to_string(), mm_name.as_bytes().to_vec().into());
145
146 ComponentWithState {
147 state: ProtocolComponentState::new(&component_id, attributes, HashMap::new()),
148 component: protocol_component,
149 component_tvl: Some(tvl),
150 entrypoints: vec![],
151 }
152 }
153
154 async fn fetch_market_makers(&mut self) -> Result<Vec<String>, RFQError> {
155 let query_params = vec![
156 ("source", self.auth_user.clone()),
157 ("baseChainType", "evm".to_string()),
158 ("baseChainId", self.chain.id().to_string()),
159 ];
160
161 let http_client = Client::new();
162 let request = http_client
163 .get(&self.market_makers_endpoint)
164 .query(&query_params)
165 .header("accept", "application/json")
166 .header("Authorization", &self.auth_key);
167
168 let response = request.send().await.map_err(|e| {
169 RFQError::ConnectionError(format!("Failed to fetch market makers: {e}"))
170 })?;
171
172 if !response.status().is_success() {
173 return Err(RFQError::ConnectionError(format!(
174 "HTTP error {}: {}",
175 response.status(),
176 response
177 .text()
178 .await
179 .unwrap_or_default()
180 )));
181 }
182
183 let mm_response: HashflowMarketMakersResponse = response.json().await.map_err(|e| {
184 RFQError::ParsingError(format!("Failed to parse market makers response: {e}"))
185 })?;
186
187 info!(
188 "Fetched {} market makers: {:?}",
189 mm_response.market_makers.len(),
190 mm_response.market_makers
191 );
192
193 Ok(mm_response.market_makers)
194 }
195
196 async fn fetch_price_levels(
197 &self,
198 market_makers: &Vec<String>,
199 ) -> Result<HashMap<String, Vec<HashflowMarketMakerLevels>>, RFQError> {
200 let mut query_params = vec![
201 ("source", self.auth_user.clone()),
202 ("baseChainType", "evm".to_string()),
203 ("baseChainId", self.chain.id().to_string()),
204 ];
205
206 for mm in market_makers {
208 query_params.push(("marketMakers[]", mm.clone()));
209 }
210
211 let http_client = Client::new();
212 let request = http_client
213 .get(&self.price_levels_endpoint)
214 .query(&query_params)
215 .header("accept", "application/json")
216 .header("Authorization", &self.auth_key);
217
218 let response = request
219 .send()
220 .await
221 .map_err(|e| RFQError::ConnectionError(format!("Failed to fetch price levels: {e}")))?;
222
223 if !response.status().is_success() {
224 return Err(RFQError::ConnectionError(format!(
225 "HTTP error {}: {}",
226 response.status(),
227 response
228 .text()
229 .await
230 .unwrap_or_default()
231 )));
232 }
233
234 let price_response: HashflowPriceLevelsResponse = response.json().await.map_err(|e| {
235 RFQError::ParsingError(format!("Failed to parse price levels response: {e}"))
236 })?;
237
238 if price_response.status != "success" {
239 let error = match price_response.error {
240 Some(error) => error.to_string(),
241 None => "no error details".to_string(),
242 };
243 return Err(RFQError::InvalidInput(format!("API returned error status: {error}")));
244 }
245
246 price_response
247 .levels
248 .ok_or_else(|| RFQError::ParsingError("API response missing levels".to_string()))
249 }
250}
251
252#[async_trait]
253impl RFQClient for HashflowClient {
254 fn stream(
255 &self,
256 ) -> BoxStream<'static, Result<(String, StateSyncMessage<TimestampHeader>), RFQError>> {
257 let mut client = self.clone();
258
259 Box::pin(async_stream::stream! {
260 let mut current_components: HashMap<String, ComponentWithState> = HashMap::new();
261 let mut ticker = interval(client.poll_time);
262
263 info!("Starting Hashflow price levels polling every {} seconds", client.poll_time.as_secs());
264 info!("TVL threshold: {:.2}", client.tvl);
265
266 loop {
267 ticker.tick().await;
268
269 let market_makers;
270 match client.fetch_market_makers().await {
271 Ok(mms) => {
272 market_makers = mms;
273 info!("Successfully fetched market makers");
274 }
275 Err(e) => {
276 info!("Failed to fetch market makers: {}", e);
277 continue;
278 }
279 }
280
281 match client.fetch_price_levels(&market_makers).await {
282 Ok(levels_by_mm) => {
283 let mut new_components = HashMap::new();
284
285 info!("Fetched price levels from {} market makers", levels_by_mm.len());
286 for (mm_name, mm_levels) in levels_by_mm.iter() {
288 for mm_level in mm_levels {
289 let base_token = &mm_level.pair.base_token;
290 let quote_token = &mm_level.pair.quote_token;
291
292 if client.tokens.contains(base_token) && client.tokens.contains(quote_token) {
294 let tokens = vec![base_token.clone(), quote_token.clone()];
295 let tvl = mm_level.calculate_tvl();
296
297 let normalized_tvl = client.normalize_tvl(
299 tvl,
300 mm_level.pair.quote_token.clone(),
301 &levels_by_mm,
302 )?;
303
304 let pair_str = format!("hashflow_{}/{}", hex::encode(base_token), hex::encode(quote_token));
306 let component_id = format!("{}", keccak256(pair_str.as_bytes()));
307
308 if normalized_tvl < client.tvl {
309 info!("Filtering out component {} due to low TVL: {:.2} < {:.2}",
310 component_id, normalized_tvl, client.tvl);
311 continue;
312 }
313
314 let component_with_state = client.create_component_with_state(
315 component_id.clone(),
316 tokens,
317 mm_name,
318 mm_level,
319 normalized_tvl
320 );
321 new_components.insert(component_id, component_with_state);
322 }
323 }
324 }
325
326 let removed_components: HashMap<String, ProtocolComponent> = current_components
328 .iter()
329 .filter(|&(id, _)| !new_components.contains_key(id))
330 .map(|(k, v)| (k.clone(), v.component.clone()))
331 .collect();
332
333 current_components = new_components.clone();
335
336 let snapshot = Snapshot {
337 states: new_components,
338 vm_storage: HashMap::new(),
339 };
340 let timestamp = SystemTime::now().duration_since(
341 SystemTime::UNIX_EPOCH
342 ).map_err(
343 |_| RFQError::ParsingError("SystemTime before UNIX EPOCH!".into())
344 )?.as_secs();
345
346 let msg = StateSyncMessage::<TimestampHeader> {
347 header: TimestampHeader { timestamp },
348 snapshots: snapshot,
349 deltas: None,
350 removed_components,
351 };
352
353 yield Ok(("hashflow".to_string(), msg));
354 },
355 Err(e) => {
356 error!("Failed to fetch price levels from Hashflow API: {}", e);
357 continue;
358 }
359 }
360 }
361 })
362 }
363
364 async fn request_binding_quote(
365 &self,
366 params: &GetAmountOutParams,
367 ) -> Result<SignedQuote, RFQError> {
368 let hashflow_chain = HashflowChain::from(self.chain);
369 let effective_trader = Bytes::from(Address::random().to_vec());
374 let quote_request = HashflowQuoteRequest {
375 source: self.auth_user.clone(),
376 base_chain: hashflow_chain.clone(),
377 quote_chain: hashflow_chain,
378 rfqs: vec![HashflowRFQ {
379 base_token: params.token_in.to_string(),
380 quote_token: params.token_out.to_string(),
381 base_token_amount: Some(params.amount_in.to_string()),
382 quote_token_amount: None,
383 trader: params.receiver.to_string(),
384 effective_trader: Some(effective_trader.to_string()),
385 }],
386 calldata: false,
387 };
388
389 let url = self.quote_endpoint.clone();
390
391 let start_time = std::time::Instant::now();
392 const MAX_RETRIES: u32 = 3;
393 let mut last_error = None;
394
395 for attempt in 0..MAX_RETRIES {
396 let elapsed = start_time.elapsed();
398 if elapsed >= self.quote_timeout {
399 return Err(last_error.unwrap_or_else(|| {
400 RFQError::ConnectionError(format!(
401 "Hashflow quote request timed out after {} seconds",
402 self.quote_timeout.as_secs()
403 ))
404 }));
405 }
406
407 let remaining_time = self.quote_timeout - elapsed;
408
409 let http_client = Client::new();
410 let request = http_client
411 .post(&url)
412 .json("e_request)
413 .header("accept", "application/json")
414 .header("Authorization", &self.auth_key);
415
416 let response = match timeout(remaining_time, request.send()).await {
417 Ok(Ok(resp)) => resp,
418 Ok(Err(e)) => {
419 warn!(
420 "Hashflow quote request failed (attempt {}/{}): {}",
421 attempt + 1,
422 MAX_RETRIES,
423 e
424 );
425 last_error = Some(RFQError::ConnectionError(format!(
426 "Failed to send Hashflow quote request: {e}"
427 )));
428 if attempt < MAX_RETRIES - 1 {
429 tokio::time::sleep(Duration::from_millis(100)).await;
430 continue;
431 } else {
432 return Err(last_error.unwrap());
433 }
434 }
435 Err(_) => {
436 return Err(RFQError::ConnectionError(format!(
437 "Hashflow quote request timed out after {} seconds",
438 self.quote_timeout.as_secs()
439 )));
440 }
441 };
442
443 if response.status() != 200 {
444 let err_msg = match response.text().await {
445 Ok(text) => text,
446 Err(e) => {
447 warn!(
448 "Hashflow error response parsing failed (attempt {}/{}): {}",
449 attempt + 1,
450 MAX_RETRIES,
451 e
452 );
453 last_error = Some(RFQError::ParsingError(format!(
454 "Failed to read response text from Hashflow failed request: {e}"
455 )));
456 if attempt < MAX_RETRIES - 1 {
457 tokio::time::sleep(Duration::from_millis(100)).await;
458 continue;
459 } else {
460 return Err(last_error.unwrap());
461 }
462 }
463 };
464 last_error = Some(RFQError::FatalError(format!(
465 "Failed to send Hashflow quote request: {err_msg}",
466 )));
467 if attempt < MAX_RETRIES - 1 {
468 warn!(
469 "Hashflow returned non-200 status (attempt {}/{}): {}",
470 attempt + 1,
471 MAX_RETRIES,
472 err_msg
473 );
474 tokio::time::sleep(Duration::from_millis(100)).await;
475 continue;
476 } else {
477 return Err(last_error.unwrap());
478 }
479 }
480
481 let quote_response = match response
482 .json::<HashflowQuoteResponse>()
483 .await
484 {
485 Ok(resp) => resp,
486 Err(e) => {
487 warn!(
488 "Hashflow quote response parsing failed (attempt {}/{}): {}",
489 attempt + 1,
490 MAX_RETRIES,
491 e
492 );
493 last_error = Some(RFQError::ParsingError(format!(
494 "Failed to parse Hashflow quote response: {e}"
495 )));
496 if attempt < MAX_RETRIES - 1 {
497 tokio::time::sleep(Duration::from_millis(100)).await;
498 continue;
499 } else {
500 return Err(last_error.unwrap());
501 }
502 }
503 };
504
505 match quote_response.status.as_str() {
506 "success" => {
507 if let Some(quotes) = quote_response.quotes {
508 if quotes.is_empty() {
509 return Err(RFQError::QuoteNotFound(format!(
510 "Hashflow quote not found for {} {} ->{}",
511 params.amount_in, params.token_in, params.token_out,
512 )));
513 }
514 let quote = quotes[0].clone();
516 quote.validate(params, &effective_trader)?;
517
518 let mut quote_attributes: HashMap<String, Bytes> = HashMap::new();
519 quote_attributes.insert("pool".to_string(), quote.quote_data.pool);
520 if let Some(external_account) = quote.quote_data.external_account {
521 quote_attributes
522 .insert("external_account".to_string(), external_account);
523 } else {
524 quote_attributes.insert(
525 "external_account".to_string(),
526 Bytes::from_str(&Address::ZERO.to_string()).map_err(|_| {
527 RFQError::ParsingError(
528 "Failed to parse zero address".to_string(),
529 )
530 })?,
531 );
532 }
533 quote_attributes.insert("trader".to_string(), quote.quote_data.trader);
534 quote_attributes
535 .insert("effective_trader".to_string(), effective_trader.clone());
536 quote_attributes
537 .insert("base_token".to_string(), quote.quote_data.base_token);
538 quote_attributes
539 .insert("quote_token".to_string(), quote.quote_data.quote_token);
540 quote_attributes.insert(
541 "base_token_amount".to_string(),
542 Bytes::from(
543 biguint_to_u256(
544 &BigUint::from_str("e.quote_data.base_token_amount)
545 .map_err(|_| {
546 RFQError::ParsingError(format!(
547 "Failed to parse base token amount: {}",
548 quote.quote_data.base_token_amount
549 ))
550 })?,
551 )
552 .to_be_bytes::<32>()
553 .to_vec(),
554 ),
555 );
556 quote_attributes.insert(
557 "quote_token_amount".to_string(),
558 Bytes::from(
559 biguint_to_u256(
560 &BigUint::from_str("e.quote_data.quote_token_amount)
561 .map_err(|_| {
562 RFQError::ParsingError(format!(
563 "Failed to parse quote token amount: {}",
564 quote.quote_data.quote_token_amount
565 ))
566 })?,
567 )
568 .to_be_bytes::<32>()
569 .to_vec(),
570 ),
571 );
572 quote_attributes.insert(
573 "quote_expiry".to_string(),
574 Bytes::from(
575 U256::from(quote.quote_data.quote_expiry)
576 .to_be_bytes::<32>()
577 .to_vec(),
578 ),
579 );
580 quote_attributes.insert(
581 "nonce".to_string(),
582 Bytes::from(
583 U256::from(quote.quote_data.nonce)
584 .to_be_bytes::<32>()
585 .to_vec(),
586 ),
587 );
588 quote_attributes.insert("tx_id".to_string(), quote.quote_data.tx_id);
589 quote_attributes.insert("signature".to_string(), quote.signature);
590
591 let signed_quote = SignedQuote {
592 base_token: params.token_in.clone(),
593 quote_token: params.token_out.clone(),
594 amount_in: BigUint::from_str("e.quote_data.base_token_amount)
595 .map_err(|_| {
596 RFQError::ParsingError(format!(
597 "Failed to parse amount in string: {}",
598 quote.quote_data.base_token_amount
599 ))
600 })?,
601 amount_out: BigUint::from_str("e.quote_data.quote_token_amount)
602 .map_err(|_| {
603 RFQError::ParsingError(format!(
604 "Failed to parse amount out string: {}",
605 quote.quote_data.quote_token_amount
606 ))
607 })?,
608 quote_attributes,
609 };
610 return Ok(signed_quote);
611 } else {
612 return Err(RFQError::QuoteNotFound(format!(
613 "Hashflow quote not found for {} {} ->{}",
614 params.amount_in, params.token_in, params.token_out,
615 )));
616 }
617 }
618 "fail" => {
619 let Some(error) = quote_response.error else {
620 return Err(RFQError::FatalError(
621 "Hashflow API error: request failed without an error".to_string(),
622 ));
623 };
624 return Err(RFQError::FatalError(format!("Hashflow API error: {error}")));
625 }
626 _ => {
627 return Err(RFQError::FatalError(
628 "Hashflow API error: Unknown status".to_string(),
629 ));
630 }
631 }
632 }
633
634 Err(last_error.unwrap_or_else(|| {
635 RFQError::ConnectionError("Hashflow quote request failed after retries".to_string())
636 }))
637 }
638}
639
640#[cfg(test)]
641mod tests {
642 use std::{env, str::FromStr, time::Duration};
643
644 use dotenv::dotenv;
645 use futures::StreamExt;
646 use tokio::time::timeout;
647
648 use super::*;
649 use crate::rfq::{
650 constants::get_hashflow_auth,
651 protocols::hashflow::models::{HashflowPair, HashflowPriceLevel},
652 };
653
654 #[test]
655 fn test_normalize_tvl_same_quote_token() {
656 let client = create_test_client();
657 let levels = HashMap::new();
658
659 let result = client.normalize_tvl(
661 1000.0,
662 Bytes::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap(),
663 &levels,
664 );
665 assert!(result.is_ok());
666 assert_eq!(result.unwrap(), 1000.0);
667 }
668
669 #[test]
670 fn test_normalize_tvl_different_quote_token() {
671 let client = create_test_client();
672 let mut levels = HashMap::new();
673 let weth = Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap();
674 let usdc = Bytes::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap();
675
676 let eth_usdc_level = HashflowMarketMakerLevels {
678 pair: HashflowPair { base_token: weth.clone(), quote_token: usdc },
679 levels: vec![
680 HashflowPriceLevel { quantity: 1.0, price: 3000.0 }, ],
682 };
683
684 levels.insert("test_mm".to_string(), vec![eth_usdc_level]);
685
686 let result = client.normalize_tvl(2.0, weth, &levels);
688 assert!(result.is_ok());
689 assert_eq!(result.unwrap(), 6000.0);
691 }
692
693 #[test]
694 fn test_normalize_tvl_no_conversion_available() {
695 let client = create_test_client();
696 let levels = HashMap::new();
697 let result = client.normalize_tvl(
698 1000.0,
699 Bytes::from_str("0x1234567890123456789012345678901234567890").unwrap(),
700 &levels,
701 );
702 assert!(result.is_ok());
703 assert_eq!(result.unwrap(), 0.0);
704 }
705
706 fn create_test_client() -> HashflowClient {
707 let quote_tokens = HashSet::from([
708 Bytes::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap(), Bytes::from_str("0xdAC17F958D2ee523a2206206994597C13D831ec7").unwrap(), ]);
711
712 HashflowClient::new(
713 Chain::Ethereum,
714 HashSet::new(),
715 1.0,
716 quote_tokens,
717 "test_user".to_string(),
718 "test_key".to_string(),
719 Duration::from_secs(5),
720 Duration::from_secs(5),
721 )
722 .unwrap()
723 }
724
725 #[tokio::test]
726 #[ignore] async fn test_hashflow_api_polling() {
728 dotenv().expect("Missing .env file");
729 let auth = get_hashflow_auth().unwrap();
730
731 let wbtc = Bytes::from_str("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599").unwrap();
732 let weth = Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap();
733
734 let tokens = HashSet::from([wbtc, weth.clone()]);
735
736 let quote_tokens = HashSet::from([
737 Bytes::from_str("0xa0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap(), Bytes::from_str("0xdac17f958d2ee523a2206206994597c13d831ec7").unwrap(), ]);
740
741 let client = HashflowClient::new(
742 Chain::Ethereum,
743 tokens,
744 1.0, quote_tokens,
746 auth.user,
747 auth.key,
748 Duration::from_secs(1),
749 Duration::from_secs(5),
750 )
751 .unwrap();
752
753 let mut stream = client.stream();
754
755 let result = timeout(Duration::from_secs(10), async {
756 let mut message_count = 0;
757 let max_messages = 3;
758 let mut total_components_received = 0;
759
760 while let Some(result) = stream.next().await {
761 match result {
762 Ok((component_id, msg)) => {
763 println!("Received message with ID: {component_id}");
764
765 assert!(!component_id.is_empty());
766 assert_eq!(component_id, "hashflow");
767 assert!(msg.header.timestamp > 0);
768
769 let snapshot = &msg.snapshots;
770 total_components_received += snapshot.states.len();
771
772 println!("Received {} components in this message (Total so far: {})",
773 snapshot.states.len(), total_components_received);
774
775 for (id, component_with_state) in &snapshot.states {
776 let attributes = &component_with_state.state.attributes;
777 let levels: &Bytes = attributes.get("levels").unwrap();
778 if attributes.contains_key("levels") {
780 println!("{levels:?}");
781 assert!(!attributes["levels"].is_empty());
782 }
783 if attributes.contains_key("mm") {
785 assert!(!attributes["mm"].is_empty());
786 }
787
788 if let Some(tvl) = component_with_state.component_tvl {
789 assert!(tvl >= 1.0);
790 println!("Component {id} TVL: ${tvl:.2}");
791 }
792 }
793
794 message_count += 1;
795 if message_count >= max_messages {
796 break;
797 }
798 }
799 Err(e) => {
800 panic!("Stream error: {e}");
801 }
802 }
803 }
804
805 assert!(message_count > 0, "Should have received at least one message");
806 assert!(total_components_received >= 1, "Should have received at least 1 component with $1 TVL threshold");
807 println!("Successfully received {message_count} messages with {total_components_received} total components");
808 })
809 .await;
810
811 match result {
812 Ok(_) => println!("Test completed successfully"),
813 Err(_) => panic!("Test timed out - no messages received within 5 seconds"),
814 }
815 }
816
817 #[tokio::test]
818 #[ignore] async fn test_request_binding_quote() {
820 let wbtc = Bytes::from_str("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599").unwrap();
821 let weth = Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap();
822
823 let auth_user = String::from("propellerheads");
824 dotenv().expect("Missing .env file");
825 let auth_key = env::var("HASHFLOW_KEY").unwrap();
826
827 let client = HashflowClient::new(
828 Chain::Ethereum,
829 HashSet::from_iter(vec![weth.clone(), wbtc.clone()]),
830 10.0,
831 HashSet::new(),
832 auth_user,
833 auth_key,
834 Duration::from_secs(0),
835 Duration::from_secs(5),
836 )
837 .unwrap();
838
839 let router = Bytes::from_str("0xfD0b31d2E955fA55e3fa641Fe90e08b677188d35").unwrap();
840
841 let params = GetAmountOutParams {
842 amount_in: BigUint::from(1_000000000000000000u64),
843 token_in: weth.clone(),
844 token_out: wbtc.clone(),
845 sender: router.clone(),
846 receiver: router.clone(),
847 };
848 let quote = client
849 .request_binding_quote(¶ms)
850 .await
851 .unwrap();
852
853 assert_eq!(quote.base_token, weth);
854 assert_eq!(quote.quote_token, wbtc);
855 assert_eq!(quote.amount_in, BigUint::from(1_000000000000000000u64));
856
857 assert!(quote.amount_out > BigUint::from(3000000u64));
859
860 assert_eq!(quote.quote_attributes.len(), 12);
861 let expected_attributes = [
862 "pool",
863 "external_account",
864 "trader",
865 "effective_trader",
866 "base_token",
867 "quote_token",
868 "base_token_amount",
869 "quote_token_amount",
870 "quote_expiry",
871 "nonce",
872 "tx_id",
873 "signature",
874 ];
875 for attr in expected_attributes {
876 assert!(
877 quote
878 .quote_attributes
879 .contains_key(attr),
880 "Missing attribute: {attr}"
881 );
882 }
883 assert_eq!(
884 quote
885 .quote_attributes
886 .get("trader")
887 .unwrap(),
888 &router
889 );
890 }
891
892 const QUOTE_RESPONSE: &str = r#"{"status":"success","error":null,"rfqId":"test-rfq-id","internalRfqIds":null,"quotes":[{"quoteData":{"pool":"0x71D9750ECF0c5081FAE4E3EDC4253E52024b0B59","externalAccount":null,"trader":"0xfD0b31d2E955fA55e3fa641Fe90e08b677188d35","effectiveTrader":"{{EFFECTIVE_TRADER}}","baseToken":"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2","baseTokenAmount":"1000000000000000000","quoteToken":"0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599","quoteTokenAmount":"3329502","quoteExpiry":1707847360,"nonce":1707844960943648659,"txid":"0x0000000000000000000000000000000000000000000000000000000000000001"},"signature":"0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12"}]}"#;
895
896 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"}]}"#;
897
898 async fn read_request_body(stream: &mut tokio::net::TcpStream) -> String {
900 use tokio::io::AsyncReadExt;
901
902 let mut raw = Vec::new();
903 let mut buf = [0u8; 1024];
904 loop {
905 let n = stream.read(&mut buf).await.unwrap();
906 raw.extend_from_slice(&buf[..n]);
907 let text = String::from_utf8_lossy(&raw);
908 if let Some(header_end) = text.find("\r\n\r\n") {
909 let content_length: usize = text
910 .lines()
911 .find_map(|line| {
912 line.to_ascii_lowercase()
913 .strip_prefix("content-length:")
914 .map(|v| v.trim().parse().unwrap())
915 })
916 .unwrap_or(0);
917 if raw.len() >= header_end + 4 + content_length {
918 return text[header_end + 4..].to_string();
919 }
920 }
921 if n == 0 {
922 return String::new();
923 }
924 }
925 }
926
927 fn effective_trader_of(request_body: &str) -> String {
929 let start = request_body
930 .find("\"effectiveTrader\":\"")
931 .expect("request carries no effectiveTrader") +
932 "\"effectiveTrader\":\"".len();
933 request_body[start..start + request_body[start..].find('"').unwrap()].to_string()
934 }
935
936 async fn create_delayed_response_server(
940 delay_ms: u64,
941 json_response: &'static str,
942 ) -> (std::net::SocketAddr, std::sync::Arc<std::sync::Mutex<Vec<String>>>) {
943 use std::sync::{Arc, Mutex};
944
945 use tokio::{io::AsyncWriteExt, net::TcpListener};
946
947 let listener = TcpListener::bind("127.0.0.1:0")
948 .await
949 .unwrap();
950 let addr = listener.local_addr().unwrap();
951 let request_log: Arc<Mutex<Vec<String>>> = Arc::default();
952 let request_log_server = request_log.clone();
953
954 tokio::spawn(async move {
955 while let Ok((mut stream, _)) = listener.accept().await {
956 let json_response_clone = json_response.to_owned();
957 let request_log = request_log_server.clone();
958 tokio::spawn(async move {
959 let body = read_request_body(&mut stream).await;
960 let json_response_clone = json_response_clone
961 .replace("{{EFFECTIVE_TRADER}}", &effective_trader_of(&body));
962 request_log.lock().unwrap().push(body);
963 tokio::time::sleep(Duration::from_millis(delay_ms)).await;
964 let response = format!(
965 "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
966 json_response_clone.len(),
967 json_response_clone
968 );
969 let _ = stream
970 .write_all(response.as_bytes())
971 .await;
972 let _ = stream.flush().await;
973 let _ = stream.shutdown().await;
974 });
975 }
976 });
977
978 tokio::time::sleep(Duration::from_millis(50)).await;
979 (addr, request_log)
980 }
981
982 fn create_test_hashflow_client(
983 quote_endpoint: String,
984 quote_timeout: Duration,
985 ) -> HashflowClient {
986 let token_in = Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap();
987 let token_out = Bytes::from_str("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599").unwrap();
988
989 HashflowClient {
990 chain: Chain::Ethereum,
991 price_levels_endpoint: "http://unused/price-levels".to_string(),
992 market_makers_endpoint: "http://unused/market-makers".to_string(),
993 quote_endpoint,
994 tokens: HashSet::from([token_in, token_out]),
995 tvl: 10.0,
996 auth_key: "test_key".to_string(),
997 auth_user: "test_user".to_string(),
998 quote_tokens: HashSet::new(),
999 poll_time: Duration::from_secs(0),
1000 quote_timeout,
1001 }
1002 }
1003
1004 fn create_test_quote_params() -> GetAmountOutParams {
1006 let token_in = Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap();
1007 let token_out = Bytes::from_str("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599").unwrap();
1008 let router = Bytes::from_str("0xfD0b31d2E955fA55e3fa641Fe90e08b677188d35").unwrap();
1009
1010 GetAmountOutParams {
1011 amount_in: BigUint::from(1_000000000000000000u64),
1012 token_in,
1013 token_out,
1014 sender: router.clone(),
1015 receiver: router,
1016 }
1017 }
1018
1019 #[tokio::test]
1020 async fn test_request_binding_quote_without_effective_trader() {
1021 let (addr, _) =
1024 create_delayed_response_server(0, QUOTE_RESPONSE_WITHOUT_EFFECTIVE_TRADER).await;
1025 let client = create_test_hashflow_client(
1026 format!("http://127.0.0.1:{}/rfq", addr.port()),
1027 Duration::from_secs(1),
1028 );
1029 let params = create_test_quote_params();
1030
1031 let err = client
1032 .request_binding_quote(¶ms)
1033 .await
1034 .unwrap_err();
1035
1036 assert!(format!("{err:?}").contains("Effective trader mismatch"));
1037 }
1038
1039 #[tokio::test]
1040 async fn test_request_binding_quote_field_mapping() {
1041 let (addr, request_log) = create_delayed_response_server(0, QUOTE_RESPONSE).await;
1044 let client = create_test_hashflow_client(
1045 format!("http://127.0.0.1:{}/rfq", addr.port()),
1046 Duration::from_secs(1),
1047 );
1048 let params = create_test_quote_params();
1049
1050 let first_quote = client
1051 .request_binding_quote(¶ms)
1052 .await
1053 .unwrap();
1054 client
1055 .request_binding_quote(¶ms)
1056 .await
1057 .unwrap();
1058
1059 let requests = request_log.lock().unwrap();
1060 assert_eq!(requests.len(), 2);
1061 for body in requests.iter() {
1062 assert!(
1063 body.contains(&format!("\"trader\":\"{}\"", params.receiver)),
1064 "trader is not the receiver: {body}"
1065 );
1066 }
1067 let first = effective_trader_of(&requests[0]);
1068 let second = effective_trader_of(&requests[1]);
1069 assert_eq!(first.len(), 42, "effective trader is not an address");
1070 assert_ne!(first, second, "effective traders are not unique per quote");
1071 assert_ne!(first, params.receiver.to_string(), "effective trader equals the trader");
1072 assert_eq!(
1073 first_quote
1074 .quote_attributes
1075 .get("effective_trader")
1076 .unwrap()
1077 .to_string(),
1078 first,
1079 "quote attributes do not carry the requested effective trader"
1080 );
1081 }
1082
1083 #[tokio::test]
1084 async fn test_hashflow_quote_timeout() {
1085 let (addr, _) = create_delayed_response_server(500, QUOTE_RESPONSE).await;
1086
1087 let client_short_timeout = create_test_hashflow_client(
1089 format!("http://127.0.0.1:{}/rfq", addr.port()),
1090 Duration::from_millis(200),
1091 );
1092 let params = create_test_quote_params();
1093
1094 let start = std::time::Instant::now();
1096 let result = client_short_timeout
1097 .request_binding_quote(¶ms)
1098 .await;
1099 let elapsed = start.elapsed();
1100
1101 assert!(result.is_err());
1103 let err = result.unwrap_err();
1104 match err {
1105 RFQError::ConnectionError(msg) => {
1106 assert!(msg.contains("timed out"), "Expected timeout error, got: {}", msg);
1107 }
1108 _ => panic!("Expected ConnectionError, got: {:?}", err),
1109 }
1110 assert!(
1112 elapsed.as_millis() >= 200 && elapsed.as_millis() < 400,
1113 "Expected timeout around 200ms, got: {:?}",
1114 elapsed
1115 );
1116
1117 let client_long_timeout = create_test_hashflow_client(
1121 format!("http://127.0.0.1:{}/rfq", addr.port()),
1122 Duration::from_secs(1),
1123 );
1124
1125 let result = client_long_timeout
1127 .request_binding_quote(¶ms)
1128 .await;
1129
1130 assert!(result.is_ok(), "Expected success, got: {:?}", result);
1132 }
1133
1134 async fn create_retry_server() -> (std::net::SocketAddr, std::sync::Arc<std::sync::Mutex<u32>>)
1136 {
1137 use std::sync::{Arc, Mutex};
1138
1139 use tokio::{io::AsyncWriteExt, net::TcpListener};
1140
1141 let request_count = Arc::new(Mutex::new(0u32));
1142 let request_count_clone = request_count.clone();
1143
1144 let listener = TcpListener::bind("127.0.0.1:0")
1145 .await
1146 .unwrap();
1147 let addr = listener.local_addr().unwrap();
1148
1149 tokio::spawn(async move {
1150 while let Ok((mut stream, _)) = listener.accept().await {
1151 let count_clone = request_count_clone.clone();
1152 tokio::spawn(async move {
1153 *count_clone.lock().unwrap() += 1;
1154 let count = *count_clone.lock().unwrap();
1155 println!("Mock server: Received request #{count}");
1156
1157 let body = read_request_body(&mut stream).await;
1158 if count <= 2 {
1159 let response = "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 21\r\n\r\nInternal Server Error";
1160 let _ = stream
1161 .write_all(response.as_bytes())
1162 .await;
1163 } else {
1164 let json_response = QUOTE_RESPONSE
1165 .replace("{{EFFECTIVE_TRADER}}", &effective_trader_of(&body));
1166 let response = format!(
1167 "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
1168 json_response.len(),
1169 json_response
1170 );
1171 let _ = stream
1172 .write_all(response.as_bytes())
1173 .await;
1174 }
1175 let _ = stream.flush().await;
1176 let _ = stream.shutdown().await;
1177 });
1178 }
1179 });
1180
1181 tokio::time::sleep(Duration::from_millis(50)).await;
1182 (addr, request_count)
1183 }
1184
1185 #[tokio::test]
1186 async fn test_hashflow_quote_retry_on_bad_response() {
1187 let (addr, request_count) = create_retry_server().await;
1188
1189 let client = create_test_hashflow_client(
1190 format!("http://127.0.0.1:{}/rfq", addr.port()),
1191 Duration::from_secs(5),
1192 );
1193 let params = create_test_quote_params();
1194 let result = client
1195 .request_binding_quote(¶ms)
1196 .await;
1197
1198 assert!(result.is_ok(), "Expected success after retries, got: {:?}", result);
1199 let quote = result.unwrap();
1200
1201 assert_eq!(quote.amount_in, BigUint::from(1_000000000000000000u64));
1203 assert_eq!(quote.amount_out, BigUint::from(3329502u64));
1204
1205 let final_count = *request_count.lock().unwrap();
1207 assert_eq!(final_count, 3, "Expected 3 requests, got {}", final_count);
1208 }
1209
1210 #[test]
1211 fn test_hashflow_client_serialize_deserialize_roundtrip() {
1212 let token_in = Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap();
1213 let token_out = Bytes::from_str("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599").unwrap();
1214 let quote_token = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
1215
1216 let original = HashflowClient {
1217 chain: Chain::Ethereum,
1218 price_levels_endpoint: "https://api.hashflow.com/price_levels".to_string(),
1219 market_makers_endpoint: "https://api.hashflow.com/market_makers".to_string(),
1220 quote_endpoint: "https://api.hashflow.com/quote".to_string(),
1221 tokens: HashSet::from([token_in.clone(), token_out.clone()]),
1222 tvl: 50.5,
1223 auth_key: "secret_key".to_string(),
1224 auth_user: "secret_user".to_string(),
1225 quote_tokens: HashSet::from([quote_token.clone()]),
1226 poll_time: Duration::from_secs(10),
1227 quote_timeout: Duration::from_millis(5500),
1228 };
1229
1230 let serialized = serde_json::to_string(&original).unwrap();
1231 let deserialized: HashflowClient = serde_json::from_str(&serialized).unwrap();
1232
1233 assert_eq!(deserialized.chain, original.chain);
1235 assert_eq!(deserialized.price_levels_endpoint, original.price_levels_endpoint);
1236 assert_eq!(deserialized.market_makers_endpoint, original.market_makers_endpoint);
1237 assert_eq!(deserialized.quote_endpoint, original.quote_endpoint);
1238 assert_eq!(deserialized.tokens, original.tokens);
1239 assert_eq!(deserialized.tvl, original.tvl);
1240 assert_eq!(deserialized.quote_tokens, original.quote_tokens);
1241 assert_eq!(deserialized.poll_time, original.poll_time);
1242 assert_eq!(deserialized.quote_timeout, original.quote_timeout);
1243
1244 assert_eq!(deserialized.auth_key, "");
1246 assert_eq!(deserialized.auth_user, "");
1247 assert_ne!(deserialized.auth_key, original.auth_key);
1248 assert_ne!(deserialized.auth_user, original.auth_user);
1249 }
1250
1251 #[test]
1252 fn test_hashflow_client_deserialize_with_credentials() {
1253 let json = r#"{
1256 "chain": "ethereum",
1257 "price_levels_endpoint": "https://api.hashflow.com/price_levels",
1258 "market_makers_endpoint": "https://api.hashflow.com/market_makers",
1259 "quote_endpoint": "https://api.hashflow.com/quote",
1260 "tokens": [],
1261 "tvl": 10.0,
1262 "auth_key": "provided_key",
1263 "auth_user": "provided_user",
1264 "quote_tokens": [],
1265 "poll_time": {"secs": 10, "nanos": 0},
1266 "quote_timeout": {"secs": 30, "nanos": 0}
1267 }"#;
1268
1269 let client: HashflowClient = serde_json::from_str(json).unwrap();
1270
1271 assert_eq!(client.auth_key, "provided_key");
1273 assert_eq!(client.auth_user, "provided_user");
1274 }
1275}