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 Ok(BebopAllPairsState::new(books, tokens, client))
63 }
64}
65
66#[cfg(test)]
67mod tests {
68 use std::env;
69
70 use tycho_common::simulation::protocol_sim::ProtocolSim;
71
72 use super::*;
73 use crate::rfq::protocols::test_utils::{all_pairs_snapshot, decode, usdc, wbtc, weth};
74
75 fn snapshot() -> (ComponentWithState, HashMap<Bytes, Token>) {
76 env::set_var("BEBOP_KEY", "test_key");
77 let books = vec![
78 BebopPriceData {
79 base: wbtc().address.to_vec(),
80 quote: usdc().address.to_vec(),
81 last_update_ts: 1703097600,
82 bids: vec![65000.0, 1.5, 64950.0, 2.0, 64900.0, 0.5],
83 asks: vec![65100.0, 1.0, 65150.0, 2.5, 65200.0, 1.5],
84 },
85 BebopPriceData {
86 base: weth().address.to_vec(),
87 quote: usdc().address.to_vec(),
88 last_update_ts: 1703097600,
89 bids: vec![3000.0, 2.0],
90 asks: vec![3100.0, 1.5],
91 },
92 ];
93 all_pairs_snapshot("rfq:bebop", &[wbtc(), usdc(), weth()], &books)
94 }
95
96 #[tokio::test]
97 async fn test_decodes_books() {
98 let (snapshot, tokens) = snapshot();
99 let state = decode::<BebopAllPairsState>(snapshot, &tokens)
100 .await
101 .unwrap();
102
103 let wbtc_price = state
104 .spot_price(&wbtc(), &usdc())
105 .unwrap();
106 assert!((65000.0..=65100.0).contains(&wbtc_price), "{wbtc_price}");
107 let weth_price = state
108 .spot_price(&weth(), &usdc())
109 .unwrap();
110 assert!((3000.0..=3100.0).contains(&weth_price), "{weth_price}");
111 }
112
113 #[tokio::test]
114 async fn test_once_per_maker_attribute() {
115 let (mut snapshot, tokens) = snapshot();
116 snapshot
117 .component
118 .static_attributes
119 .insert(QuoteRule::ATTRIBUTE.to_string(), b"once_per_maker".into());
120 let result = decode::<BebopAllPairsState>(snapshot, &tokens).await;
121 assert!(
122 matches!(result.unwrap_err(), InvalidSnapshotError::ValueError(msg) if msg.contains("names no market maker"))
123 );
124 }
125}