1use std::{
2 collections::{HashMap, HashSet},
3 str::FromStr,
4 time::SystemTime,
5};
6
7use alloy::primitives::{utils::keccak256, Address};
8use async_trait::async_trait;
9use futures::{stream::BoxStream, StreamExt};
10use http::Request;
11use num_bigint::BigUint;
12use prost::Message as ProstMessage;
13use reqwest::Client;
14use serde::{Deserialize, Serialize};
15use tokio::time::{sleep, timeout, Duration};
16use tokio_tungstenite::{
17 connect_async_with_config,
18 tungstenite::{handshake::client::generate_key, Message},
19};
20use tracing::{error, info, warn};
21use tycho_common::{
22 models::{protocol::GetAmountOutParams, Chain},
23 simulation::indicatively_priced::SignedQuote,
24 Bytes,
25};
26
27use crate::{
28 rfq::{
29 client::RFQClient,
30 errors::RFQError,
31 models::TimestampHeader,
32 protocols::bebop::models::{
33 BebopOrderToSign, BebopPriceData, BebopPricingUpdate, BebopQuoteResponse,
34 },
35 },
36 tycho_client::feed::synchronizer::{ComponentWithState, Snapshot, StateSyncMessage},
37 tycho_common::models::protocol::{ProtocolComponent, ProtocolComponentState},
38};
39
40fn bytes_to_address(address: &Bytes) -> Result<Address, RFQError> {
41 if address.len() == 20 {
42 Ok(Address::from_slice(address))
43 } else {
44 Err(RFQError::InvalidInput(format!("Invalid ERC20 token address: {address:?}")))
45 }
46}
47
48fn chain_to_bebop_url(chain: Chain) -> Result<String, RFQError> {
50 let chain_path = match chain {
51 Chain::Ethereum => "ethereum",
52 Chain::Base => "base",
53 _ => return Err(RFQError::FatalError(format!("Unsupported chain: {chain:?}"))),
54 };
55 let url = format!("api.bebop.xyz/pmm/{chain_path}/v3");
56 Ok(url)
57}
58
59#[derive(Clone, Debug, Serialize, Deserialize)]
60pub struct BebopClient {
61 chain: Chain,
62 price_ws: String,
63 quote_endpoint: String,
64 tokens: HashSet<Bytes>,
66 tvl: f64,
68 #[serde(skip_serializing, default)]
70 ws_key: String,
71 quote_tokens: HashSet<Bytes>,
73 quote_timeout: Duration,
74}
75
76impl BebopClient {
77 pub const PROTOCOL_SYSTEM: &'static str = "rfq:bebop";
78
79 pub fn new(
80 chain: Chain,
81 tokens: HashSet<Bytes>,
82 tvl: f64,
83 ws_key: String,
84 quote_tokens: HashSet<Bytes>,
85 quote_timeout: Duration,
86 ) -> Result<Self, RFQError> {
87 let url = chain_to_bebop_url(chain)?;
88 Ok(Self {
89 price_ws: "wss://".to_string() + &url + "/pricing?format=protobuf",
90 quote_endpoint: "https://".to_string() + &url + "/quote",
91 tokens,
92 chain,
93 tvl,
94 ws_key,
95 quote_tokens,
96 quote_timeout,
97 })
98 }
99
100 fn create_component_with_state(
101 &self,
102 component_id: String,
103 tokens: Vec<tycho_common::Bytes>,
104 price_data: &BebopPriceData,
105 tvl: f64,
106 ) -> ComponentWithState {
107 let protocol_component = ProtocolComponent {
108 id: component_id.clone(),
109 protocol_system: Self::PROTOCOL_SYSTEM.to_string(),
110 protocol_type_name: "bebop_pool".to_string(),
111 chain: self.chain,
112 tokens,
113 contract_addresses: vec![], static_attributes: Default::default(),
115 change: Default::default(),
116 creation_tx: Default::default(),
117 created_at: Default::default(),
118 };
119
120 let mut attributes = HashMap::new();
121
122 if !price_data.bids.is_empty() {
126 let bids_pairs: Vec<(f32, f32)> = price_data
127 .bids
128 .as_chunks::<2>()
129 .0
130 .iter()
131 .map(|chunk| (chunk[0], chunk[1]))
132 .collect();
133 let bids_json = serde_json::to_string(&bids_pairs).unwrap_or_default();
134 attributes.insert("bids".to_string(), bids_json.as_bytes().to_vec().into());
135 }
136 if !price_data.asks.is_empty() {
137 let asks_pairs: Vec<(f32, f32)> = price_data
138 .asks
139 .as_chunks::<2>()
140 .0
141 .iter()
142 .map(|chunk| (chunk[0], chunk[1]))
143 .collect();
144 let asks_json = serde_json::to_string(&asks_pairs).unwrap_or_default();
145 attributes.insert("asks".to_string(), asks_json.as_bytes().to_vec().into());
146 }
147
148 ComponentWithState {
149 state: ProtocolComponentState::new(&component_id, attributes, HashMap::new()),
150 component: protocol_component,
151 component_tvl: Some(tvl),
152 entrypoints: vec![],
153 }
154 }
155
156 fn process_quote_response(
157 quote_response: BebopQuoteResponse,
158 params: &GetAmountOutParams,
159 ) -> Result<SignedQuote, RFQError> {
160 match quote_response {
161 BebopQuoteResponse::Success(quote) => {
162 quote.validate(params)?;
163
164 let mut quote_attributes: HashMap<String, Bytes> = HashMap::new();
165 quote_attributes.insert("tx_to".into(), quote.tx.to);
168 quote_attributes.insert("calldata".into(), quote.tx.data);
169 quote_attributes.insert(
170 "partial_fill_offset".into(),
171 Bytes::from(
172 quote
173 .partial_fill_offset
174 .to_be_bytes()
175 .to_vec(),
176 ),
177 );
178 let signed_quote = match quote.to_sign {
179 BebopOrderToSign::Single(ref single) => SignedQuote {
180 base_token: params.token_in.clone(),
181 quote_token: params.token_out.clone(),
182 amount_in: BigUint::from_str(&single.taker_amount).map_err(|_| {
183 RFQError::ParsingError(format!(
184 "Failed to parse amount in string: {}",
185 single.taker_amount
186 ))
187 })?,
188 amount_out: BigUint::from_str(&single.maker_amount).map_err(|_| {
189 RFQError::ParsingError(format!(
190 "Failed to parse amount out string: {}",
191 single.maker_amount
192 ))
193 })?,
194 quote_attributes,
195 },
196 BebopOrderToSign::Aggregate(aggregate) => {
197 let amount_in: BigUint = aggregate
199 .taker_tokens
200 .iter()
201 .zip(&aggregate.taker_amounts)
202 .flat_map(|(tokens, amounts)| {
203 tokens
204 .iter()
205 .zip(amounts)
206 .filter_map(|(token, amount)| {
207 if token == ¶ms.token_in {
208 BigUint::from_str(amount).ok()
209 } else {
210 None
211 }
212 })
213 })
214 .sum();
215
216 let amount_out: BigUint = aggregate
218 .maker_tokens
219 .iter()
220 .zip(&aggregate.maker_amounts)
221 .flat_map(|(tokens, amounts)| {
222 tokens
223 .iter()
224 .zip(amounts)
225 .filter_map(|(token, amount)| {
226 if token == ¶ms.token_out {
227 BigUint::from_str(amount).ok()
228 } else {
229 None
230 }
231 })
232 })
233 .sum();
234
235 SignedQuote {
236 base_token: params.token_in.clone(),
237 quote_token: params.token_out.clone(),
238 amount_in,
239 amount_out,
240 quote_attributes,
241 }
242 }
243 };
244
245 Ok(signed_quote)
246 }
247 BebopQuoteResponse::Error(err) => Err(RFQError::FatalError(format!(
248 "Bebop API error: code {} - {} (requestId: {})",
249 err.error.error_code, err.error.message, err.error.request_id
250 ))),
251 }
252 }
253}
254
255#[async_trait]
256impl RFQClient for BebopClient {
257 fn stream(
258 &self,
259 ) -> BoxStream<'static, Result<(String, StateSyncMessage<TimestampHeader>), RFQError>> {
260 let tokens = self.tokens.clone();
261 let url = self.price_ws.clone();
262 let tvl_threshold = self.tvl;
263 let authorization = format!("Bearer {}", self.ws_key);
264 let client = self.clone();
265
266 Box::pin(async_stream::stream! {
267 let mut current_components: HashMap<String, ComponentWithState> = HashMap::new();
268 let mut reconnect_attempts = 0;
269 const MAX_RECONNECT_ATTEMPTS: u32 = 10;
270
271 loop {
272 let request = Request::builder()
273 .method("GET")
274 .uri(&url)
275 .header("Host", "api.bebop.xyz")
276 .header("Upgrade", "websocket")
277 .header("Connection", "Upgrade")
278 .header("Sec-WebSocket-Key", generate_key())
279 .header("Sec-WebSocket-Version", "13")
280 .header("Authorization", &authorization)
281 .body(())
282 .map_err(|_| RFQError::FatalError("Failed to build request".into()))?;
283
284 let (ws_stream, _) = match connect_async_with_config(request, None, false).await {
286 Ok(connection) => {
287 info!("Successfully connected to Bebop WebSocket");
288 reconnect_attempts = 0; connection
290 },
291 Err(e) => {
292 reconnect_attempts += 1;
293 error!("Failed to connect to Bebop WebSocket (attempt {}): {}", reconnect_attempts, e);
294
295 if reconnect_attempts >= MAX_RECONNECT_ATTEMPTS {
296 yield Err(RFQError::ConnectionError(format!("Failed to connect after {MAX_RECONNECT_ATTEMPTS} attempts: {e}")));
297 return;
298 }
299
300 let backoff_duration = Duration::from_secs(2_u64.pow(reconnect_attempts.min(5)));
301 info!("Retrying connection in {} seconds...", backoff_duration.as_secs());
302 sleep(backoff_duration).await;
303 continue;
304 }
305 };
306
307 let (_, mut ws_receiver) = ws_stream.split();
308
309 while let Some(msg) = ws_receiver.next().await {
311 match msg {
312 Ok(Message::Binary(data)) => {
313 match BebopPricingUpdate::decode(&data[..]) {
314 Ok(protobuf_update) => {
315 let mut new_components = HashMap::new();
316
317 for price_data in &protobuf_update.pairs {
319 let base_bytes = Bytes::from(price_data.base.clone());
320 let quote_bytes = Bytes::from(price_data.quote.clone());
321 if tokens.contains(&base_bytes) && tokens.contains("e_bytes) {
322 let pair_tokens = vec![
323 base_bytes.clone(), quote_bytes.clone()
324 ];
325
326 let mut quote_price_data: Option<&BebopPriceData> = None;
327 if !client.quote_tokens.contains("e_bytes) {
330 for approved_quote_token in &client.quote_tokens {
331 if let Some(quote_data) = protobuf_update.pairs.iter()
334 .find(|p| {
335 (p.base == quote_bytes.as_ref() && p.quote == approved_quote_token.as_ref()) ||
336 (p.quote == quote_bytes.as_ref() && p.base == approved_quote_token.as_ref())
337 }) {
338 quote_price_data = Some(quote_data);
339 break;
340 }
341 }
342
343 if quote_price_data.is_none() {
346 warn!("Quote token {} does not have price levels in approved quote token. Skipping.", hex::encode("e_bytes));
347 continue;
348 }
349 }
350
351 let tvl = price_data.calculate_tvl(quote_price_data);
352 if tvl < tvl_threshold {
353 continue;
354 }
355
356 let pair_str = format!("bebop_{}/{}", hex::encode(&base_bytes), hex::encode("e_bytes));
357 let component_id = format!("{}", keccak256(pair_str.as_bytes()));
358 let component_with_state = client.create_component_with_state(
359 component_id.clone(),
360 pair_tokens,
361 price_data,
362 tvl
363 );
364 new_components.insert(component_id, component_with_state);
365 }
366 }
367
368 let removed_components: HashMap<String, ProtocolComponent> = current_components
372 .iter()
373 .filter(|&(id, _)| !new_components.contains_key(id))
374 .map(|(k, v)| (k.clone(), v.component.clone()))
375 .collect();
376
377 current_components = new_components.clone();
379
380 let snapshot = Snapshot {
381 states: new_components,
382 vm_storage: HashMap::new(),
383 };
384 let timestamp = SystemTime::now().duration_since(
385 SystemTime::UNIX_EPOCH
386 ).map_err(
387 |_| RFQError::ParsingError("SystemTime before UNIX EPOCH!".into())
388 )?.as_secs();
389
390 let msg = StateSyncMessage::<TimestampHeader> {
391 header: TimestampHeader { timestamp },
392 snapshots: snapshot,
393 deltas: None, removed_components,
395 };
396
397 yield Ok(("bebop".to_string(), msg));
399 },
400 Err(e) => {
401 error!("Failed to parse protobuf message: {}", e);
402 break;
403 }
404 }
405 }
406 Ok(Message::Close(_)) => {
407 info!("WebSocket connection closed by server");
408 break;
409 }
410 Err(e) => {
411 error!("WebSocket error: {}", e);
412 break;
413 }
414 _ => {} }
416 }
417
418 reconnect_attempts += 1;
420 if reconnect_attempts >= MAX_RECONNECT_ATTEMPTS {
421 yield Err(RFQError::ConnectionError(format!("Connection failed after {MAX_RECONNECT_ATTEMPTS} attempts")));
422 return;
423 }
424
425 let backoff_duration = Duration::from_secs(2_u64.pow(reconnect_attempts.min(5)));
426 info!("Reconnecting in {} seconds (attempt {})...", backoff_duration.as_secs(), reconnect_attempts);
427 sleep(backoff_duration).await;
428 }
430 })
431 }
432
433 async fn request_binding_quote(
434 &self,
435 params: &GetAmountOutParams,
436 ) -> Result<SignedQuote, RFQError> {
437 let sell_token = bytes_to_address(¶ms.token_in)?.to_string();
438 let buy_token = bytes_to_address(¶ms.token_out)?.to_string();
439 let sell_amount = params.amount_in.to_string();
440 let sender = bytes_to_address(¶ms.sender)?.to_string();
441 let receiver = bytes_to_address(¶ms.receiver)?.to_string();
442
443 let url = self.quote_endpoint.clone();
444
445 let client = Client::new();
446
447 let start_time = std::time::Instant::now();
448 const MAX_RETRIES: u32 = 3;
449 let mut last_error = None;
450
451 for attempt in 0..MAX_RETRIES {
452 let elapsed = start_time.elapsed();
454 if elapsed >= self.quote_timeout {
455 return Err(last_error.unwrap_or_else(|| {
456 RFQError::ConnectionError(format!(
457 "Bebop quote request timed out after {} seconds",
458 self.quote_timeout.as_secs()
459 ))
460 }));
461 }
462
463 let remaining_time = self.quote_timeout - elapsed;
464
465 let request = client
466 .get(&url)
467 .query(&[
468 ("sell_tokens", sell_token.clone()),
469 ("buy_tokens", buy_token.clone()),
470 ("sell_amounts", sell_amount.clone()),
471 ("taker_address", sender.clone()),
472 ("receiver_address", receiver.clone()),
473 ("approval_type", "Standard".into()),
474 ("skip_validation", "true".into()),
475 ("skip_taker_checks", "true".into()),
476 ("gasless", "false".into()),
477 ("expiry_type", "standard".into()),
478 ("fee", "0".into()),
479 ("is_ui", "false".into()),
480 ])
481 .header("accept", "application/json")
482 .bearer_auth(&self.ws_key);
483
484 let response = match timeout(remaining_time, request.send()).await {
485 Ok(Ok(resp)) => resp,
486 Ok(Err(e)) => {
487 warn!(
488 "Bebop quote request failed (attempt {}/{}): {}",
489 attempt + 1,
490 MAX_RETRIES,
491 e
492 );
493 last_error = Some(RFQError::ConnectionError(format!(
494 "Failed to send Bebop quote request: {e}"
495 )));
496 if attempt < MAX_RETRIES - 1 {
497 continue;
498 } else {
499 return Err(last_error.unwrap());
500 }
501 }
502 Err(_) => {
503 return Err(RFQError::ConnectionError(format!(
504 "Bebop quote request timed out after {} seconds",
505 self.quote_timeout.as_secs()
506 )));
507 }
508 };
509
510 let quote_response = match response
511 .json::<BebopQuoteResponse>()
512 .await
513 {
514 Ok(resp) => resp,
515 Err(e) => {
516 warn!(
517 "Bebop quote response parsing failed (attempt {}/{}): {}",
518 attempt + 1,
519 MAX_RETRIES,
520 e
521 );
522 last_error = Some(RFQError::ParsingError(format!(
523 "Failed to parse Bebop quote response: {e}"
524 )));
525 if attempt < MAX_RETRIES - 1 {
526 sleep(Duration::from_millis(100)).await;
527 continue;
528 } else {
529 return Err(last_error.unwrap());
530 }
531 }
532 };
533
534 return Self::process_quote_response(quote_response, params);
535 }
536
537 Err(last_error.unwrap_or_else(|| {
538 RFQError::ConnectionError("Bebop quote request failed after retries".to_string())
539 }))
540 }
541}
542
543#[cfg(test)]
544mod tests {
545 use std::{
546 sync::{Arc, Mutex},
547 time::Duration,
548 };
549
550 use dotenv::dotenv;
551 use futures::SinkExt;
552 use tokio::{net::TcpListener, time::timeout};
553 use tokio_tungstenite::accept_async;
554
555 use super::*;
556 use crate::rfq::constants::get_bebop_auth;
557
558 #[tokio::test]
559 #[ignore] async fn test_bebop_websocket_connection() {
561 let wbtc = Bytes::from_str("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599").unwrap();
564 let weth = Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap();
565
566 dotenv().expect("Missing .env file");
567 let auth = get_bebop_auth().expect("Failed to get Bebop authentication");
568
569 let quote_tokens = HashSet::from([
570 Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(), Bytes::from_str("0xdac17f958d2ee523a2206206994597c13d831ec7").unwrap(), ]);
574
575 let client = BebopClient::new(
576 Chain::Ethereum,
577 HashSet::from_iter(vec![weth.clone(), wbtc.clone()]),
578 10.0, auth.key,
580 quote_tokens,
581 Duration::from_secs(30),
582 )
583 .unwrap();
584
585 let mut stream = client.stream();
586
587 let result = timeout(Duration::from_secs(10), async {
592 let mut message_count = 0;
593 let max_messages = 1;
594
595 while let Some(result) = stream.next().await {
596 match result {
597 Ok((component_id, msg)) => {
598 println!("Received message with ID: {component_id}");
599
600 assert!(!component_id.is_empty());
601 assert_eq!(component_id, "bebop");
602 assert!(msg.header.timestamp > 0);
603 assert!(!msg.snapshots.states.is_empty());
604
605 let snapshot = &msg.snapshots;
606
607 assert!(!snapshot.states.is_empty());
609
610 println!("Received {} components in this message", snapshot.states.len());
611 for (id, component_with_state) in &snapshot.states {
612 assert_eq!(
613 component_with_state
614 .component
615 .protocol_system,
616 "rfq:bebop"
617 );
618 assert_eq!(
619 component_with_state
620 .component
621 .protocol_type_name,
622 "bebop_pool"
623 );
624 assert_eq!(component_with_state.component.chain, Chain::Ethereum);
625
626 let attributes = &component_with_state.state.attributes;
627
628 assert!(attributes.contains_key("bids"));
630 assert!(attributes.contains_key("asks"));
631 assert!(!attributes["bids"].is_empty());
632 assert!(!attributes["asks"].is_empty());
633
634 if let Some(tvl) = component_with_state.component_tvl {
635 assert!(tvl >= 0.0);
636 println!("Component {id} TVL: ${tvl:.2}");
637 }
638 }
639
640 message_count += 1;
641 if message_count >= max_messages {
642 break;
643 }
644 }
645 Err(e) => {
646 panic!("Stream error: {e}");
647 }
648 }
649 }
650
651 assert!(message_count > 0, "Should have received at least one message");
652 println!("Successfully received {message_count} messages");
653 })
654 .await;
655
656 match result {
657 Ok(_) => println!("Test completed successfully"),
658 Err(_) => panic!("Test timed out - no messages received within 10 seconds"),
659 }
660 }
661
662 #[tokio::test]
663 async fn test_websocket_reconnection() {
664 let listener = TcpListener::bind("127.0.0.1:0")
666 .await
667 .unwrap();
668 let addr = listener.local_addr().unwrap();
669
670 let connection_count = Arc::new(Mutex::new(0u32));
672
673 let connection_count_clone = connection_count.clone();
675
676 tokio::spawn(async move {
677 while let Ok((stream, _)) = listener.accept().await {
678 *connection_count_clone.lock().unwrap() += 1;
679 let count = *connection_count_clone.lock().unwrap();
680 println!("Mock server: Connection #{count} established");
681
682 tokio::spawn(async move {
683 if let Ok(ws_stream) = accept_async(stream).await {
684 let (mut ws_sender, _ws_receiver) = ws_stream.split();
685
686 let weth_addr =
688 hex::decode("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap();
689 let usdc_addr =
690 hex::decode("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap();
691
692 let test_price_data = BebopPriceData {
693 base: weth_addr,
694 quote: usdc_addr,
695 last_update_ts: 1752617378,
696 bids: vec![3070.05f32, 0.325717f32],
697 asks: vec![3070.527f32, 0.325717f32],
698 };
699
700 let pricing_update = BebopPricingUpdate { pairs: vec![test_price_data] };
701
702 let test_message = pricing_update.encode_to_vec();
703
704 if count == 1 {
705 println!("Mock server: Connection #1 - sending message then dropping.");
707 let _ = ws_sender
708 .send(Message::Binary(test_message.clone().into()))
709 .await;
710
711 tokio::time::sleep(Duration::from_millis(100)).await;
713 println!("Mock server: Dropping connection #1");
714 let _ = ws_sender.close().await;
715 } else if count == 2 {
716 println!("Mock server: Connection #2 - maintaining stable connection.");
718 let _ = ws_sender
719 .send(Message::Binary(test_message.clone().into()))
720 .await;
721 }
722 }
723 });
724 }
725 });
726
727 tokio::time::sleep(Duration::from_millis(50)).await;
729
730 let mut test_quote_tokens = HashSet::new();
731 test_quote_tokens
732 .insert(Bytes::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap());
733
734 let tokens_formatted = vec![
735 Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap(),
736 Bytes::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap(),
737 ];
738
739 let client = BebopClient {
741 chain: Chain::Ethereum,
742 price_ws: format!("ws://127.0.0.1:{}", addr.port()),
743 tokens: tokens_formatted.into_iter().collect(),
744 tvl: 1000.0,
745 ws_key: "test_key".to_string(),
746 quote_tokens: test_quote_tokens,
747 quote_endpoint: "".to_string(),
748 quote_timeout: Duration::from_secs(5),
749 };
750
751 let start_time = std::time::Instant::now();
752 let mut successful_messages = 0;
753 let mut connection_errors = 0;
754 let mut first_message_received = false;
755 let mut second_message_received = false;
756
757 while start_time.elapsed() < Duration::from_secs(5) && successful_messages < 2 {
764 match timeout(Duration::from_millis(1000), client.stream().next()).await {
765 Ok(Some(result)) => match result {
766 Ok((_component_id, _message)) => {
767 successful_messages += 1;
768 println!("Received successful message {successful_messages}");
769
770 if successful_messages == 1 {
771 first_message_received = true;
772 println!("First message received - connection should drop after this.");
773 } else if successful_messages == 2 {
774 second_message_received = true;
775 println!("Second message received after reconnection.");
776 }
777 }
778 Err(e) => {
779 connection_errors += 1;
780 println!("Connection error during reconnection: {e:?}");
781 }
782 },
783 Ok(None) => {
784 panic!("Stream ended unexpectedly");
785 }
786 Err(_) => {
787 println!("Timeout waiting for message (normal during reconnections)");
788 continue;
789 }
790 }
791 }
792
793 let final_connection_count = *connection_count.lock().unwrap();
794
795 assert_eq!(final_connection_count, 2);
799 assert!(first_message_received);
800 assert!(second_message_received);
801 assert_eq!(connection_errors, 0);
802 assert_eq!(successful_messages, 2);
803 }
804
805 #[tokio::test]
806 #[ignore] async fn test_bebop_quote_single_order() {
808 let token_in = Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap();
809 let token_out = Bytes::from_str("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599").unwrap();
810 dotenv().expect("Missing .env file");
811 let auth = get_bebop_auth().expect("Failed to get Bebop authentication");
812
813 let client = BebopClient::new(
814 Chain::Ethereum,
815 HashSet::from_iter(vec![token_in.clone(), token_out.clone()]),
816 10.0, auth.key,
818 HashSet::new(),
819 Duration::from_secs(30),
820 )
821 .unwrap();
822
823 let router = Bytes::from_str("0xfD0b31d2E955fA55e3fa641Fe90e08b677188d35").unwrap();
824
825 let params = GetAmountOutParams {
826 amount_in: BigUint::from(1_000000000000000000u64),
827 token_in: token_in.clone(),
828 token_out: token_out.clone(),
829 sender: router.clone(),
830 receiver: router,
831 };
832 let quote = client
833 .request_binding_quote(¶ms)
834 .await
835 .unwrap();
836
837 assert_eq!(quote.base_token, token_in);
838 assert_eq!(quote.quote_token, token_out);
839 assert_eq!(quote.amount_in, BigUint::from(1_000000000000000000u64));
840
841 assert!(quote.amount_out > BigUint::from(1_000_000u64));
844
845 assert_eq!(
847 quote
848 .quote_attributes
849 .get("calldata")
850 .unwrap()[..4],
851 Bytes::from_str("0x4dcebcba")
852 .unwrap()
853 .to_vec()
854 );
855 let partial_fill_offset_slice = quote
856 .quote_attributes
857 .get("partial_fill_offset")
858 .unwrap()
859 .as_ref();
860 let mut partial_fill_offset_array = [0u8; 8];
861 partial_fill_offset_array.copy_from_slice(partial_fill_offset_slice);
862
863 assert_eq!(u64::from_be_bytes(partial_fill_offset_array), 12);
864 }
865
866 #[tokio::test]
867 #[ignore] async fn test_bebop_quote_aggregate_order() {
869 let token_in = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
872 let token_out = Bytes::from_str("0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3").unwrap();
873 dotenv().expect("Missing .env file");
874 let auth = get_bebop_auth().expect("Failed to get Bebop authentication");
875
876 let client = BebopClient::new(
877 Chain::Ethereum,
878 HashSet::from_iter(vec![token_in.clone(), token_out.clone()]),
879 10.0, auth.key,
881 HashSet::new(),
882 Duration::from_secs(30),
883 )
884 .unwrap();
885
886 let router = Bytes::from_str("0xfD0b31d2E955fA55e3fa641Fe90e08b677188d35").unwrap();
887
888 let amount_in = BigUint::from_str("20_000_000_000").unwrap(); let params = GetAmountOutParams {
890 amount_in: amount_in.clone(),
891 token_in: token_in.clone(),
892 token_out: token_out.clone(),
893 sender: router.clone(),
894 receiver: router,
895 };
896 let quote = client
897 .request_binding_quote(¶ms)
898 .await
899 .unwrap();
900
901 assert_eq!(quote.base_token, token_in);
902 assert_eq!(quote.quote_token, token_out);
903 assert_eq!(quote.amount_in, amount_in);
904
905 assert!(quote.amount_out > BigUint::from_str("18000000000000000000000").unwrap()); assert_eq!(
910 quote
911 .quote_attributes
912 .get("calldata")
913 .unwrap()[..4],
914 Bytes::from_str("0xa2f74893")
915 .unwrap()
916 .to_vec()
917 );
918 let partial_fill_offset_slice = quote
919 .quote_attributes
920 .get("partial_fill_offset")
921 .unwrap()
922 .as_ref();
923 let mut partial_fill_offset_array = [0u8; 8];
924 partial_fill_offset_array.copy_from_slice(partial_fill_offset_slice);
925
926 assert_eq!(u64::from_be_bytes(partial_fill_offset_array), 2);
929 }
930
931 #[test]
932 fn test_process_bebop_quote_response_aggregate_order() {
933 let json =
934 std::fs::read_to_string("src/rfq/protocols/bebop/test_responses/aggregate_order.json")
935 .unwrap();
936 let quote_response: BebopQuoteResponse = serde_json::from_str(&json).unwrap();
937 let params = GetAmountOutParams {
938 amount_in: BigUint::from_str("43067495979235520920162").unwrap(),
939 token_in: Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
940 token_out: Bytes::from_str("0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3").unwrap(),
941 sender: Bytes::from_str("0xfd0b31d2e955fa55e3fa641fe90e08b677188d35").unwrap(),
942 receiver: Bytes::from_str("0xfd0b31d2e955fa55e3fa641fe90e08b677188d35").unwrap(),
943 };
944 let res = BebopClient::process_quote_response(quote_response, ¶ms).unwrap();
945 assert_eq!(res.amount_out, BigUint::from_str("21700473797683400419007").unwrap());
946 assert_eq!(res.amount_in, BigUint::from_str("20000000000").unwrap());
947 assert_eq!(res.base_token, params.token_in);
948 assert_eq!(res.quote_token, params.token_out);
949 }
950
951 #[test]
952 fn test_process_bebop_quote_response_aggregate_order_with_multihop() {
953 let json = std::fs::read_to_string(
954 "src/rfq/protocols/bebop/test_responses/aggregate_order_with_multihop.json",
955 )
956 .unwrap();
957 let quote_response: BebopQuoteResponse = serde_json::from_str(&json).unwrap();
958 let params = GetAmountOutParams {
959 amount_in: BigUint::from_str("43067495979235520920162").unwrap(),
960 token_in: Bytes::from_str("0xDEf1CA1fb7FBcDC777520aa7f396b4E015F497aB").unwrap(),
961 token_out: Bytes::from_str("0xdAC17F958D2ee523a2206206994597C13D831ec7").unwrap(),
962 sender: Bytes::from_str("0x809305d724B6E79C71e10a097ABadd1274B9C279").unwrap(),
963 receiver: Bytes::from_str("0x809305d724B6E79C71e10a097ABadd1274B9C279").unwrap(),
964 };
965 let res = BebopClient::process_quote_response(quote_response, ¶ms).unwrap();
966 assert_eq!(res.amount_out, BigUint::from_str("11186653890").unwrap());
967 assert_eq!(res.amount_in, BigUint::from_str("43067495979235520920162").unwrap());
968 assert_eq!(res.base_token, params.token_in);
969 assert_eq!(res.quote_token, params.token_out);
970 }
971
972 async fn create_delayed_response_server(delay_ms: u64) -> std::net::SocketAddr {
974 use tokio::io::AsyncWriteExt;
975
976 let listener = TcpListener::bind("127.0.0.1:0")
977 .await
978 .unwrap();
979 let addr = listener.local_addr().unwrap();
980
981 let json_response =
982 std::fs::read_to_string("src/rfq/protocols/bebop/test_responses/aggregate_order.json")
983 .unwrap();
984
985 tokio::spawn(async move {
986 while let Ok((mut stream, _)) = listener.accept().await {
987 let json_response_clone = json_response.clone();
988 tokio::spawn(async move {
989 sleep(Duration::from_millis(delay_ms)).await;
990
991 let response = format!(
992 "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
993 json_response_clone.len(),
994 json_response_clone
995 );
996 let _ = stream
997 .write_all(response.as_bytes())
998 .await;
999 let _ = stream.flush().await;
1000 let _ = stream.shutdown().await;
1001 });
1002 }
1003 });
1004
1005 addr
1006 }
1007
1008 fn create_test_bebop_client(quote_endpoint: String, quote_timeout: Duration) -> BebopClient {
1009 let token_in = Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap();
1010 let token_out = Bytes::from_str("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599").unwrap();
1011
1012 BebopClient {
1013 chain: Chain::Ethereum,
1014 price_ws: "ws://example.com".to_string(),
1015 quote_endpoint,
1016 tokens: HashSet::from([token_in, token_out]),
1017 tvl: 10.0,
1018 ws_key: "test_key".to_string(),
1019 quote_tokens: HashSet::new(),
1020 quote_timeout,
1021 }
1022 }
1023
1024 fn create_test_quote_params() -> GetAmountOutParams {
1026 let token_in = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
1027 let token_out = Bytes::from_str("0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3").unwrap();
1028 let router = Bytes::from_str("0xfD0b31d2E955fA55e3fa641Fe90e08b677188d35").unwrap();
1029
1030 GetAmountOutParams {
1031 amount_in: BigUint::from_str("43067495979235520920162").unwrap(),
1032 token_in,
1033 token_out,
1034 sender: router.clone(),
1035 receiver: router,
1036 }
1037 }
1038
1039 #[tokio::test]
1040 async fn test_bebop_quote_timeout() {
1041 let addr = create_delayed_response_server(500).await;
1042
1043 let client_short_timeout = create_test_bebop_client(
1045 format!("http://127.0.0.1:{}/quote", addr.port()),
1046 Duration::from_millis(200),
1047 );
1048 let params = create_test_quote_params();
1049
1050 let start = std::time::Instant::now();
1051 let result = client_short_timeout
1052 .request_binding_quote(¶ms)
1053 .await;
1054 let elapsed = start.elapsed();
1055
1056 assert!(result.is_err());
1057 let err = result.unwrap_err();
1058 match err {
1059 RFQError::ConnectionError(msg) => {
1060 assert!(msg.contains("timed out"), "Expected timeout error, got: {}", msg);
1061 }
1062 _ => panic!("Expected ConnectionError, got: {:?}", err),
1063 }
1064 assert!(
1065 elapsed.as_millis() >= 200 && elapsed.as_millis() < 400,
1066 "Expected timeout around 200ms, got: {:?}",
1067 elapsed
1068 );
1069
1070 let client_long_timeout = create_test_bebop_client(
1074 format!("http://127.0.0.1:{}/quote", addr.port()),
1075 Duration::from_secs(1),
1076 );
1077
1078 let result = client_long_timeout
1079 .request_binding_quote(¶ms)
1080 .await;
1081
1082 assert!(result.is_ok(), "Expected success, got: {:?}", result);
1084 let quote = result.unwrap();
1085
1086 assert_eq!(quote.base_token, params.token_in);
1088 assert_eq!(quote.quote_token, params.token_out);
1089 }
1090
1091 async fn create_retry_server() -> (std::net::SocketAddr, Arc<Mutex<u32>>) {
1094 use std::sync::{Arc, Mutex};
1095
1096 use tokio::io::AsyncWriteExt;
1097
1098 let request_count = Arc::new(Mutex::new(0u32));
1099 let request_count_clone = request_count.clone();
1100
1101 let listener = TcpListener::bind("127.0.0.1:0")
1102 .await
1103 .unwrap();
1104 let addr = listener.local_addr().unwrap();
1105
1106 let json_response =
1107 std::fs::read_to_string("src/rfq/protocols/bebop/test_responses/aggregate_order.json")
1108 .unwrap();
1109
1110 tokio::spawn(async move {
1111 while let Ok((mut stream, _)) = listener.accept().await {
1112 let count_clone = request_count_clone.clone();
1113 let json_response_clone = json_response.clone();
1114 tokio::spawn(async move {
1115 *count_clone.lock().unwrap() += 1;
1116 let count = *count_clone.lock().unwrap();
1117 println!("Mock server: Received request #{count}");
1118
1119 if count <= 2 {
1120 let response = "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 21\r\n\r\nInternal Server Error";
1121 let _ = stream
1122 .write_all(response.as_bytes())
1123 .await;
1124 } else {
1125 let response = format!(
1126 "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
1127 json_response_clone.len(),
1128 json_response_clone
1129 );
1130 let _ = stream
1131 .write_all(response.as_bytes())
1132 .await;
1133 }
1134 let _ = stream.flush().await;
1135 let _ = stream.shutdown().await;
1136 });
1137 }
1138 });
1139 (addr, request_count)
1140 }
1141
1142 #[tokio::test]
1143 async fn test_bebop_quote_retry_on_bad_response() {
1144 let (addr, request_count) = create_retry_server().await;
1145
1146 let client = create_test_bebop_client(
1147 format!("http://127.0.0.1:{}/quote", addr.port()),
1148 Duration::from_secs(5),
1149 );
1150 let params = create_test_quote_params();
1151 let result = client
1152 .request_binding_quote(¶ms)
1153 .await;
1154
1155 assert!(result.is_ok(), "Expected success after retries, got: {:?}", result);
1156 let quote = result.unwrap();
1157
1158 assert_eq!(quote.amount_in, BigUint::from_str("20000000000").unwrap());
1160 assert_eq!(quote.amount_out, BigUint::from_str("21700473797683400419007").unwrap());
1161
1162 let final_count = *request_count.lock().unwrap();
1164 assert_eq!(final_count, 3, "Expected 3 requests, got {}", final_count);
1165 }
1166
1167 #[test]
1168 fn test_bebop_client_serialize_deserialize_roundtrip() {
1169 let token_in = Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap();
1170 let token_out = Bytes::from_str("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599").unwrap();
1171 let quote_token = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
1172
1173 let original = BebopClient {
1174 chain: Chain::Ethereum,
1175 price_ws: "wss://api.bebop.xyz/pricing".to_string(),
1176 quote_endpoint: "https://api.bebop.xyz/quote".to_string(),
1177 tokens: HashSet::from([token_in.clone(), token_out.clone()]),
1178 tvl: 50.5,
1179 ws_key: "secret_key".to_string(),
1180 quote_tokens: HashSet::from([quote_token.clone()]),
1181 quote_timeout: Duration::from_millis(5500),
1182 };
1183
1184 let serialized = serde_json::to_string(&original).unwrap();
1185 let deserialized: BebopClient = serde_json::from_str(&serialized).unwrap();
1186
1187 assert_eq!(deserialized.chain, original.chain);
1189 assert_eq!(deserialized.price_ws, original.price_ws);
1190 assert_eq!(deserialized.quote_endpoint, original.quote_endpoint);
1191 assert_eq!(deserialized.tokens, original.tokens);
1192 assert_eq!(deserialized.tvl, original.tvl);
1193 assert_eq!(deserialized.quote_tokens, original.quote_tokens);
1194 assert_eq!(deserialized.quote_timeout, original.quote_timeout);
1195
1196 assert_eq!(deserialized.ws_key, "");
1198 assert_ne!(deserialized.ws_key, original.ws_key);
1199 }
1200
1201 #[test]
1202 fn test_bebop_client_deserialize_with_credentials() {
1203 let json = r#"{
1206 "chain": "ethereum",
1207 "price_ws": "wss://api.bebop.xyz/pricing",
1208 "quote_endpoint": "https://api.bebop.xyz/quote",
1209 "tokens": [],
1210 "tvl": 10.0,
1211 "ws_key": "provided_key",
1212 "quote_tokens": [],
1213 "quote_timeout": {"secs": 30, "nanos": 0}
1214 }"#;
1215
1216 let client: BebopClient = serde_json::from_str(json).unwrap();
1217
1218 assert_eq!(client.ws_key, "provided_key");
1220 }
1221}