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 reconnect_attempts = 0;
284 const MAX_RECONNECT_ATTEMPTS: 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 reconnect_attempts = 0; connection
305 },
306 Err(e) => {
307 reconnect_attempts += 1;
308 error!("Failed to connect to Bebop WebSocket (attempt {}): {}", reconnect_attempts, e);
309
310 if reconnect_attempts >= MAX_RECONNECT_ATTEMPTS {
311 yield Err(RFQError::ConnectionError(format!("Failed to connect after {MAX_RECONNECT_ATTEMPTS} attempts: {e}")));
312 return;
313 }
314
315 let backoff_duration = Duration::from_secs(2_u64.pow(reconnect_attempts.min(5)));
316 info!("Retrying connection in {} seconds...", backoff_duration.as_secs());
317 sleep(backoff_duration).await;
318 continue;
319 }
320 };
321
322 let (_, mut ws_receiver) = ws_stream.split();
323
324 while let Some(msg) = ws_receiver.next().await {
326 match msg {
327 Ok(Message::Binary(data)) => {
328 match BebopPricingUpdate::decode(&data[..]) {
329 Ok(protobuf_update) => {
330 let mut new_components = HashMap::new();
331
332 for price_data in &protobuf_update.pairs {
334 let base_bytes = Bytes::from(price_data.base.clone());
335 let quote_bytes = Bytes::from(price_data.quote.clone());
336 if tokens.contains(&base_bytes) && tokens.contains("e_bytes) {
337 let pair_tokens = vec![
338 base_bytes.clone(), quote_bytes.clone()
339 ];
340
341 let mut quote_price_data: Option<&BebopPriceData> = None;
342 if !client.quote_tokens.contains("e_bytes) {
345 for approved_quote_token in &client.quote_tokens {
346 if let Some(quote_data) = protobuf_update.pairs.iter()
349 .find(|p| {
350 (p.base == quote_bytes.as_ref() && p.quote == approved_quote_token.as_ref()) ||
351 (p.quote == quote_bytes.as_ref() && p.base == approved_quote_token.as_ref())
352 }) {
353 quote_price_data = Some(quote_data);
354 break;
355 }
356 }
357
358 if quote_price_data.is_none() {
361 warn!("Quote token {} does not have price levels in approved quote token. Skipping.", hex::encode("e_bytes));
362 continue;
363 }
364 }
365
366 let tvl = price_data.calculate_tvl(quote_price_data);
367 if tvl < tvl_threshold {
368 continue;
369 }
370
371 let pair_str = format!("bebop_{}/{}", hex::encode(&base_bytes), hex::encode("e_bytes));
372 let component_id = format!("{}", keccak256(pair_str.as_bytes()));
373 let component_with_state = client.create_component_with_state(
374 component_id.clone(),
375 pair_tokens,
376 price_data,
377 tvl
378 );
379 new_components.insert(component_id, component_with_state);
380 }
381 }
382
383 let removed_components: HashMap<String, ProtocolComponent> = current_components
387 .iter()
388 .filter(|&(id, _)| !new_components.contains_key(id))
389 .map(|(k, v)| (k.clone(), v.component.clone()))
390 .collect();
391
392 current_components = new_components.clone();
394
395 let snapshot = Snapshot {
396 states: new_components,
397 vm_storage: HashMap::new(),
398 };
399 let timestamp = SystemTime::now().duration_since(
400 SystemTime::UNIX_EPOCH
401 ).map_err(
402 |_| RFQError::ParsingError("SystemTime before UNIX EPOCH!".into())
403 )?.as_secs();
404
405 let msg = StateSyncMessage::<TimestampHeader> {
406 header: TimestampHeader { timestamp },
407 snapshots: snapshot,
408 deltas: None, removed_components,
410 };
411
412 yield Ok(("bebop".to_string(), msg));
414 },
415 Err(e) => {
416 error!("Failed to parse protobuf message: {}", e);
417 break;
418 }
419 }
420 }
421 Ok(Message::Close(_)) => {
422 info!("WebSocket connection closed by server");
423 break;
424 }
425 Err(e) => {
426 error!("WebSocket error: {}", e);
427 break;
428 }
429 _ => {} }
431 }
432
433 reconnect_attempts += 1;
435 if reconnect_attempts >= MAX_RECONNECT_ATTEMPTS {
436 yield Err(RFQError::ConnectionError(format!("Connection failed after {MAX_RECONNECT_ATTEMPTS} attempts")));
437 return;
438 }
439
440 let backoff_duration = Duration::from_secs(2_u64.pow(reconnect_attempts.min(5)));
441 info!("Reconnecting in {} seconds (attempt {})...", backoff_duration.as_secs(), reconnect_attempts);
442 sleep(backoff_duration).await;
443 }
445 })
446 }
447
448 async fn request_binding_quote(
449 &self,
450 params: &GetAmountOutParams,
451 ) -> Result<SignedQuote, RFQError> {
452 let sell_token = bytes_to_address(¶ms.token_in)?.to_string();
453 let buy_token = bytes_to_address(¶ms.token_out)?.to_string();
454 let sell_amount = params.amount_in.to_string();
455 let sender = bytes_to_address(¶ms.sender)?.to_string();
456 let receiver = bytes_to_address(¶ms.receiver)?.to_string();
457
458 let url = self.quote_endpoint.clone();
459
460 let mut query = vec![
461 ("sell_tokens", sell_token),
462 ("buy_tokens", buy_token),
463 ("sell_amounts", sell_amount),
464 ("taker_address", sender),
465 ("receiver_address", receiver),
466 ("approval_type", "Standard".into()),
467 ("skip_validation", "true".into()),
468 ("skip_taker_checks", "true".into()),
469 ("gasless", "false".into()),
470 ("expiry_type", "standard".into()),
471 ("fee", "0".into()),
472 ("is_ui", "false".into()),
473 ];
474 if let Some(origin_address) = &self.origin_address {
475 query.push(("origin_address", bytes_to_address(origin_address)?.to_string()));
476 }
477 if let Some(origin_target) = &self.origin_target {
478 query.push(("origin_target", bytes_to_address(origin_target)?.to_string()));
479 }
480 if let Some(origin_source) = &self.origin_source {
481 query.push(("origin_source", origin_source.clone()));
482 }
483
484 let client = Client::new();
485
486 let start_time = std::time::Instant::now();
487 const MAX_RETRIES: u32 = 3;
488 let mut last_error = None;
489
490 for attempt in 0..MAX_RETRIES {
491 let elapsed = start_time.elapsed();
493 if elapsed >= self.quote_timeout {
494 return Err(last_error.unwrap_or_else(|| {
495 RFQError::ConnectionError(format!(
496 "Bebop quote request timed out after {} seconds",
497 self.quote_timeout.as_secs()
498 ))
499 }));
500 }
501
502 let remaining_time = self.quote_timeout - elapsed;
503
504 let request = client
505 .get(&url)
506 .query(&query)
507 .header("accept", "application/json")
508 .bearer_auth(&self.ws_key);
509
510 let response = match timeout(remaining_time, request.send()).await {
511 Ok(Ok(resp)) => resp,
512 Ok(Err(e)) => {
513 warn!(
514 "Bebop quote request failed (attempt {}/{}): {}",
515 attempt + 1,
516 MAX_RETRIES,
517 e
518 );
519 last_error = Some(RFQError::ConnectionError(format!(
520 "Failed to send Bebop quote request: {e}"
521 )));
522 if attempt < MAX_RETRIES - 1 {
523 continue;
524 } else {
525 return Err(last_error.unwrap());
526 }
527 }
528 Err(_) => {
529 return Err(RFQError::ConnectionError(format!(
530 "Bebop quote request timed out after {} seconds",
531 self.quote_timeout.as_secs()
532 )));
533 }
534 };
535
536 let quote_response = match response
537 .json::<BebopQuoteResponse>()
538 .await
539 {
540 Ok(resp) => resp,
541 Err(e) => {
542 warn!(
543 "Bebop quote response parsing failed (attempt {}/{}): {}",
544 attempt + 1,
545 MAX_RETRIES,
546 e
547 );
548 last_error = Some(RFQError::ParsingError(format!(
549 "Failed to parse Bebop quote response: {e}"
550 )));
551 if attempt < MAX_RETRIES - 1 {
552 sleep(Duration::from_millis(100)).await;
553 continue;
554 } else {
555 return Err(last_error.unwrap());
556 }
557 }
558 };
559
560 return Self::process_quote_response(quote_response, params);
561 }
562
563 Err(last_error.unwrap_or_else(|| {
564 RFQError::ConnectionError("Bebop quote request failed after retries".to_string())
565 }))
566 }
567}
568
569#[cfg(test)]
570mod tests {
571 use std::{
572 sync::{Arc, Mutex},
573 time::Duration,
574 };
575
576 use dotenv::dotenv;
577 use futures::SinkExt;
578 use tokio::{net::TcpListener, time::timeout};
579 use tokio_tungstenite::accept_async;
580
581 use super::*;
582 use crate::rfq::constants::get_bebop_auth;
583
584 const SWAP_SINGLE_SELECTOR: [u8; 4] = [0x4d, 0xce, 0xbc, 0xba];
586 const SWAP_AGGREGATE_SELECTOR: [u8; 4] = [0xa2, 0xf7, 0x48, 0x93];
588 const ROUTER_SWAP_SELECTOR: [u8; 4] = [0x95, 0x86, 0xd0, 0xe8];
590
591 #[tokio::test]
592 #[ignore] async fn test_bebop_websocket_connection() {
594 let wbtc = Bytes::from_str("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599").unwrap();
597 let weth = Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap();
598
599 dotenv().expect("Missing .env file");
600 let auth = get_bebop_auth().expect("Failed to get Bebop authentication");
601
602 let quote_tokens = HashSet::from([
603 Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(), Bytes::from_str("0xdac17f958d2ee523a2206206994597c13d831ec7").unwrap(), ]);
607
608 let client = BebopClient::new(
609 Chain::Ethereum,
610 HashSet::from_iter(vec![weth.clone(), wbtc.clone()]),
611 10.0, auth.key,
613 quote_tokens,
614 Duration::from_secs(30),
615 None,
616 None,
617 None,
618 )
619 .unwrap();
620
621 let mut stream = client.stream();
622
623 let result = timeout(Duration::from_secs(10), async {
628 let mut message_count = 0;
629 let max_messages = 1;
630
631 while let Some(result) = stream.next().await {
632 match result {
633 Ok((component_id, msg)) => {
634 println!("Received message with ID: {component_id}");
635
636 assert!(!component_id.is_empty());
637 assert_eq!(component_id, "bebop");
638 assert!(msg.header.timestamp > 0);
639 assert!(!msg.snapshots.states.is_empty());
640
641 let snapshot = &msg.snapshots;
642
643 assert!(!snapshot.states.is_empty());
645
646 println!("Received {} components in this message", snapshot.states.len());
647 for (id, component_with_state) in &snapshot.states {
648 assert_eq!(
649 component_with_state
650 .component
651 .protocol_system,
652 "rfq:bebop"
653 );
654 assert_eq!(
655 component_with_state
656 .component
657 .protocol_type_name,
658 "bebop_pool"
659 );
660 assert_eq!(component_with_state.component.chain, Chain::Ethereum);
661
662 let attributes = &component_with_state.state.attributes;
663
664 assert!(attributes.contains_key("bids"));
666 assert!(attributes.contains_key("asks"));
667 assert!(!attributes["bids"].is_empty());
668 assert!(!attributes["asks"].is_empty());
669
670 if let Some(tvl) = component_with_state.component_tvl {
671 assert!(tvl >= 0.0);
672 println!("Component {id} TVL: ${tvl:.2}");
673 }
674 }
675
676 message_count += 1;
677 if message_count >= max_messages {
678 break;
679 }
680 }
681 Err(e) => {
682 panic!("Stream error: {e}");
683 }
684 }
685 }
686
687 assert!(message_count > 0, "Should have received at least one message");
688 println!("Successfully received {message_count} messages");
689 })
690 .await;
691
692 match result {
693 Ok(_) => println!("Test completed successfully"),
694 Err(_) => panic!("Test timed out - no messages received within 10 seconds"),
695 }
696 }
697
698 #[tokio::test]
699 async fn test_websocket_reconnection() {
700 let listener = TcpListener::bind("127.0.0.1:0")
702 .await
703 .unwrap();
704 let addr = listener.local_addr().unwrap();
705
706 let connection_count = Arc::new(Mutex::new(0u32));
708
709 let connection_count_clone = connection_count.clone();
711
712 tokio::spawn(async move {
713 while let Ok((stream, _)) = listener.accept().await {
714 *connection_count_clone.lock().unwrap() += 1;
715 let count = *connection_count_clone.lock().unwrap();
716 println!("Mock server: Connection #{count} established");
717
718 tokio::spawn(async move {
719 if let Ok(ws_stream) = accept_async(stream).await {
720 let (mut ws_sender, _ws_receiver) = ws_stream.split();
721
722 let weth_addr =
724 hex::decode("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap();
725 let usdc_addr =
726 hex::decode("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap();
727
728 let test_price_data = BebopPriceData {
729 base: weth_addr,
730 quote: usdc_addr,
731 last_update_ts: 1752617378,
732 bids: vec![3070.05f32, 0.325717f32],
733 asks: vec![3070.527f32, 0.325717f32],
734 };
735
736 let pricing_update = BebopPricingUpdate { pairs: vec![test_price_data] };
737
738 let test_message = pricing_update.encode_to_vec();
739
740 if count == 1 {
741 println!("Mock server: Connection #1 - sending message then dropping.");
743 let _ = ws_sender
744 .send(Message::Binary(test_message.clone().into()))
745 .await;
746
747 tokio::time::sleep(Duration::from_millis(100)).await;
749 println!("Mock server: Dropping connection #1");
750 let _ = ws_sender.close().await;
751 } else if count == 2 {
752 println!("Mock server: Connection #2 - maintaining stable connection.");
754 let _ = ws_sender
755 .send(Message::Binary(test_message.clone().into()))
756 .await;
757 }
758 }
759 });
760 }
761 });
762
763 tokio::time::sleep(Duration::from_millis(50)).await;
765
766 let mut test_quote_tokens = HashSet::new();
767 test_quote_tokens
768 .insert(Bytes::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap());
769
770 let tokens_formatted = vec![
771 Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap(),
772 Bytes::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap(),
773 ];
774
775 let client = BebopClient {
777 chain: Chain::Ethereum,
778 price_ws: format!("ws://127.0.0.1:{}", addr.port()),
779 tokens: tokens_formatted.into_iter().collect(),
780 tvl: 1000.0,
781 ws_key: "test_key".to_string(),
782 quote_tokens: test_quote_tokens,
783 quote_endpoint: "".to_string(),
784 quote_timeout: Duration::from_secs(5),
785 origin_address: None,
786 origin_target: None,
787 origin_source: None,
788 };
789
790 let start_time = std::time::Instant::now();
791 let mut successful_messages = 0;
792 let mut connection_errors = 0;
793 let mut first_message_received = false;
794 let mut second_message_received = false;
795
796 while start_time.elapsed() < Duration::from_secs(5) && successful_messages < 2 {
803 match timeout(Duration::from_millis(1000), client.stream().next()).await {
804 Ok(Some(result)) => match result {
805 Ok((_component_id, _message)) => {
806 successful_messages += 1;
807 println!("Received successful message {successful_messages}");
808
809 if successful_messages == 1 {
810 first_message_received = true;
811 println!("First message received - connection should drop after this.");
812 } else if successful_messages == 2 {
813 second_message_received = true;
814 println!("Second message received after reconnection.");
815 }
816 }
817 Err(e) => {
818 connection_errors += 1;
819 println!("Connection error during reconnection: {e:?}");
820 }
821 },
822 Ok(None) => {
823 panic!("Stream ended unexpectedly");
824 }
825 Err(_) => {
826 println!("Timeout waiting for message (normal during reconnections)");
827 continue;
828 }
829 }
830 }
831
832 let final_connection_count = *connection_count.lock().unwrap();
833
834 assert_eq!(final_connection_count, 2);
838 assert!(first_message_received);
839 assert!(second_message_received);
840 assert_eq!(connection_errors, 0);
841 assert_eq!(successful_messages, 2);
842 }
843
844 #[tokio::test]
845 #[ignore] async fn test_bebop_quote_single_order() {
847 let token_in = Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap();
848 let token_out = Bytes::from_str("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599").unwrap();
849 dotenv().expect("Missing .env file");
850 let auth = get_bebop_auth().expect("Failed to get Bebop authentication");
851
852 let router = Bytes::from_str("0xfD0b31d2E955fA55e3fa641Fe90e08b677188d35").unwrap();
853
854 let client = BebopClient::new(
855 Chain::Ethereum,
856 HashSet::from_iter(vec![token_in.clone(), token_out.clone()]),
857 10.0, auth.key,
859 HashSet::new(),
860 Duration::from_secs(30),
861 Some(Bytes::from_str("0x00000000219ab540356cBB839Cbe05303d7705Fa").unwrap()),
862 Some(router.clone()),
863 Some("tycho-test".to_string()),
864 )
865 .unwrap();
866
867 let params = GetAmountOutParams {
868 amount_in: BigUint::from(1_000000000000000000u64),
869 token_in: token_in.clone(),
870 token_out: token_out.clone(),
871 sender: router.clone(),
872 receiver: router,
873 };
874 let quote = client
875 .request_binding_quote(¶ms)
876 .await
877 .unwrap();
878
879 assert_eq!(quote.base_token, token_in);
880 assert_eq!(quote.quote_token, token_out);
881 assert_eq!(quote.amount_in, BigUint::from(1_000000000000000000u64));
882
883 assert!(quote.amount_out > BigUint::from(1_000_000u64));
886
887 let selector = "e
891 .quote_attributes
892 .get("calldata")
893 .unwrap()[..4];
894 if selector == SWAP_SINGLE_SELECTOR {
895 let partial_fill_offset_slice = quote
896 .quote_attributes
897 .get("partial_fill_offset")
898 .unwrap()
899 .as_ref();
900 let mut partial_fill_offset_array = [0u8; 8];
901 partial_fill_offset_array.copy_from_slice(partial_fill_offset_slice);
902
903 assert_eq!(u64::from_be_bytes(partial_fill_offset_array), 12);
904 } else {
905 assert_eq!(selector, ROUTER_SWAP_SELECTOR);
906 }
907 }
908
909 #[tokio::test]
910 #[ignore] async fn test_bebop_quote_aggregate_order() {
912 let token_in = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
915 let token_out = Bytes::from_str("0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3").unwrap();
916 dotenv().expect("Missing .env file");
917 let auth = get_bebop_auth().expect("Failed to get Bebop authentication");
918
919 let router = Bytes::from_str("0xfD0b31d2E955fA55e3fa641Fe90e08b677188d35").unwrap();
920
921 let client = BebopClient::new(
922 Chain::Ethereum,
923 HashSet::from_iter(vec![token_in.clone(), token_out.clone()]),
924 10.0, auth.key,
926 HashSet::new(),
927 Duration::from_secs(30),
928 Some(Bytes::from_str("0x00000000219ab540356cBB839Cbe05303d7705Fa").unwrap()),
929 Some(router.clone()),
930 Some("tycho-test".to_string()),
931 )
932 .unwrap();
933
934 let amount_in = BigUint::from_str("20_000_000_000").unwrap(); let params = GetAmountOutParams {
936 amount_in: amount_in.clone(),
937 token_in: token_in.clone(),
938 token_out: token_out.clone(),
939 sender: router.clone(),
940 receiver: router,
941 };
942 let quote = client
943 .request_binding_quote(¶ms)
944 .await
945 .unwrap();
946
947 assert_eq!(quote.base_token, token_in);
948 assert_eq!(quote.quote_token, token_out);
949 assert_eq!(quote.amount_in, amount_in);
950
951 assert!(quote.amount_out > BigUint::from_str("18000000000000000000000").unwrap()); let selector = "e
958 .quote_attributes
959 .get("calldata")
960 .unwrap()[..4];
961 if selector == SWAP_AGGREGATE_SELECTOR {
962 let partial_fill_offset_slice = quote
963 .quote_attributes
964 .get("partial_fill_offset")
965 .unwrap()
966 .as_ref();
967 let mut partial_fill_offset_array = [0u8; 8];
968 partial_fill_offset_array.copy_from_slice(partial_fill_offset_slice);
969
970 assert_eq!(u64::from_be_bytes(partial_fill_offset_array), 2);
973 } else {
974 assert_eq!(selector, ROUTER_SWAP_SELECTOR);
975 }
976 }
977
978 #[test]
979 fn test_process_bebop_quote_response_aggregate_order() {
980 let json =
981 std::fs::read_to_string("src/rfq/protocols/bebop/test_responses/aggregate_order.json")
982 .unwrap();
983 let quote_response: BebopQuoteResponse = serde_json::from_str(&json).unwrap();
984 let params = GetAmountOutParams {
985 amount_in: BigUint::from_str("20000000000").unwrap(),
986 token_in: Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
987 token_out: Bytes::from_str("0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3").unwrap(),
988 sender: Bytes::from_str("0xfd0b31d2e955fa55e3fa641fe90e08b677188d35").unwrap(),
989 receiver: Bytes::from_str("0xfd0b31d2e955fa55e3fa641fe90e08b677188d35").unwrap(),
990 };
991 let res = BebopClient::process_quote_response(quote_response, ¶ms).unwrap();
992 assert_eq!(res.amount_out, BigUint::from_str("52571055094221715780641").unwrap());
993 assert_eq!(res.amount_in, BigUint::from_str("20000000000").unwrap());
994 assert_eq!(res.base_token, params.token_in);
995 assert_eq!(res.quote_token, params.token_out);
996 }
997
998 #[test]
999 fn test_process_bebop_quote_response_aggregate_order_with_multihop() {
1000 let json = std::fs::read_to_string(
1001 "src/rfq/protocols/bebop/test_responses/aggregate_order_with_multihop.json",
1002 )
1003 .unwrap();
1004 let quote_response: BebopQuoteResponse = serde_json::from_str(&json).unwrap();
1005 let params = GetAmountOutParams {
1006 amount_in: BigUint::from_str("43067495979235520920162").unwrap(),
1007 token_in: Bytes::from_str("0xDEf1CA1fb7FBcDC777520aa7f396b4E015F497aB").unwrap(),
1008 token_out: Bytes::from_str("0xdAC17F958D2ee523a2206206994597C13D831ec7").unwrap(),
1009 sender: Bytes::from_str("0x809305d724B6E79C71e10a097ABadd1274B9C279").unwrap(),
1010 receiver: Bytes::from_str("0x809305d724B6E79C71e10a097ABadd1274B9C279").unwrap(),
1011 };
1012 let res = BebopClient::process_quote_response(quote_response, ¶ms).unwrap();
1013 assert_eq!(res.amount_out, BigUint::from_str("11186653890").unwrap());
1014 assert_eq!(res.amount_in, BigUint::from_str("43067495979235520920162").unwrap());
1015 assert_eq!(res.base_token, params.token_in);
1016 assert_eq!(res.quote_token, params.token_out);
1017 }
1018
1019 #[test]
1020 fn test_process_bebop_quote_response_single_order() {
1021 let json =
1024 std::fs::read_to_string("src/rfq/protocols/bebop/test_responses/single_order.json")
1025 .unwrap();
1026 let quote_response: BebopQuoteResponse = serde_json::from_str(&json).unwrap();
1027 let router = Bytes::from_str("0xfD0b31d2E955fA55e3fa641Fe90e08b677188d35").unwrap();
1028 let params = GetAmountOutParams {
1029 amount_in: BigUint::from_str("1000000000000000000").unwrap(),
1030 token_in: Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap(),
1031 token_out: Bytes::from_str("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599").unwrap(),
1032 sender: router.clone(),
1033 receiver: router,
1034 };
1035 let res = BebopClient::process_quote_response(quote_response, ¶ms).unwrap();
1036 assert_eq!(res.amount_in, BigUint::from_str("1000000000000000000").unwrap());
1037 assert_eq!(res.amount_out, BigUint::from_str("2915408").unwrap());
1038 let settlement = Bytes::from_str("0xbbbbbBB520d69a9775E85b458C58c648259FAD5F").unwrap();
1039 assert_eq!(
1040 res.quote_attributes
1041 .get("tx_to")
1042 .unwrap(),
1043 &settlement
1044 );
1045 assert_eq!(
1046 res.quote_attributes
1047 .get("calldata")
1048 .unwrap()[..4],
1049 SWAP_SINGLE_SELECTOR
1050 );
1051 }
1052
1053 #[test]
1054 fn test_process_bebop_quote_response_single_order_router_mode() {
1055 let json = std::fs::read_to_string(
1059 "src/rfq/protocols/bebop/test_responses/single_order_router_mode.json",
1060 )
1061 .unwrap();
1062 let quote_response: BebopQuoteResponse = serde_json::from_str(&json).unwrap();
1063 let router = Bytes::from_str("0xfD0b31d2E955fA55e3fa641Fe90e08b677188d35").unwrap();
1064 let params = GetAmountOutParams {
1065 amount_in: BigUint::from_str("1000000000000000000").unwrap(),
1066 token_in: Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap(),
1067 token_out: Bytes::from_str("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599").unwrap(),
1068 sender: router.clone(),
1069 receiver: router,
1070 };
1071 let res = BebopClient::process_quote_response(quote_response, ¶ms).unwrap();
1072 assert_eq!(res.amount_in, BigUint::from_str("1000000000000000000").unwrap());
1073 assert_eq!(res.amount_out, BigUint::from_str("2926296").unwrap());
1074 let bebop_router = Bytes::from_str("0xBeb0009ACa35087ce7cCF11637E24dd1Aad3bf2A").unwrap();
1075 assert_eq!(
1076 res.quote_attributes
1077 .get("tx_to")
1078 .unwrap(),
1079 &bebop_router
1080 );
1081 assert_eq!(
1082 res.quote_attributes
1083 .get("calldata")
1084 .unwrap()[..4],
1085 ROUTER_SWAP_SELECTOR
1086 );
1087 }
1088
1089 async fn create_delayed_response_server(delay_ms: u64) -> std::net::SocketAddr {
1091 use tokio::io::AsyncWriteExt;
1092
1093 let listener = TcpListener::bind("127.0.0.1:0")
1094 .await
1095 .unwrap();
1096 let addr = listener.local_addr().unwrap();
1097
1098 let json_response =
1099 std::fs::read_to_string("src/rfq/protocols/bebop/test_responses/aggregate_order.json")
1100 .unwrap();
1101
1102 tokio::spawn(async move {
1103 while let Ok((mut stream, _)) = listener.accept().await {
1104 let json_response_clone = json_response.clone();
1105 tokio::spawn(async move {
1106 sleep(Duration::from_millis(delay_ms)).await;
1107
1108 let response = format!(
1109 "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
1110 json_response_clone.len(),
1111 json_response_clone
1112 );
1113 let _ = stream
1114 .write_all(response.as_bytes())
1115 .await;
1116 let _ = stream.flush().await;
1117 let _ = stream.shutdown().await;
1118 });
1119 }
1120 });
1121
1122 addr
1123 }
1124
1125 fn create_test_bebop_client(quote_endpoint: String, quote_timeout: Duration) -> BebopClient {
1126 let token_in = Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap();
1127 let token_out = Bytes::from_str("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599").unwrap();
1128
1129 BebopClient {
1130 chain: Chain::Ethereum,
1131 price_ws: "ws://example.com".to_string(),
1132 quote_endpoint,
1133 tokens: HashSet::from([token_in, token_out]),
1134 tvl: 10.0,
1135 ws_key: "test_key".to_string(),
1136 quote_tokens: HashSet::new(),
1137 quote_timeout,
1138 origin_address: None,
1139 origin_target: None,
1140 origin_source: None,
1141 }
1142 }
1143
1144 fn create_test_quote_params() -> GetAmountOutParams {
1146 let token_in = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
1147 let token_out = Bytes::from_str("0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3").unwrap();
1148 let router = Bytes::from_str("0xfD0b31d2E955fA55e3fa641Fe90e08b677188d35").unwrap();
1149
1150 GetAmountOutParams {
1151 amount_in: BigUint::from_str("20000000000").unwrap(),
1152 token_in,
1153 token_out,
1154 sender: router.clone(),
1155 receiver: router,
1156 }
1157 }
1158
1159 #[tokio::test]
1160 async fn test_bebop_quote_timeout() {
1161 let addr = create_delayed_response_server(500).await;
1162
1163 let client_short_timeout = create_test_bebop_client(
1165 format!("http://127.0.0.1:{}/quote", addr.port()),
1166 Duration::from_millis(200),
1167 );
1168 let params = create_test_quote_params();
1169
1170 let start = std::time::Instant::now();
1171 let result = client_short_timeout
1172 .request_binding_quote(¶ms)
1173 .await;
1174 let elapsed = start.elapsed();
1175
1176 assert!(result.is_err());
1177 let err = result.unwrap_err();
1178 match err {
1179 RFQError::ConnectionError(msg) => {
1180 assert!(msg.contains("timed out"), "Expected timeout error, got: {}", msg);
1181 }
1182 _ => panic!("Expected ConnectionError, got: {:?}", err),
1183 }
1184 assert!(
1185 elapsed.as_millis() >= 200 && elapsed.as_millis() < 400,
1186 "Expected timeout around 200ms, got: {:?}",
1187 elapsed
1188 );
1189
1190 let client_long_timeout = create_test_bebop_client(
1194 format!("http://127.0.0.1:{}/quote", addr.port()),
1195 Duration::from_secs(1),
1196 );
1197
1198 let result = client_long_timeout
1199 .request_binding_quote(¶ms)
1200 .await;
1201
1202 assert!(result.is_ok(), "Expected success, got: {:?}", result);
1204 let quote = result.unwrap();
1205
1206 assert_eq!(quote.base_token, params.token_in);
1208 assert_eq!(quote.quote_token, params.token_out);
1209 }
1210
1211 async fn create_retry_server() -> (std::net::SocketAddr, Arc<Mutex<u32>>) {
1214 use std::sync::{Arc, Mutex};
1215
1216 use tokio::io::AsyncWriteExt;
1217
1218 let request_count = Arc::new(Mutex::new(0u32));
1219 let request_count_clone = request_count.clone();
1220
1221 let listener = TcpListener::bind("127.0.0.1:0")
1222 .await
1223 .unwrap();
1224 let addr = listener.local_addr().unwrap();
1225
1226 let json_response =
1227 std::fs::read_to_string("src/rfq/protocols/bebop/test_responses/aggregate_order.json")
1228 .unwrap();
1229
1230 tokio::spawn(async move {
1231 while let Ok((mut stream, _)) = listener.accept().await {
1232 let count_clone = request_count_clone.clone();
1233 let json_response_clone = json_response.clone();
1234 tokio::spawn(async move {
1235 *count_clone.lock().unwrap() += 1;
1236 let count = *count_clone.lock().unwrap();
1237 println!("Mock server: Received request #{count}");
1238
1239 if count <= 2 {
1240 let response = "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 21\r\n\r\nInternal Server Error";
1241 let _ = stream
1242 .write_all(response.as_bytes())
1243 .await;
1244 } else {
1245 let response = format!(
1246 "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
1247 json_response_clone.len(),
1248 json_response_clone
1249 );
1250 let _ = stream
1251 .write_all(response.as_bytes())
1252 .await;
1253 }
1254 let _ = stream.flush().await;
1255 let _ = stream.shutdown().await;
1256 });
1257 }
1258 });
1259 (addr, request_count)
1260 }
1261
1262 #[tokio::test]
1263 async fn test_bebop_quote_retry_on_bad_response() {
1264 let (addr, request_count) = create_retry_server().await;
1265
1266 let client = create_test_bebop_client(
1267 format!("http://127.0.0.1:{}/quote", addr.port()),
1268 Duration::from_secs(5),
1269 );
1270 let params = create_test_quote_params();
1271 let result = client
1272 .request_binding_quote(¶ms)
1273 .await;
1274
1275 assert!(result.is_ok(), "Expected success after retries, got: {:?}", result);
1276 let quote = result.unwrap();
1277
1278 assert_eq!(quote.amount_in, BigUint::from_str("20000000000").unwrap());
1280 assert_eq!(quote.amount_out, BigUint::from_str("52571055094221715780641").unwrap());
1281
1282 let final_count = *request_count.lock().unwrap();
1284 assert_eq!(final_count, 3, "Expected 3 requests, got {}", final_count);
1285 }
1286
1287 #[test]
1288 fn test_bebop_client_serialize_deserialize_roundtrip() {
1289 let token_in = Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap();
1290 let token_out = Bytes::from_str("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599").unwrap();
1291 let quote_token = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
1292
1293 let original = BebopClient {
1294 chain: Chain::Ethereum,
1295 price_ws: "wss://api.bebop.xyz/pricing".to_string(),
1296 quote_endpoint: "https://api.bebop.xyz/quote".to_string(),
1297 tokens: HashSet::from([token_in.clone(), token_out.clone()]),
1298 tvl: 50.5,
1299 ws_key: "secret_key".to_string(),
1300 quote_tokens: HashSet::from([quote_token.clone()]),
1301 quote_timeout: Duration::from_millis(5500),
1302 origin_address: Some(
1303 Bytes::from_str("0x00000000219ab540356cBB839Cbe05303d7705Fa").unwrap(),
1304 ),
1305 origin_target: Some(
1306 Bytes::from_str("0xdA892C989d07A18B5DD3F392d949f00dF15C5736").unwrap(),
1307 ),
1308 origin_source: Some("tycho".to_string()),
1309 };
1310
1311 let serialized = serde_json::to_string(&original).unwrap();
1312 let deserialized: BebopClient = serde_json::from_str(&serialized).unwrap();
1313
1314 assert_eq!(deserialized.chain, original.chain);
1316 assert_eq!(deserialized.price_ws, original.price_ws);
1317 assert_eq!(deserialized.quote_endpoint, original.quote_endpoint);
1318 assert_eq!(deserialized.tokens, original.tokens);
1319 assert_eq!(deserialized.tvl, original.tvl);
1320 assert_eq!(deserialized.quote_tokens, original.quote_tokens);
1321 assert_eq!(deserialized.quote_timeout, original.quote_timeout);
1322 assert_eq!(deserialized.origin_address, original.origin_address);
1323 assert_eq!(deserialized.origin_target, original.origin_target);
1324 assert_eq!(deserialized.origin_source, original.origin_source);
1325
1326 assert_eq!(deserialized.ws_key, "");
1328 assert_ne!(deserialized.ws_key, original.ws_key);
1329 }
1330
1331 #[test]
1332 fn test_bebop_client_deserialize_with_credentials() {
1333 let json = r#"{
1336 "chain": "ethereum",
1337 "price_ws": "wss://api.bebop.xyz/pricing",
1338 "quote_endpoint": "https://api.bebop.xyz/quote",
1339 "tokens": [],
1340 "tvl": 10.0,
1341 "ws_key": "provided_key",
1342 "quote_tokens": [],
1343 "quote_timeout": {"secs": 30, "nanos": 0}
1344 }"#;
1345
1346 let client: BebopClient = serde_json::from_str(json).unwrap();
1347
1348 assert_eq!(client.ws_key, "provided_key");
1350 }
1351
1352 #[test]
1353 fn test_process_bebop_quote_response_aggregate_order_router_mode() {
1354 let json = std::fs::read_to_string(
1357 "src/rfq/protocols/bebop/test_responses/aggregate_order_router_mode.json",
1358 )
1359 .unwrap();
1360 let quote_response: BebopQuoteResponse = serde_json::from_str(&json).unwrap();
1361 let router = Bytes::from_str("0xfD0b31d2E955fA55e3fa641Fe90e08b677188d35").unwrap();
1362 let params = GetAmountOutParams {
1363 amount_in: BigUint::from_str("20000000000").unwrap(),
1364 token_in: Bytes::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap(),
1365 token_out: Bytes::from_str("0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3").unwrap(),
1366 sender: router.clone(),
1367 receiver: router,
1368 };
1369 let res = BebopClient::process_quote_response(quote_response, ¶ms).unwrap();
1370 assert_eq!(res.amount_in, BigUint::from_str("20000000000").unwrap());
1371 assert_eq!(res.amount_out, BigUint::from_str("52577858553072299423490").unwrap());
1372 let bebop_router = Bytes::from_str("0xBeb0009ACa35087ce7cCF11637E24dd1Aad3bf2A").unwrap();
1373 assert_eq!(
1374 res.quote_attributes
1375 .get("tx_to")
1376 .unwrap(),
1377 &bebop_router
1378 );
1379 assert_eq!(
1380 res.quote_attributes
1381 .get("calldata")
1382 .unwrap()[..4],
1383 ROUTER_SWAP_SELECTOR
1384 );
1385 }
1386}