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 origin_address: Option<Bytes>,
76 origin_target: Option<Bytes>,
78 origin_source: Option<String>,
80}
81
82impl BebopClient {
83 pub const PROTOCOL_SYSTEM: &'static str = "rfq:bebop";
84
85 #[allow(clippy::too_many_arguments)]
88 pub fn new(
89 chain: Chain,
90 tokens: HashSet<Bytes>,
91 tvl: f64,
92 ws_key: String,
93 quote_tokens: HashSet<Bytes>,
94 quote_timeout: Duration,
95 origin_address: Option<Bytes>,
96 origin_target: Option<Bytes>,
97 origin_source: Option<String>,
98 ) -> Result<Self, RFQError> {
99 let url = chain_to_bebop_url(chain)?;
100 Ok(Self {
101 price_ws: "wss://".to_string() + &url + "/pricing?format=protobuf",
102 quote_endpoint: "https://".to_string() + &url + "/quote",
103 tokens,
104 chain,
105 tvl,
106 ws_key,
107 quote_tokens,
108 quote_timeout,
109 origin_address,
110 origin_target,
111 origin_source,
112 })
113 }
114
115 fn create_component_with_state(
116 &self,
117 component_id: String,
118 tokens: Vec<tycho_common::Bytes>,
119 price_data: &BebopPriceData,
120 tvl: f64,
121 ) -> ComponentWithState {
122 let protocol_component = ProtocolComponent {
123 id: component_id.clone(),
124 protocol_system: Self::PROTOCOL_SYSTEM.to_string(),
125 protocol_type_name: "bebop_pool".to_string(),
126 chain: self.chain,
127 tokens,
128 contract_addresses: vec![], static_attributes: Default::default(),
130 change: Default::default(),
131 creation_tx: Default::default(),
132 created_at: Default::default(),
133 };
134
135 let mut attributes = HashMap::new();
136
137 if !price_data.bids.is_empty() {
141 let bids_pairs: Vec<(f32, f32)> = price_data
142 .bids
143 .as_chunks::<2>()
144 .0
145 .iter()
146 .map(|chunk| (chunk[0], chunk[1]))
147 .collect();
148 let bids_json = serde_json::to_string(&bids_pairs).unwrap_or_default();
149 attributes.insert("bids".to_string(), bids_json.as_bytes().to_vec().into());
150 }
151 if !price_data.asks.is_empty() {
152 let asks_pairs: Vec<(f32, f32)> = price_data
153 .asks
154 .as_chunks::<2>()
155 .0
156 .iter()
157 .map(|chunk| (chunk[0], chunk[1]))
158 .collect();
159 let asks_json = serde_json::to_string(&asks_pairs).unwrap_or_default();
160 attributes.insert("asks".to_string(), asks_json.as_bytes().to_vec().into());
161 }
162
163 ComponentWithState {
164 state: ProtocolComponentState::new(&component_id, attributes, HashMap::new()),
165 component: protocol_component,
166 component_tvl: Some(tvl),
167 entrypoints: vec![],
168 }
169 }
170
171 fn process_quote_response(
172 quote_response: BebopQuoteResponse,
173 params: &GetAmountOutParams,
174 ) -> Result<SignedQuote, RFQError> {
175 match quote_response {
176 BebopQuoteResponse::Success(quote) => {
177 quote.validate(params)?;
178
179 let mut quote_attributes: HashMap<String, Bytes> = HashMap::new();
180 quote_attributes.insert("tx_to".into(), quote.tx.to);
183 quote_attributes.insert("calldata".into(), quote.tx.data);
184 quote_attributes.insert(
185 "partial_fill_offset".into(),
186 Bytes::from(
187 quote
188 .partial_fill_offset
189 .to_be_bytes()
190 .to_vec(),
191 ),
192 );
193 let signed_quote = match quote.to_sign {
194 BebopOrderToSign::Single(ref single) => SignedQuote {
195 base_token: params.token_in.clone(),
196 quote_token: params.token_out.clone(),
197 amount_in: BigUint::from_str(&single.taker_amount).map_err(|_| {
198 RFQError::ParsingError(format!(
199 "Failed to parse amount in string: {}",
200 single.taker_amount
201 ))
202 })?,
203 amount_out: BigUint::from_str(&single.maker_amount).map_err(|_| {
204 RFQError::ParsingError(format!(
205 "Failed to parse amount out string: {}",
206 single.maker_amount
207 ))
208 })?,
209 quote_attributes,
210 },
211 BebopOrderToSign::Aggregate(aggregate) => {
212 let amount_in: BigUint = aggregate
214 .taker_tokens
215 .iter()
216 .zip(&aggregate.taker_amounts)
217 .flat_map(|(tokens, amounts)| {
218 tokens
219 .iter()
220 .zip(amounts)
221 .filter_map(|(token, amount)| {
222 if token == ¶ms.token_in {
223 BigUint::from_str(amount).ok()
224 } else {
225 None
226 }
227 })
228 })
229 .sum();
230
231 let amount_out: BigUint = aggregate
233 .maker_tokens
234 .iter()
235 .zip(&aggregate.maker_amounts)
236 .flat_map(|(tokens, amounts)| {
237 tokens
238 .iter()
239 .zip(amounts)
240 .filter_map(|(token, amount)| {
241 if token == ¶ms.token_out {
242 BigUint::from_str(amount).ok()
243 } else {
244 None
245 }
246 })
247 })
248 .sum();
249
250 SignedQuote {
251 base_token: params.token_in.clone(),
252 quote_token: params.token_out.clone(),
253 amount_in,
254 amount_out,
255 quote_attributes,
256 }
257 }
258 };
259
260 Ok(signed_quote)
261 }
262 BebopQuoteResponse::Error(err) => Err(RFQError::FatalError(format!(
263 "Bebop API error: code {} - {} (requestId: {})",
264 err.error.error_code, err.error.message, err.error.request_id
265 ))),
266 }
267 }
268}
269
270#[async_trait]
271impl RFQClient for BebopClient {
272 fn stream(
273 &self,
274 ) -> BoxStream<'static, Result<(String, StateSyncMessage<TimestampHeader>), RFQError>> {
275 let tokens = self.tokens.clone();
276 let url = self.price_ws.clone();
277 let tvl_threshold = self.tvl;
278 let authorization = format!("Bearer {}", self.ws_key);
279 let client = self.clone();
280
281 Box::pin(async_stream::stream! {
282 let mut current_components: HashMap<String, ComponentWithState> = HashMap::new();
283 let mut consecutive_failures = 0;
284 const MAX_CONSECUTIVE_FAILURES: u32 = 10;
285
286 loop {
287 let request = Request::builder()
288 .method("GET")
289 .uri(&url)
290 .header("Host", "api.bebop.xyz")
291 .header("Upgrade", "websocket")
292 .header("Connection", "Upgrade")
293 .header("Sec-WebSocket-Key", generate_key())
294 .header("Sec-WebSocket-Version", "13")
295 .header("Authorization", &authorization)
296 .body(())
297 .map_err(|_| RFQError::FatalError("Failed to build request".into()))?;
298
299 let (ws_stream, _) = match connect_async_with_config(request, None, false).await {
301 Ok(connection) => {
302 info!("Successfully connected to Bebop WebSocket");
303 connection
304 },
305 Err(e) => {
306 consecutive_failures += 1;
307 error!("Failed to connect to Bebop WebSocket (consecutive failure {}): {}", consecutive_failures, e);
308
309 if consecutive_failures >= MAX_CONSECUTIVE_FAILURES {
310 yield Err(RFQError::ConnectionError(format!("Failed to connect after {MAX_CONSECUTIVE_FAILURES} consecutive failures: {e}")));
311 return;
312 }
313
314 let backoff_duration = Duration::from_secs(2_u64.pow(consecutive_failures.min(5)));
315 info!("Retrying connection in {} seconds...", backoff_duration.as_secs());
316 sleep(backoff_duration).await;
317 continue;
318 }
319 };
320
321 let (_, mut ws_receiver) = ws_stream.split();
322
323 while let Some(msg) = ws_receiver.next().await {
325 match msg {
326 Ok(Message::Binary(data)) => {
327 match BebopPricingUpdate::decode(&data[..]) {
328 Ok(protobuf_update) => {
329 consecutive_failures = 0;
332
333 let mut new_components = HashMap::new();
334
335 for price_data in &protobuf_update.pairs {
337 let base_bytes = Bytes::from(price_data.base.clone());
338 let quote_bytes = Bytes::from(price_data.quote.clone());
339 if tokens.contains(&base_bytes) && tokens.contains("e_bytes) {
340 let pair_tokens = vec![
341 base_bytes.clone(), quote_bytes.clone()
342 ];
343
344 let mut quote_price_data: Option<&BebopPriceData> = None;
345 if !client.quote_tokens.contains("e_bytes) {
348 for approved_quote_token in &client.quote_tokens {
349 if let Some(quote_data) = protobuf_update.pairs.iter()
352 .find(|p| {
353 (p.base == quote_bytes.as_ref() && p.quote == approved_quote_token.as_ref()) ||
354 (p.quote == quote_bytes.as_ref() && p.base == approved_quote_token.as_ref())
355 }) {
356 quote_price_data = Some(quote_data);
357 break;
358 }
359 }
360
361 if quote_price_data.is_none() {
364 warn!("Quote token {} does not have price levels in approved quote token. Skipping.", hex::encode("e_bytes));
365 continue;
366 }
367 }
368
369 let tvl = price_data.calculate_tvl(quote_price_data);
370 if tvl < tvl_threshold {
371 continue;
372 }
373
374 let pair_str = format!("bebop_{}/{}", hex::encode(&base_bytes), hex::encode("e_bytes));
375 let component_id = format!("{}", keccak256(pair_str.as_bytes()));
376 let component_with_state = client.create_component_with_state(
377 component_id.clone(),
378 pair_tokens,
379 price_data,
380 tvl
381 );
382 new_components.insert(component_id, component_with_state);
383 }
384 }
385
386 let removed_components: HashMap<String, ProtocolComponent> = current_components
390 .iter()
391 .filter(|&(id, _)| !new_components.contains_key(id))
392 .map(|(k, v)| (k.clone(), v.component.clone()))
393 .collect();
394
395 current_components = new_components.clone();
397
398 let snapshot = Snapshot {
399 states: new_components,
400 vm_storage: HashMap::new(),
401 };
402 let timestamp = SystemTime::now().duration_since(
403 SystemTime::UNIX_EPOCH
404 ).map_err(
405 |_| RFQError::ParsingError("SystemTime before UNIX EPOCH!".into())
406 )?.as_secs();
407
408 let msg = StateSyncMessage::<TimestampHeader> {
409 header: TimestampHeader { timestamp },
410 snapshots: snapshot,
411 deltas: None, removed_components,
413 };
414
415 yield Ok(("bebop".to_string(), msg));
417 },
418 Err(e) => {
419 error!("Failed to parse protobuf message: {}", e);
420 break;
421 }
422 }
423 }
424 Ok(Message::Close(frame)) => {
425 match frame {
426 Some(frame) => warn!("WebSocket closed by server: {frame}"),
427 None => warn!("WebSocket closed by server without a close frame"),
428 }
429 break;
430 }
431 Err(e) => {
432 error!("WebSocket error: {}", e);
433 break;
434 }
435 _ => {} }
437 }
438
439 consecutive_failures += 1;
442 if consecutive_failures >= MAX_CONSECUTIVE_FAILURES {
443 yield Err(RFQError::ConnectionError(format!("No pricing data received after {MAX_CONSECUTIVE_FAILURES} consecutive failures")));
444 return;
445 }
446
447 let backoff_duration = Duration::from_secs(2_u64.pow(consecutive_failures.min(5)));
448 info!("Reconnecting in {} seconds (consecutive failure {})...", backoff_duration.as_secs(), consecutive_failures);
449 sleep(backoff_duration).await;
450 }
452 })
453 }
454
455 async fn request_binding_quote(
456 &self,
457 params: &GetAmountOutParams,
458 ) -> Result<SignedQuote, RFQError> {
459 let sell_token = bytes_to_address(¶ms.token_in)?.to_string();
460 let buy_token = bytes_to_address(¶ms.token_out)?.to_string();
461 let sell_amount = params.amount_in.to_string();
462 let sender = bytes_to_address(¶ms.sender)?.to_string();
463 let receiver = bytes_to_address(¶ms.receiver)?.to_string();
464
465 let url = self.quote_endpoint.clone();
466
467 let mut query = vec![
468 ("sell_tokens", sell_token),
469 ("buy_tokens", buy_token),
470 ("sell_amounts", sell_amount),
471 ("taker_address", sender),
472 ("receiver_address", receiver),
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 if let Some(origin_address) = &self.origin_address {
482 query.push(("origin_address", bytes_to_address(origin_address)?.to_string()));
483 }
484 if let Some(origin_target) = &self.origin_target {
485 query.push(("origin_target", bytes_to_address(origin_target)?.to_string()));
486 }
487 if let Some(origin_source) = &self.origin_source {
488 query.push(("origin_source", origin_source.clone()));
489 }
490
491 let client = Client::new();
492
493 let start_time = std::time::Instant::now();
494 const MAX_RETRIES: u32 = 3;
495 let mut last_error = None;
496
497 for attempt in 0..MAX_RETRIES {
498 let elapsed = start_time.elapsed();
500 if elapsed >= self.quote_timeout {
501 return Err(last_error.unwrap_or_else(|| {
502 RFQError::ConnectionError(format!(
503 "Bebop quote request timed out after {} seconds",
504 self.quote_timeout.as_secs()
505 ))
506 }));
507 }
508
509 let remaining_time = self.quote_timeout - elapsed;
510
511 let request = client
512 .get(&url)
513 .query(&query)
514 .header("accept", "application/json")
515 .bearer_auth(&self.ws_key);
516
517 let response = match timeout(remaining_time, request.send()).await {
518 Ok(Ok(resp)) => resp,
519 Ok(Err(e)) => {
520 warn!(
521 "Bebop quote request failed (attempt {}/{}): {}",
522 attempt + 1,
523 MAX_RETRIES,
524 e
525 );
526 last_error = Some(RFQError::ConnectionError(format!(
527 "Failed to send Bebop quote request: {e}"
528 )));
529 if attempt < MAX_RETRIES - 1 {
530 continue;
531 } else {
532 return Err(last_error.unwrap());
533 }
534 }
535 Err(_) => {
536 return Err(RFQError::ConnectionError(format!(
537 "Bebop quote request timed out after {} seconds",
538 self.quote_timeout.as_secs()
539 )));
540 }
541 };
542
543 let quote_response = match response
544 .json::<BebopQuoteResponse>()
545 .await
546 {
547 Ok(resp) => resp,
548 Err(e) => {
549 warn!(
550 "Bebop quote response parsing failed (attempt {}/{}): {}",
551 attempt + 1,
552 MAX_RETRIES,
553 e
554 );
555 last_error = Some(RFQError::ParsingError(format!(
556 "Failed to parse Bebop quote response: {e}"
557 )));
558 if attempt < MAX_RETRIES - 1 {
559 sleep(Duration::from_millis(100)).await;
560 continue;
561 } else {
562 return Err(last_error.unwrap());
563 }
564 }
565 };
566
567 return Self::process_quote_response(quote_response, params);
568 }
569
570 Err(last_error.unwrap_or_else(|| {
571 RFQError::ConnectionError("Bebop quote request failed after retries".to_string())
572 }))
573 }
574}
575
576#[cfg(test)]
577mod tests {
578 use std::{
579 sync::{Arc, Mutex},
580 time::Duration,
581 };
582
583 use dotenv::dotenv;
584 use futures::SinkExt;
585 use tokio::{net::TcpListener, time::timeout};
586 use tokio_tungstenite::accept_async;
587
588 use super::*;
589 use crate::rfq::constants::get_bebop_auth;
590
591 const SWAP_SINGLE_SELECTOR: [u8; 4] = [0x4d, 0xce, 0xbc, 0xba];
593 const SWAP_AGGREGATE_SELECTOR: [u8; 4] = [0xa2, 0xf7, 0x48, 0x93];
595 const ROUTER_SWAP_SELECTOR: [u8; 4] = [0x95, 0x86, 0xd0, 0xe8];
597
598 #[tokio::test]
599 #[ignore] async fn test_bebop_websocket_connection() {
601 let wbtc = Bytes::from_str("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599").unwrap();
604 let weth = Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap();
605
606 dotenv().expect("Missing .env file");
607 let auth = get_bebop_auth().expect("Failed to get Bebop authentication");
608
609 let quote_tokens = HashSet::from([
610 Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(), Bytes::from_str("0xdac17f958d2ee523a2206206994597c13d831ec7").unwrap(), ]);
614
615 let client = BebopClient::new(
616 Chain::Ethereum,
617 HashSet::from_iter(vec![weth.clone(), wbtc.clone()]),
618 10.0, auth.key,
620 quote_tokens,
621 Duration::from_secs(30),
622 None,
623 None,
624 None,
625 )
626 .unwrap();
627
628 let mut stream = client.stream();
629
630 let result = timeout(Duration::from_secs(10), async {
635 let mut message_count = 0;
636 let max_messages = 1;
637
638 while let Some(result) = stream.next().await {
639 match result {
640 Ok((component_id, msg)) => {
641 println!("Received message with ID: {component_id}");
642
643 assert!(!component_id.is_empty());
644 assert_eq!(component_id, "bebop");
645 assert!(msg.header.timestamp > 0);
646 assert!(!msg.snapshots.states.is_empty());
647
648 let snapshot = &msg.snapshots;
649
650 assert!(!snapshot.states.is_empty());
652
653 println!("Received {} components in this message", snapshot.states.len());
654 for (id, component_with_state) in &snapshot.states {
655 assert_eq!(
656 component_with_state
657 .component
658 .protocol_system,
659 "rfq:bebop"
660 );
661 assert_eq!(
662 component_with_state
663 .component
664 .protocol_type_name,
665 "bebop_pool"
666 );
667 assert_eq!(component_with_state.component.chain, Chain::Ethereum);
668
669 let attributes = &component_with_state.state.attributes;
670
671 assert!(attributes.contains_key("bids"));
673 assert!(attributes.contains_key("asks"));
674 assert!(!attributes["bids"].is_empty());
675 assert!(!attributes["asks"].is_empty());
676
677 if let Some(tvl) = component_with_state.component_tvl {
678 assert!(tvl >= 0.0);
679 println!("Component {id} TVL: ${tvl:.2}");
680 }
681 }
682
683 message_count += 1;
684 if message_count >= max_messages {
685 break;
686 }
687 }
688 Err(e) => {
689 panic!("Stream error: {e}");
690 }
691 }
692 }
693
694 assert!(message_count > 0, "Should have received at least one message");
695 println!("Successfully received {message_count} messages");
696 })
697 .await;
698
699 match result {
700 Ok(_) => println!("Test completed successfully"),
701 Err(_) => panic!("Test timed out - no messages received within 10 seconds"),
702 }
703 }
704
705 #[tokio::test]
706 async fn test_websocket_reconnection() {
707 let listener = TcpListener::bind("127.0.0.1:0")
709 .await
710 .unwrap();
711 let addr = listener.local_addr().unwrap();
712
713 let connection_count = Arc::new(Mutex::new(0u32));
715
716 let connection_count_clone = connection_count.clone();
718
719 tokio::spawn(async move {
720 while let Ok((stream, _)) = listener.accept().await {
721 *connection_count_clone.lock().unwrap() += 1;
722 let count = *connection_count_clone.lock().unwrap();
723 println!("Mock server: Connection #{count} established");
724
725 tokio::spawn(async move {
726 if let Ok(ws_stream) = accept_async(stream).await {
727 let (mut ws_sender, _ws_receiver) = ws_stream.split();
728
729 let weth_addr =
731 hex::decode("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap();
732 let usdc_addr =
733 hex::decode("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap();
734
735 let test_price_data = BebopPriceData {
736 base: weth_addr,
737 quote: usdc_addr,
738 last_update_ts: 1752617378,
739 bids: vec![3070.05f32, 0.325717f32],
740 asks: vec![3070.527f32, 0.325717f32],
741 };
742
743 let pricing_update = BebopPricingUpdate { pairs: vec![test_price_data] };
744
745 let test_message = pricing_update.encode_to_vec();
746
747 if count == 1 {
748 println!("Mock server: Connection #1 - sending message then dropping.");
750 let _ = ws_sender
751 .send(Message::Binary(test_message.clone().into()))
752 .await;
753
754 tokio::time::sleep(Duration::from_millis(100)).await;
756 println!("Mock server: Dropping connection #1");
757 let _ = ws_sender.close().await;
758 } else if count == 2 {
759 println!("Mock server: Connection #2 - maintaining stable connection.");
761 let _ = ws_sender
762 .send(Message::Binary(test_message.clone().into()))
763 .await;
764 }
765 }
766 });
767 }
768 });
769
770 tokio::time::sleep(Duration::from_millis(50)).await;
772
773 let mut test_quote_tokens = HashSet::new();
774 test_quote_tokens
775 .insert(Bytes::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap());
776
777 let tokens_formatted = vec![
778 Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap(),
779 Bytes::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap(),
780 ];
781
782 let client = BebopClient {
784 chain: Chain::Ethereum,
785 price_ws: format!("ws://127.0.0.1:{}", addr.port()),
786 tokens: tokens_formatted.into_iter().collect(),
787 tvl: 1000.0,
788 ws_key: "test_key".to_string(),
789 quote_tokens: test_quote_tokens,
790 quote_endpoint: "".to_string(),
791 quote_timeout: Duration::from_secs(5),
792 origin_address: None,
793 origin_target: None,
794 origin_source: None,
795 };
796
797 let start_time = std::time::Instant::now();
798 let mut successful_messages = 0;
799 let mut connection_errors = 0;
800 let mut first_message_received = false;
801 let mut second_message_received = false;
802
803 while start_time.elapsed() < Duration::from_secs(5) && successful_messages < 2 {
810 match timeout(Duration::from_millis(1000), client.stream().next()).await {
811 Ok(Some(result)) => match result {
812 Ok((_component_id, _message)) => {
813 successful_messages += 1;
814 println!("Received successful message {successful_messages}");
815
816 if successful_messages == 1 {
817 first_message_received = true;
818 println!("First message received - connection should drop after this.");
819 } else if successful_messages == 2 {
820 second_message_received = true;
821 println!("Second message received after reconnection.");
822 }
823 }
824 Err(e) => {
825 connection_errors += 1;
826 println!("Connection error during reconnection: {e:?}");
827 }
828 },
829 Ok(None) => {
830 panic!("Stream ended unexpectedly");
831 }
832 Err(_) => {
833 println!("Timeout waiting for message (normal during reconnections)");
834 continue;
835 }
836 }
837 }
838
839 let final_connection_count = *connection_count.lock().unwrap();
840
841 assert_eq!(final_connection_count, 2);
845 assert!(first_message_received);
846 assert!(second_message_received);
847 assert_eq!(connection_errors, 0);
848 assert_eq!(successful_messages, 2);
849 }
850
851 #[tokio::test]
852 #[ignore] async fn test_bebop_quote_single_order() {
854 let token_in = Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap();
855 let token_out = Bytes::from_str("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599").unwrap();
856 dotenv().expect("Missing .env file");
857 let auth = get_bebop_auth().expect("Failed to get Bebop authentication");
858
859 let router = Bytes::from_str("0xfD0b31d2E955fA55e3fa641Fe90e08b677188d35").unwrap();
860
861 let client = BebopClient::new(
862 Chain::Ethereum,
863 HashSet::from_iter(vec![token_in.clone(), token_out.clone()]),
864 10.0, auth.key,
866 HashSet::new(),
867 Duration::from_secs(30),
868 Some(Bytes::from_str("0x00000000219ab540356cBB839Cbe05303d7705Fa").unwrap()),
869 Some(router.clone()),
870 Some("tycho-test".to_string()),
871 )
872 .unwrap();
873
874 let params = GetAmountOutParams {
875 amount_in: BigUint::from(1_000000000000000000u64),
876 token_in: token_in.clone(),
877 token_out: token_out.clone(),
878 sender: router.clone(),
879 receiver: router,
880 };
881 let quote = client
882 .request_binding_quote(¶ms)
883 .await
884 .unwrap();
885
886 assert_eq!(quote.base_token, token_in);
887 assert_eq!(quote.quote_token, token_out);
888 assert_eq!(quote.amount_in, BigUint::from(1_000000000000000000u64));
889
890 assert!(quote.amount_out > BigUint::from(1_000_000u64));
893
894 let selector = "e
898 .quote_attributes
899 .get("calldata")
900 .unwrap()[..4];
901 if selector == SWAP_SINGLE_SELECTOR {
902 let partial_fill_offset_slice = quote
903 .quote_attributes
904 .get("partial_fill_offset")
905 .unwrap()
906 .as_ref();
907 let mut partial_fill_offset_array = [0u8; 8];
908 partial_fill_offset_array.copy_from_slice(partial_fill_offset_slice);
909
910 assert_eq!(u64::from_be_bytes(partial_fill_offset_array), 12);
911 } else {
912 assert_eq!(selector, ROUTER_SWAP_SELECTOR);
913 }
914 }
915
916 #[tokio::test]
917 #[ignore] async fn test_bebop_quote_aggregate_order() {
919 let token_in = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
922 let token_out = Bytes::from_str("0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3").unwrap();
923 dotenv().expect("Missing .env file");
924 let auth = get_bebop_auth().expect("Failed to get Bebop authentication");
925
926 let router = Bytes::from_str("0xfD0b31d2E955fA55e3fa641Fe90e08b677188d35").unwrap();
927
928 let client = BebopClient::new(
929 Chain::Ethereum,
930 HashSet::from_iter(vec![token_in.clone(), token_out.clone()]),
931 10.0, auth.key,
933 HashSet::new(),
934 Duration::from_secs(30),
935 Some(Bytes::from_str("0x00000000219ab540356cBB839Cbe05303d7705Fa").unwrap()),
936 Some(router.clone()),
937 Some("tycho-test".to_string()),
938 )
939 .unwrap();
940
941 let amount_in = BigUint::from_str("20_000_000_000").unwrap(); let params = GetAmountOutParams {
943 amount_in: amount_in.clone(),
944 token_in: token_in.clone(),
945 token_out: token_out.clone(),
946 sender: router.clone(),
947 receiver: router,
948 };
949 let quote = client
950 .request_binding_quote(¶ms)
951 .await
952 .unwrap();
953
954 assert_eq!(quote.base_token, token_in);
955 assert_eq!(quote.quote_token, token_out);
956 assert_eq!(quote.amount_in, amount_in);
957
958 assert!(quote.amount_out > BigUint::from_str("18000000000000000000000").unwrap()); let selector = "e
965 .quote_attributes
966 .get("calldata")
967 .unwrap()[..4];
968 if selector == SWAP_AGGREGATE_SELECTOR {
969 let partial_fill_offset_slice = quote
970 .quote_attributes
971 .get("partial_fill_offset")
972 .unwrap()
973 .as_ref();
974 let mut partial_fill_offset_array = [0u8; 8];
975 partial_fill_offset_array.copy_from_slice(partial_fill_offset_slice);
976
977 assert_eq!(u64::from_be_bytes(partial_fill_offset_array), 2);
980 } else {
981 assert_eq!(selector, ROUTER_SWAP_SELECTOR);
982 }
983 }
984
985 #[test]
986 fn test_process_bebop_quote_response_aggregate_order() {
987 let json =
988 std::fs::read_to_string("src/rfq/protocols/bebop/test_responses/aggregate_order.json")
989 .unwrap();
990 let quote_response: BebopQuoteResponse = serde_json::from_str(&json).unwrap();
991 let params = GetAmountOutParams {
992 amount_in: BigUint::from_str("20000000000").unwrap(),
993 token_in: Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
994 token_out: Bytes::from_str("0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3").unwrap(),
995 sender: Bytes::from_str("0xfd0b31d2e955fa55e3fa641fe90e08b677188d35").unwrap(),
996 receiver: Bytes::from_str("0xfd0b31d2e955fa55e3fa641fe90e08b677188d35").unwrap(),
997 };
998 let res = BebopClient::process_quote_response(quote_response, ¶ms).unwrap();
999 assert_eq!(res.amount_out, BigUint::from_str("52571055094221715780641").unwrap());
1000 assert_eq!(res.amount_in, BigUint::from_str("20000000000").unwrap());
1001 assert_eq!(res.base_token, params.token_in);
1002 assert_eq!(res.quote_token, params.token_out);
1003 }
1004
1005 #[test]
1006 fn test_process_bebop_quote_response_aggregate_order_with_multihop() {
1007 let json = std::fs::read_to_string(
1008 "src/rfq/protocols/bebop/test_responses/aggregate_order_with_multihop.json",
1009 )
1010 .unwrap();
1011 let quote_response: BebopQuoteResponse = serde_json::from_str(&json).unwrap();
1012 let params = GetAmountOutParams {
1013 amount_in: BigUint::from_str("43067495979235520920162").unwrap(),
1014 token_in: Bytes::from_str("0xDEf1CA1fb7FBcDC777520aa7f396b4E015F497aB").unwrap(),
1015 token_out: Bytes::from_str("0xdAC17F958D2ee523a2206206994597C13D831ec7").unwrap(),
1016 sender: Bytes::from_str("0x809305d724B6E79C71e10a097ABadd1274B9C279").unwrap(),
1017 receiver: Bytes::from_str("0x809305d724B6E79C71e10a097ABadd1274B9C279").unwrap(),
1018 };
1019 let res = BebopClient::process_quote_response(quote_response, ¶ms).unwrap();
1020 assert_eq!(res.amount_out, BigUint::from_str("11186653890").unwrap());
1021 assert_eq!(res.amount_in, BigUint::from_str("43067495979235520920162").unwrap());
1022 assert_eq!(res.base_token, params.token_in);
1023 assert_eq!(res.quote_token, params.token_out);
1024 }
1025
1026 #[test]
1027 fn test_process_bebop_quote_response_single_order() {
1028 let json =
1031 std::fs::read_to_string("src/rfq/protocols/bebop/test_responses/single_order.json")
1032 .unwrap();
1033 let quote_response: BebopQuoteResponse = serde_json::from_str(&json).unwrap();
1034 let router = Bytes::from_str("0xfD0b31d2E955fA55e3fa641Fe90e08b677188d35").unwrap();
1035 let params = GetAmountOutParams {
1036 amount_in: BigUint::from_str("1000000000000000000").unwrap(),
1037 token_in: Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap(),
1038 token_out: Bytes::from_str("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599").unwrap(),
1039 sender: router.clone(),
1040 receiver: router,
1041 };
1042 let res = BebopClient::process_quote_response(quote_response, ¶ms).unwrap();
1043 assert_eq!(res.amount_in, BigUint::from_str("1000000000000000000").unwrap());
1044 assert_eq!(res.amount_out, BigUint::from_str("2915408").unwrap());
1045 let settlement = Bytes::from_str("0xbbbbbBB520d69a9775E85b458C58c648259FAD5F").unwrap();
1046 assert_eq!(
1047 res.quote_attributes
1048 .get("tx_to")
1049 .unwrap(),
1050 &settlement
1051 );
1052 assert_eq!(
1053 res.quote_attributes
1054 .get("calldata")
1055 .unwrap()[..4],
1056 SWAP_SINGLE_SELECTOR
1057 );
1058 }
1059
1060 #[test]
1061 fn test_process_bebop_quote_response_single_order_router_mode() {
1062 let json = std::fs::read_to_string(
1066 "src/rfq/protocols/bebop/test_responses/single_order_router_mode.json",
1067 )
1068 .unwrap();
1069 let quote_response: BebopQuoteResponse = serde_json::from_str(&json).unwrap();
1070 let router = Bytes::from_str("0xfD0b31d2E955fA55e3fa641Fe90e08b677188d35").unwrap();
1071 let params = GetAmountOutParams {
1072 amount_in: BigUint::from_str("1000000000000000000").unwrap(),
1073 token_in: Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap(),
1074 token_out: Bytes::from_str("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599").unwrap(),
1075 sender: router.clone(),
1076 receiver: router,
1077 };
1078 let res = BebopClient::process_quote_response(quote_response, ¶ms).unwrap();
1079 assert_eq!(res.amount_in, BigUint::from_str("1000000000000000000").unwrap());
1080 assert_eq!(res.amount_out, BigUint::from_str("2926296").unwrap());
1081 let bebop_router = Bytes::from_str("0xBeb0009ACa35087ce7cCF11637E24dd1Aad3bf2A").unwrap();
1082 assert_eq!(
1083 res.quote_attributes
1084 .get("tx_to")
1085 .unwrap(),
1086 &bebop_router
1087 );
1088 assert_eq!(
1089 res.quote_attributes
1090 .get("calldata")
1091 .unwrap()[..4],
1092 ROUTER_SWAP_SELECTOR
1093 );
1094 }
1095
1096 async fn create_delayed_response_server(delay_ms: u64) -> std::net::SocketAddr {
1098 use tokio::io::AsyncWriteExt;
1099
1100 let listener = TcpListener::bind("127.0.0.1:0")
1101 .await
1102 .unwrap();
1103 let addr = listener.local_addr().unwrap();
1104
1105 let json_response =
1106 std::fs::read_to_string("src/rfq/protocols/bebop/test_responses/aggregate_order.json")
1107 .unwrap();
1108
1109 tokio::spawn(async move {
1110 while let Ok((mut stream, _)) = listener.accept().await {
1111 let json_response_clone = json_response.clone();
1112 tokio::spawn(async move {
1113 sleep(Duration::from_millis(delay_ms)).await;
1114
1115 let response = format!(
1116 "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
1117 json_response_clone.len(),
1118 json_response_clone
1119 );
1120 let _ = stream
1121 .write_all(response.as_bytes())
1122 .await;
1123 let _ = stream.flush().await;
1124 let _ = stream.shutdown().await;
1125 });
1126 }
1127 });
1128
1129 addr
1130 }
1131
1132 fn create_test_bebop_client(quote_endpoint: String, quote_timeout: Duration) -> BebopClient {
1133 let token_in = Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap();
1134 let token_out = Bytes::from_str("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599").unwrap();
1135
1136 BebopClient {
1137 chain: Chain::Ethereum,
1138 price_ws: "ws://example.com".to_string(),
1139 quote_endpoint,
1140 tokens: HashSet::from([token_in, token_out]),
1141 tvl: 10.0,
1142 ws_key: "test_key".to_string(),
1143 quote_tokens: HashSet::new(),
1144 quote_timeout,
1145 origin_address: None,
1146 origin_target: None,
1147 origin_source: None,
1148 }
1149 }
1150
1151 fn create_test_quote_params() -> GetAmountOutParams {
1153 let token_in = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
1154 let token_out = Bytes::from_str("0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3").unwrap();
1155 let router = Bytes::from_str("0xfD0b31d2E955fA55e3fa641Fe90e08b677188d35").unwrap();
1156
1157 GetAmountOutParams {
1158 amount_in: BigUint::from_str("20000000000").unwrap(),
1159 token_in,
1160 token_out,
1161 sender: router.clone(),
1162 receiver: router,
1163 }
1164 }
1165
1166 #[tokio::test]
1167 async fn test_bebop_quote_timeout() {
1168 let addr = create_delayed_response_server(500).await;
1169
1170 let client_short_timeout = create_test_bebop_client(
1172 format!("http://127.0.0.1:{}/quote", addr.port()),
1173 Duration::from_millis(200),
1174 );
1175 let params = create_test_quote_params();
1176
1177 let start = std::time::Instant::now();
1178 let result = client_short_timeout
1179 .request_binding_quote(¶ms)
1180 .await;
1181 let elapsed = start.elapsed();
1182
1183 assert!(result.is_err());
1184 let err = result.unwrap_err();
1185 match err {
1186 RFQError::ConnectionError(msg) => {
1187 assert!(msg.contains("timed out"), "Expected timeout error, got: {}", msg);
1188 }
1189 _ => panic!("Expected ConnectionError, got: {:?}", err),
1190 }
1191 assert!(
1192 elapsed.as_millis() >= 200 && elapsed.as_millis() < 400,
1193 "Expected timeout around 200ms, got: {:?}",
1194 elapsed
1195 );
1196
1197 let client_long_timeout = create_test_bebop_client(
1201 format!("http://127.0.0.1:{}/quote", addr.port()),
1202 Duration::from_secs(1),
1203 );
1204
1205 let result = client_long_timeout
1206 .request_binding_quote(¶ms)
1207 .await;
1208
1209 assert!(result.is_ok(), "Expected success, got: {:?}", result);
1211 let quote = result.unwrap();
1212
1213 assert_eq!(quote.base_token, params.token_in);
1215 assert_eq!(quote.quote_token, params.token_out);
1216 }
1217
1218 async fn create_retry_server() -> (std::net::SocketAddr, Arc<Mutex<u32>>) {
1221 use std::sync::{Arc, Mutex};
1222
1223 use tokio::io::AsyncWriteExt;
1224
1225 let request_count = Arc::new(Mutex::new(0u32));
1226 let request_count_clone = request_count.clone();
1227
1228 let listener = TcpListener::bind("127.0.0.1:0")
1229 .await
1230 .unwrap();
1231 let addr = listener.local_addr().unwrap();
1232
1233 let json_response =
1234 std::fs::read_to_string("src/rfq/protocols/bebop/test_responses/aggregate_order.json")
1235 .unwrap();
1236
1237 tokio::spawn(async move {
1238 while let Ok((mut stream, _)) = listener.accept().await {
1239 let count_clone = request_count_clone.clone();
1240 let json_response_clone = json_response.clone();
1241 tokio::spawn(async move {
1242 *count_clone.lock().unwrap() += 1;
1243 let count = *count_clone.lock().unwrap();
1244 println!("Mock server: Received request #{count}");
1245
1246 if count <= 2 {
1247 let response = "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 21\r\n\r\nInternal Server Error";
1248 let _ = stream
1249 .write_all(response.as_bytes())
1250 .await;
1251 } else {
1252 let response = format!(
1253 "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
1254 json_response_clone.len(),
1255 json_response_clone
1256 );
1257 let _ = stream
1258 .write_all(response.as_bytes())
1259 .await;
1260 }
1261 let _ = stream.flush().await;
1262 let _ = stream.shutdown().await;
1263 });
1264 }
1265 });
1266 (addr, request_count)
1267 }
1268
1269 #[tokio::test]
1270 async fn test_bebop_quote_retry_on_bad_response() {
1271 let (addr, request_count) = create_retry_server().await;
1272
1273 let client = create_test_bebop_client(
1274 format!("http://127.0.0.1:{}/quote", addr.port()),
1275 Duration::from_secs(5),
1276 );
1277 let params = create_test_quote_params();
1278 let result = client
1279 .request_binding_quote(¶ms)
1280 .await;
1281
1282 assert!(result.is_ok(), "Expected success after retries, got: {:?}", result);
1283 let quote = result.unwrap();
1284
1285 assert_eq!(quote.amount_in, BigUint::from_str("20000000000").unwrap());
1287 assert_eq!(quote.amount_out, BigUint::from_str("52571055094221715780641").unwrap());
1288
1289 let final_count = *request_count.lock().unwrap();
1291 assert_eq!(final_count, 3, "Expected 3 requests, got {}", final_count);
1292 }
1293
1294 #[test]
1295 fn test_bebop_client_serialize_deserialize_roundtrip() {
1296 let token_in = Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap();
1297 let token_out = Bytes::from_str("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599").unwrap();
1298 let quote_token = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
1299
1300 let original = BebopClient {
1301 chain: Chain::Ethereum,
1302 price_ws: "wss://api.bebop.xyz/pricing".to_string(),
1303 quote_endpoint: "https://api.bebop.xyz/quote".to_string(),
1304 tokens: HashSet::from([token_in.clone(), token_out.clone()]),
1305 tvl: 50.5,
1306 ws_key: "secret_key".to_string(),
1307 quote_tokens: HashSet::from([quote_token.clone()]),
1308 quote_timeout: Duration::from_millis(5500),
1309 origin_address: Some(
1310 Bytes::from_str("0x00000000219ab540356cBB839Cbe05303d7705Fa").unwrap(),
1311 ),
1312 origin_target: Some(
1313 Bytes::from_str("0xdA892C989d07A18B5DD3F392d949f00dF15C5736").unwrap(),
1314 ),
1315 origin_source: Some("tycho".to_string()),
1316 };
1317
1318 let serialized = serde_json::to_string(&original).unwrap();
1319 let deserialized: BebopClient = serde_json::from_str(&serialized).unwrap();
1320
1321 assert_eq!(deserialized.chain, original.chain);
1323 assert_eq!(deserialized.price_ws, original.price_ws);
1324 assert_eq!(deserialized.quote_endpoint, original.quote_endpoint);
1325 assert_eq!(deserialized.tokens, original.tokens);
1326 assert_eq!(deserialized.tvl, original.tvl);
1327 assert_eq!(deserialized.quote_tokens, original.quote_tokens);
1328 assert_eq!(deserialized.quote_timeout, original.quote_timeout);
1329 assert_eq!(deserialized.origin_address, original.origin_address);
1330 assert_eq!(deserialized.origin_target, original.origin_target);
1331 assert_eq!(deserialized.origin_source, original.origin_source);
1332
1333 assert_eq!(deserialized.ws_key, "");
1335 assert_ne!(deserialized.ws_key, original.ws_key);
1336 }
1337
1338 #[test]
1339 fn test_bebop_client_deserialize_with_credentials() {
1340 let json = r#"{
1343 "chain": "ethereum",
1344 "price_ws": "wss://api.bebop.xyz/pricing",
1345 "quote_endpoint": "https://api.bebop.xyz/quote",
1346 "tokens": [],
1347 "tvl": 10.0,
1348 "ws_key": "provided_key",
1349 "quote_tokens": [],
1350 "quote_timeout": {"secs": 30, "nanos": 0}
1351 }"#;
1352
1353 let client: BebopClient = serde_json::from_str(json).unwrap();
1354
1355 assert_eq!(client.ws_key, "provided_key");
1357 }
1358
1359 #[test]
1360 fn test_process_bebop_quote_response_aggregate_order_router_mode() {
1361 let json = std::fs::read_to_string(
1364 "src/rfq/protocols/bebop/test_responses/aggregate_order_router_mode.json",
1365 )
1366 .unwrap();
1367 let quote_response: BebopQuoteResponse = serde_json::from_str(&json).unwrap();
1368 let router = Bytes::from_str("0xfD0b31d2E955fA55e3fa641Fe90e08b677188d35").unwrap();
1369 let params = GetAmountOutParams {
1370 amount_in: BigUint::from_str("20000000000").unwrap(),
1371 token_in: Bytes::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap(),
1372 token_out: Bytes::from_str("0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3").unwrap(),
1373 sender: router.clone(),
1374 receiver: router,
1375 };
1376 let res = BebopClient::process_quote_response(quote_response, ¶ms).unwrap();
1377 assert_eq!(res.amount_in, BigUint::from_str("20000000000").unwrap());
1378 assert_eq!(res.amount_out, BigUint::from_str("52577858553072299423490").unwrap());
1379 let bebop_router = Bytes::from_str("0xBeb0009ACa35087ce7cCF11637E24dd1Aad3bf2A").unwrap();
1380 assert_eq!(
1381 res.quote_attributes
1382 .get("tx_to")
1383 .unwrap(),
1384 &bebop_router
1385 );
1386 assert_eq!(
1387 res.quote_attributes
1388 .get("calldata")
1389 .unwrap()[..4],
1390 ROUTER_SWAP_SELECTOR
1391 );
1392 }
1393}