1use std::{collections::HashMap, env, fmt, time::Duration};
2
3use tokio_stream::Stream;
4use tracing::{info, warn};
5use tycho_simulation::{
6 evm::{
7 engine_db::tycho_db::PreCachedDB,
8 protocol::{
9 aerodrome_slipstreams::state::AerodromeSlipstreamsState,
10 aerodrome_v1::state::AerodromeV1State,
11 curve::CurveState,
12 ekubo::state::EkuboState,
13 ekubo_v3::state::EkuboV3State,
14 erc4626::state::ERC4626State,
15 filters::{
16 balancer_v2_pool_filter, curve_filter, ekubo_v3_extension_filter,
17 ekubo_v3_extension_filter_with_signed_exclusive_swap, erc4626_filter,
18 fluid_v1_paused_pools_filter,
19 },
20 fluid::FluidV1,
21 lunarbase::state::LunarBaseState,
22 pancakeswap_v2::state::PancakeswapV2State,
23 uniswap_v2::state::UniswapV2State,
24 uniswap_v3::state::UniswapV3State,
25 uniswap_v4::state::UniswapV4State,
26 vm::state::EVMPoolState,
27 },
28 stream::ProtocolStreamBuilder,
29 tycho_models::Chain,
30 },
31 price_level_stream::{config::default_served_pamms, stream::PriceLevelStreamBuilder},
32 protocol::models::Update,
33 rfq::{
34 protocols::{
35 bebop::{client_builder::BebopClientBuilder, state::BebopState},
36 hashflow::{client_builder::HashflowClientBuilder, state::HashflowState},
37 },
38 stream::RFQStreamBuilder,
39 },
40 tycho_client::feed::component_tracker::ComponentFilter,
41 tycho_common::models::token::Token,
42 tycho_core::Bytes,
43};
44
45use super::DataFeedError;
46
47const EXCLUSIVE_PREFIX: &str = "exclusive:";
51
52const EXCLUSIVE_CAPABLE_PROTOCOLS: &[&str] = &["ekubo_v3"];
55
56const PRICE_LEVEL_STREAM_PREFIX: &str = "pricelevelstream:";
59
60const PROPAMM_FALLBACK_PREFIX: &str = "propammfallback:";
68
69const RFQ_PREFIX: &str = "rfq:";
72
73pub const EXCLUDE_PREFIX: &str = "exclude:";
76
77const PRICE_LEVEL_STREAM_CHAIN: Chain = Chain::Ethereum;
83
84pub(crate) fn has_tycho_protocols(protocols: &[String]) -> bool {
89 protocols.iter().any(|protocol| {
90 !protocol.starts_with(RFQ_PREFIX) && !protocol.starts_with(PRICE_LEVEL_STREAM_PREFIX)
91 })
92}
93
94pub fn matches_streamed_system(entry: &str, protocol_system: &str) -> bool {
100 if entry == protocol_system {
101 return true;
102 }
103 match (
104 entry.strip_prefix(PRICE_LEVEL_STREAM_PREFIX),
105 protocol_system.strip_prefix(PROPAMM_FALLBACK_PREFIX),
106 ) {
107 (Some(requested_venue), Some(streamed_venue)) => requested_venue == streamed_venue,
108 _ => false,
109 }
110}
111
112pub(crate) fn has_rfq_protocols(protocols: &[String]) -> bool {
114 protocols
115 .iter()
116 .any(|protocol| protocol.starts_with(RFQ_PREFIX))
117}
118
119#[derive(Debug, thiserror::Error)]
121#[error(
122 "protocol '{requested}' has no exclusive-liquidity variant; '{EXCLUSIVE_PREFIX}' is only \
123 supported for: {supported}",
124 supported = EXCLUSIVE_CAPABLE_PROTOCOLS.join(", ")
125)]
126pub struct UnsupportedExclusiveProtocol {
127 requested: String,
129}
130
131#[derive(Debug, Clone, PartialEq, Eq)]
136pub struct ProtocolSpec {
137 pub system: String,
139 pub exclusive: bool,
141}
142
143impl ProtocolSpec {
144 pub fn public(system: impl Into<String>) -> Self {
146 Self { system: system.into(), exclusive: false }
147 }
148
149 pub fn parse(entry: &str) -> Result<Self, UnsupportedExclusiveProtocol> {
157 let Some(system) = entry.strip_prefix(EXCLUSIVE_PREFIX) else {
158 return Ok(Self::public(entry));
159 };
160 if !EXCLUSIVE_CAPABLE_PROTOCOLS.contains(&system) {
161 return Err(UnsupportedExclusiveProtocol { requested: system.to_string() });
162 }
163 Ok(Self { system: system.to_string(), exclusive: true })
164 }
165}
166
167pub fn parse_exclusion(entry: &str) -> Option<Result<String, UnsupportedExclusiveProtocol>> {
180 let excluded = entry.strip_prefix(EXCLUDE_PREFIX)?;
181 Some(ProtocolSpec::parse(excluded).map(|protocol| protocol.system))
182}
183
184impl fmt::Display for ProtocolSpec {
185 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186 if self.exclusive {
187 write!(f, "{EXCLUSIVE_PREFIX}{}", self.system)
188 } else {
189 f.write_str(&self.system)
190 }
191 }
192}
193
194#[cfg(feature = "test-utils")]
199pub fn register_exchanges_for_recording(
200 builder: ProtocolStreamBuilder,
201 tvl_filter: ComponentFilter,
202 entries: &[String],
203) -> Result<ProtocolStreamBuilder, String> {
204 register_exchanges(builder, tvl_filter, entries).map_err(|e| e.to_string())
205}
206
207fn parse_protocols(entries: &[String]) -> Result<Vec<ProtocolSpec>, DataFeedError> {
214 let mut protocols = Vec::with_capacity(entries.len());
215 for entry in entries {
216 protocols
217 .push(ProtocolSpec::parse(entry).map_err(|e| DataFeedError::Config(e.to_string()))?);
218 }
219
220 let mut variants: HashMap<&str, bool> = HashMap::new();
221 for protocol in &protocols {
222 if variants
223 .insert(protocol.system.as_str(), protocol.exclusive)
224 .is_some_and(|previous| previous != protocol.exclusive)
225 {
226 return Err(DataFeedError::Config(format!(
227 "protocol '{}' requested both with and without the '{EXCLUSIVE_PREFIX}' prefix",
228 protocol.system
229 )));
230 }
231 }
232 Ok(protocols)
233}
234
235pub(crate) fn register_exchanges(
241 mut builder: ProtocolStreamBuilder,
242 tvl_filter: ComponentFilter,
243 entries: &[String],
244) -> Result<ProtocolStreamBuilder, DataFeedError> {
245 for protocol in parse_protocols(entries)? {
246 match protocol.system.as_str() {
247 "uniswap_v2" => {
248 builder =
249 builder.exchange::<UniswapV2State>("uniswap_v2", tvl_filter.clone(), None);
250 }
251 "sushiswap_v2" => {
252 builder =
253 builder.exchange::<UniswapV2State>("sushiswap_v2", tvl_filter.clone(), None);
254 }
255 "pancakeswap_v2" => {
256 builder = builder.exchange::<PancakeswapV2State>(
257 "pancakeswap_v2",
258 tvl_filter.clone(),
259 None,
260 );
261 }
262 "uniswap_v3" => {
263 builder =
264 builder.exchange::<UniswapV3State>("uniswap_v3", tvl_filter.clone(), None);
265 }
266 "pancakeswap_v3" => {
267 builder =
268 builder.exchange::<UniswapV3State>("pancakeswap_v3", tvl_filter.clone(), None);
269 }
270 "vm:balancer_v2" => {
271 builder = builder.exchange::<EVMPoolState<PreCachedDB>>(
272 "vm:balancer_v2",
273 tvl_filter.clone(),
274 Some(balancer_v2_pool_filter),
275 );
276 }
277 "uniswap_v4" => {
278 builder =
279 builder.exchange::<UniswapV4State>("uniswap_v4", tvl_filter.clone(), None);
280 }
281 "ekubo_v2" => {
282 builder = builder.exchange::<EkuboState>("ekubo_v2", tvl_filter.clone(), None);
283 }
284 "vm:curve" => {
285 builder = builder.exchange::<CurveState>(
290 "vm:curve",
291 tvl_filter.clone(),
292 Some(curve_filter),
293 );
294 }
295 "uniswap_v4_hooks" => {
296 builder = builder.exchange::<UniswapV4State>(
297 "uniswap_v4_hooks",
298 tvl_filter.clone(),
299 None,
300 );
301 }
302 "vm:maverick_v2" => {
303 builder = builder.exchange::<EVMPoolState<PreCachedDB>>(
304 "vm:maverick_v2",
305 tvl_filter.clone(),
306 None,
307 );
308 }
309 "vm:bopamm" => {
310 builder = builder.exchange::<EVMPoolState<PreCachedDB>>(
311 "vm:bopamm",
312 tvl_filter.clone(),
313 None,
314 );
315 }
316 "vm:fermiswap" => {
317 builder = builder.exchange::<EVMPoolState<PreCachedDB>>(
318 "vm:fermiswap",
319 tvl_filter.clone(),
320 None,
321 );
322 }
323 "fluid_v1" => {
324 builder = builder.exchange::<FluidV1>(
325 "fluid_v1",
326 tvl_filter.clone(),
327 Some(fluid_v1_paused_pools_filter),
328 );
329 }
330 "aerodrome_v1" => {
331 builder =
332 builder.exchange::<AerodromeV1State>("aerodrome_v1", tvl_filter.clone(), None);
333 }
334 "aerodrome_slipstreams" => {
335 builder = builder.exchange::<AerodromeSlipstreamsState>(
336 "aerodrome_slipstreams",
337 tvl_filter.clone(),
338 None,
339 );
340 }
341 "erc4626" => {
342 builder = builder.exchange::<ERC4626State>(
343 "erc4626",
344 tvl_filter.clone(),
345 Some(erc4626_filter),
346 );
347 }
348 "velodrome_slipstreams" => {
349 builder = builder.exchange::<AerodromeSlipstreamsState>(
350 "velodrome_slipstreams",
351 tvl_filter.clone(),
352 None,
353 );
354 }
355 "ekubo_v3" => {
356 let filter = if protocol.exclusive {
359 info!("Including exclusive liquidity for ekubo_v3");
360 ekubo_v3_extension_filter_with_signed_exclusive_swap
361 } else {
362 ekubo_v3_extension_filter
363 };
364 builder =
365 builder.exchange::<EkuboV3State>("ekubo_v3", tvl_filter.clone(), Some(filter));
366 }
367 "quickswap_v2" => {
368 builder =
369 builder.exchange::<UniswapV2State>("quickswap_v2", tvl_filter.clone(), None);
370 }
371 "lunarbase" => {
372 builder = builder.exchange::<LunarBaseState>("lunarbase", tvl_filter.clone(), None);
373 }
374 p if p.starts_with(RFQ_PREFIX) || p.starts_with(PRICE_LEVEL_STREAM_PREFIX) => {
375 continue;
378 }
379 _ => {
380 warn!("Skipping unknown protocol: {}", protocol);
381 }
382 }
383 }
384 Ok(builder)
385}
386
387pub(crate) fn register_rfq(
388 mut rfq_stream_builder: RFQStreamBuilder,
389 chain: Chain,
390 min_tvl: f64,
391 protocols: &[String],
392 rfq_tokens: std::collections::HashSet<Bytes>,
393) -> Result<RFQStreamBuilder, DataFeedError> {
394 for protocol in protocols {
395 match protocol.as_str() {
396 "rfq:bebop" => {
397 let key = get_env("BEBOP_KEY")?;
398 info!("Adding {protocol} RFQ client...");
399 let bebop_client = BebopClientBuilder::new(chain, key)
400 .tokens(rfq_tokens.clone())
401 .tvl_threshold(min_tvl)
402 .build()
403 .map_err(|e| DataFeedError::StreamError(e.to_string()))?;
404 rfq_stream_builder =
405 rfq_stream_builder.add_client::<BebopState>("bebop", Box::new(bebop_client));
406 }
407 "rfq:hashflow" => {
408 let user = get_env("HASHFLOW_USER")?;
409 let key = get_env("HASHFLOW_KEY")?;
410 info!("Adding {protocol} RFQ client...");
411 let hashflow_client = HashflowClientBuilder::new(chain, user, key)
412 .tokens(rfq_tokens.clone())
413 .tvl_threshold(min_tvl)
414 .poll_time(Duration::from_secs(30))
415 .build()
416 .map_err(|e| DataFeedError::StreamError(e.to_string()))?;
417 rfq_stream_builder = rfq_stream_builder
418 .add_client::<HashflowState>("hashflow", Box::new(hashflow_client));
419 }
420 p if p.starts_with(RFQ_PREFIX) => {
421 warn!("Skipping unknown RFQ protocol: {}", p);
422 }
423 _ => {}
424 }
425 }
426 Ok(rfq_stream_builder)
427}
428
429pub(crate) fn open_price_level_stream(
448 chain: Chain,
449 protocols: &[String],
450 tokens: &HashMap<Bytes, Token>,
451) -> Result<Option<impl Stream<Item = Update> + Send>, DataFeedError> {
452 let venues: Vec<&str> = protocols
453 .iter()
454 .filter_map(|protocol| protocol.strip_prefix(PRICE_LEVEL_STREAM_PREFIX))
455 .collect();
456 if venues.is_empty() {
457 return Ok(None);
458 }
459 if chain != PRICE_LEVEL_STREAM_CHAIN {
460 return Err(DataFeedError::Config(format!(
461 "the pAMM price level stream serves {PRICE_LEVEL_STREAM_CHAIN} only, but this feed \
462 runs on {chain}"
463 )));
464 }
465
466 let served = default_served_pamms();
467 let mut builder = PriceLevelStreamBuilder::new().with_tokens(tokens.clone());
468 for venue in venues {
469 let Some(config) = served
470 .iter()
471 .find(|config| config.protocol == venue)
472 else {
473 return Err(DataFeedError::Config(format!(
474 "unknown pAMM '{venue}' for the price level stream; served venues are: {}",
475 served
476 .iter()
477 .map(|config| config.protocol.as_str())
478 .collect::<Vec<_>>()
479 .join(", ")
480 )));
481 };
482 info!("Adding {PRICE_LEVEL_STREAM_PREFIX}{venue} price level venue...");
483 builder = builder.add_pamm(config.clone());
484 }
485 Ok(Some(builder.build()))
486}
487
488#[cfg(feature = "test-utils")]
493pub fn open_price_level_stream_for_recording(
494 chain: Chain,
495 protocols: &[String],
496 tokens: &HashMap<Bytes, Token>,
497) -> Result<Option<impl Stream<Item = Update> + Send>, String> {
498 open_price_level_stream(chain, protocols, tokens).map_err(|e| e.to_string())
499}
500
501fn get_env(var: &str) -> Result<String, DataFeedError> {
502 env::var(var).map_err(|_| DataFeedError::Config(format!("{} env var not set", var)))
503}
504
505#[cfg(test)]
506mod tests {
507 use tycho_simulation::price_level_stream::config::PRICE_LEVEL_STREAM_FAMILY;
508
509 use super::*;
510
511 fn register(entries: &[&str]) -> Result<ProtocolStreamBuilder, DataFeedError> {
512 register_exchanges(
513 ProtocolStreamBuilder::new("localhost:0", Chain::Ethereum),
514 ComponentFilter::with_tvl_range(1.0, 10.0),
515 &entries
516 .iter()
517 .map(|entry| (*entry).to_string())
518 .collect::<Vec<_>>(),
519 )
520 }
521
522 fn price_level_stream(
523 chain: Chain,
524 entries: &[&str],
525 ) -> Result<Option<impl Stream<Item = Update> + Send>, DataFeedError> {
526 open_price_level_stream(
527 chain,
528 &entries
529 .iter()
530 .map(|entry| (*entry).to_string())
531 .collect::<Vec<_>>(),
532 &HashMap::new(),
533 )
534 }
535
536 #[test]
537 fn test_parse_plain_protocol() {
538 let protocol = ProtocolSpec::parse("uniswap_v3").unwrap();
539 assert_eq!(protocol, ProtocolSpec { system: "uniswap_v3".to_string(), exclusive: false });
540 }
541
542 #[test]
543 fn test_parse_exclusive_protocol() {
544 let protocol = ProtocolSpec::parse("exclusive:ekubo_v3").unwrap();
545 assert_eq!(protocol, ProtocolSpec { system: "ekubo_v3".to_string(), exclusive: true });
546 }
547
548 #[test]
549 fn test_parse_exclusive_unsupported_protocol() {
550 let err = ProtocolSpec::parse("exclusive:uniswap_v3").unwrap_err();
551 assert!(err
552 .to_string()
553 .contains("has no exclusive-liquidity variant"));
554 }
555
556 #[test]
557 fn test_parse_exclusive_without_protocol() {
558 assert!(ProtocolSpec::parse("exclusive:").is_err());
559 }
560
561 #[test]
562 fn test_parse_leaves_other_prefixes_intact() {
563 assert_eq!(
564 ProtocolSpec::parse("rfq:bebop").unwrap(),
565 ProtocolSpec { system: "rfq:bebop".to_string(), exclusive: false }
566 );
567 assert_eq!(
568 ProtocolSpec::parse("vm:curve").unwrap(),
569 ProtocolSpec { system: "vm:curve".to_string(), exclusive: false }
570 );
571 }
572
573 #[test]
574 fn test_parse_exclusion() {
575 assert_eq!(
576 parse_exclusion("exclude:vm:fermiswap")
577 .unwrap()
578 .unwrap(),
579 "vm:fermiswap"
580 );
581 }
582
583 #[test]
584 fn test_parse_exclusion_strips_the_exclusive_prefix() {
585 assert_eq!(
586 parse_exclusion("exclude:exclusive:ekubo_v3")
587 .unwrap()
588 .unwrap(),
589 "ekubo_v3"
590 );
591 }
592
593 #[test]
594 fn test_parse_exclusion_rejects_unsupported_exclusive() {
595 assert!(parse_exclusion("exclude:exclusive:uniswap_v3")
596 .unwrap()
597 .is_err());
598 }
599
600 #[test]
601 fn test_parse_exclusion_without_protocol() {
602 assert_eq!(
603 parse_exclusion("exclude:")
604 .unwrap()
605 .unwrap(),
606 ""
607 );
608 }
609
610 #[test]
611 fn test_parse_exclusion_of_a_plain_entry() {
612 assert!(parse_exclusion("uniswap_v3").is_none());
613 }
614
615 #[test]
616 fn test_display_round_trips() {
617 for entry in ["uniswap_v3", "exclusive:ekubo_v3", "rfq:bebop", "vm:curve"] {
618 let protocol = ProtocolSpec::parse(entry).unwrap();
619 assert_eq!(protocol.to_string(), entry);
620 assert_eq!(ProtocolSpec::parse(&protocol.to_string()).unwrap(), protocol);
621 }
622 }
623
624 #[test]
625 fn test_register_exchanges_accepts_exclusive_ekubo_v3() {
626 assert!(register(&["uniswap_v3", "exclusive:ekubo_v3"]).is_ok());
627 }
628
629 #[test]
630 fn test_register_exchanges_rejects_unsupported_exclusive() {
631 let Err(err) = register(&["exclusive:uniswap_v3"]) else {
632 panic!("expected `exclusive:uniswap_v3` to be rejected");
633 };
634 assert!(matches!(err, DataFeedError::Config(_)), "expected a config error, got {err:?}");
635 }
636
637 #[test]
638 fn test_register_exchanges_skips_unknown_protocol() {
639 assert!(register(&["not_a_protocol"]).is_ok());
640 }
641
642 #[test]
643 fn test_register_exchanges_rejects_conflicting_variants() {
644 for protocols in [["ekubo_v3", "exclusive:ekubo_v3"], ["exclusive:ekubo_v3", "ekubo_v3"]] {
645 let Err(err) = register(&protocols) else {
646 panic!("expected {protocols:?} to be rejected");
647 };
648 assert!(
649 err.to_string()
650 .contains("both with and without"),
651 "unexpected error for {protocols:?}: {err}"
652 );
653 }
654 }
655
656 #[test]
657 fn test_register_exchanges_allows_repeated_protocol() {
658 assert!(register(&["uniswap_v3", "uniswap_v3"]).is_ok());
659 }
660 #[test]
661 fn test_price_level_stream_prefix_matches_family() {
662 assert_eq!(PRICE_LEVEL_STREAM_PREFIX, format!("{PRICE_LEVEL_STREAM_FAMILY}:"));
663 }
664
665 #[test]
666 fn test_matches_streamed_system() {
667 assert!(matches_streamed_system("uniswap_v3", "uniswap_v3"));
668 assert!(matches_streamed_system(
669 "pricelevelstream:fermiswap",
670 "pricelevelstream:fermiswap"
671 ));
672 assert!(matches_streamed_system("pricelevelstream:fermiswap", "propammfallback:fermiswap"));
674 }
675
676 #[test]
677 fn test_matches_streamed_system_rejects_another_venue() {
678 assert!(!matches_streamed_system("pricelevelstream:fermiswap", "propammfallback:kipseli"));
679 assert!(!matches_streamed_system("pricelevelstream:fermiswap", "vm:fermiswap"));
680 assert!(!matches_streamed_system("uniswap_v3", "propammfallback:fermiswap"));
681 assert!(!matches_streamed_system("vm:fermiswap", "propammfallback:fermiswap"));
682 }
683
684 #[test]
685 fn test_has_tycho_protocols() {
686 assert!(has_tycho_protocols(&["uniswap_v3".to_string()]));
687 assert!(has_tycho_protocols(&["rfq:bebop".to_string(), "uniswap_v3".to_string()]));
688 assert!(!has_tycho_protocols(&[
689 "rfq:bebop".to_string(),
690 "pricelevelstream:fermiswap".to_string(),
691 ]));
692 assert!(!has_tycho_protocols(&[]));
693 }
694
695 #[test]
696 fn test_register_exchanges_skips_price_level_entries() {
697 assert!(register(&["uniswap_v3", "pricelevelstream:fermiswap"]).is_ok());
698 }
699
700 #[test]
701 fn test_open_price_level_stream_without_entries() {
702 let Ok(None) = price_level_stream(Chain::Ethereum, &["uniswap_v3", "rfq:bebop"]) else {
703 panic!("expected no price level stream without a `pricelevelstream:` entry");
704 };
705 }
706
707 #[test]
708 fn test_open_price_level_stream_served_venue() {
709 let Ok(Some(_)) = price_level_stream(Chain::Ethereum, &["pricelevelstream:fermiswap"])
710 else {
711 panic!("expected fermiswap to be served");
712 };
713 }
714
715 #[test]
716 fn test_open_price_level_stream_several_venues() {
717 let Ok(Some(_)) = price_level_stream(
718 Chain::Ethereum,
719 &["pricelevelstream:fermiswap", "pricelevelstream:kipseli"],
720 ) else {
721 panic!("expected both venues to be served");
722 };
723 }
724
725 #[test]
726 fn test_open_price_level_stream_unknown_venue() {
727 for entries in [
728 vec!["pricelevelstream:nope"],
729 vec!["pricelevelstream:fermiswap", "pricelevelstream:nope"],
730 ] {
731 let Err(err) = price_level_stream(Chain::Ethereum, &entries) else {
732 panic!("expected an unserved venue to be rejected in {entries:?}");
733 };
734 assert!(
735 err.to_string()
736 .contains("unknown pAMM 'nope'"),
737 "got {err}"
738 );
739 }
740 }
741
742 #[test]
743 fn test_open_price_level_stream_without_entries_off_ethereum() {
744 let Ok(None) = price_level_stream(Chain::Base, &["uniswap_v3"]) else {
745 panic!("expected chains without a `pricelevelstream:` entry to be left alone");
746 };
747 }
748
749 #[test]
750 fn test_open_price_level_stream_other_chain() {
751 let Err(err) = price_level_stream(Chain::Base, &["pricelevelstream:fermiswap"]) else {
752 panic!("expected the price level stream to be rejected off Ethereum");
753 };
754 assert!(
755 err.to_string()
756 .contains("serves ethereum only"),
757 "got {err}"
758 );
759 }
760}