tycho_simulation/rfq/protocols/bebop/
all_pairs_decoder.rs1use std::collections::HashMap;
2
3use tycho_client::feed::synchronizer::ComponentWithState;
4use tycho_common::{models::token::Token, Bytes};
5
6use super::{all_pairs_state::BebopAllPairsState, models::BebopPriceData};
7use crate::{
8 protocol::{
9 errors::InvalidSnapshotError,
10 models::{DecoderContext, TryFromWithBlock},
11 },
12 rfq::{
13 constants::{get_bebop_auth, get_bebop_origins},
14 models::{ComponentLayout, QuoteRule, TimestampHeader},
15 protocols::{
16 bebop::client_builder::BebopClientBuilder,
17 component::{decode_all_pairs_component, DecodedAllPairs},
18 },
19 },
20};
21
22impl TryFromWithBlock<ComponentWithState, TimestampHeader> for BebopAllPairsState {
23 type Error = InvalidSnapshotError;
24
25 async fn try_from_with_header(
26 snapshot: ComponentWithState,
27 _timestamp_header: TimestampHeader,
28 _account_balances: &HashMap<Bytes, HashMap<Bytes, Bytes>>,
29 all_tokens: &HashMap<Bytes, Token>,
30 _decoder_context: &DecoderContext,
31 ) -> Result<Self, Self::Error> {
32 let DecodedAllPairs { books, tokens, quote_rule } =
33 decode_all_pairs_component::<BebopPriceData>(&snapshot, all_tokens)?;
34 if quote_rule.is_some_and(|rule| rule != QuoteRule::OncePerVenue) {
35 return Err(InvalidSnapshotError::ValueError(
36 "Bebop names no market maker; its quote rule is once_per_venue".into(),
37 ));
38 }
39
40 let auth = get_bebop_auth().map_err(|e| {
41 InvalidSnapshotError::ValueError(format!("Failed to get Bebop authentication: {e}"))
42 })?;
43 let origins = get_bebop_origins().map_err(|e| {
44 InvalidSnapshotError::ValueError(format!("Failed to get Bebop origins: {e}"))
45 })?;
46 let mut builder = BebopClientBuilder::new(snapshot.component.chain, auth.key)
47 .tokens(tokens.keys().cloned().collect())
48 .component_layout(ComponentLayout::AllPairs);
49 if let Some(origin_address) = origins.address {
50 builder = builder.origin_address(origin_address);
51 }
52 if let Some(origin_target) = origins.target {
53 builder = builder.origin_target(origin_target);
54 }
55 if let Some(origin_source) = origins.source {
56 builder = builder.origin_source(origin_source);
57 }
58 let client = builder.build().map_err(|e| {
59 InvalidSnapshotError::ValueError(format!("Couldn't create BebopClient: {e}"))
60 })?;
61
62 BebopAllPairsState::new(books, tokens, client)
63 .map_err(|e| InvalidSnapshotError::ValueError(e.to_string()))
64 }
65}
66
67#[cfg(test)]
68mod tests {
69 use std::env;
70
71 use tycho_common::simulation::protocol_sim::ProtocolSim;
72
73 use super::*;
74 use crate::rfq::protocols::test_utils::{all_pairs_snapshot, decode, usdc, wbtc, weth};
75
76 fn snapshot() -> (ComponentWithState, HashMap<Bytes, Token>) {
77 env::set_var("BEBOP_KEY", "test_key");
78 let books = vec![
79 BebopPriceData {
80 base: wbtc().address.to_vec(),
81 quote: usdc().address.to_vec(),
82 last_update_ts: 1703097600,
83 bids: vec![65000.0, 1.5, 64950.0, 2.0, 64900.0, 0.5],
84 asks: vec![65100.0, 1.0, 65150.0, 2.5, 65200.0, 1.5],
85 },
86 BebopPriceData {
87 base: weth().address.to_vec(),
88 quote: usdc().address.to_vec(),
89 last_update_ts: 1703097600,
90 bids: vec![3000.0, 2.0],
91 asks: vec![3100.0, 1.5],
92 },
93 ];
94 all_pairs_snapshot("rfq:bebop", &[wbtc(), usdc(), weth()], &books)
95 }
96
97 #[tokio::test]
98 async fn test_decodes_books() {
99 let (snapshot, tokens) = snapshot();
100 let state = decode::<BebopAllPairsState>(snapshot, &tokens)
101 .await
102 .unwrap();
103
104 let wbtc_price = state
105 .spot_price(&wbtc(), &usdc())
106 .unwrap();
107 assert!((65000.0..=65100.0).contains(&wbtc_price), "{wbtc_price}");
108 let weth_price = state
109 .spot_price(&weth(), &usdc())
110 .unwrap();
111 assert!((3000.0..=3100.0).contains(&weth_price), "{weth_price}");
112 }
113
114 #[tokio::test]
115 async fn test_once_per_maker_attribute() {
116 let (mut snapshot, tokens) = snapshot();
117 snapshot
118 .component
119 .static_attributes
120 .insert(QuoteRule::ATTRIBUTE.to_string(), b"once_per_maker".into());
121 let result = decode::<BebopAllPairsState>(snapshot, &tokens).await;
122 assert!(
123 matches!(result.unwrap_err(), InvalidSnapshotError::ValueError(msg) if msg.contains("names no market maker"))
124 );
125 }
126}