1use std::{
2 collections::{hash_map::Entry, HashMap, HashSet},
3 time::Duration,
4};
5
6use chrono::Utc;
7use num_bigint::BigUint;
8use tokio_stream::{Stream, StreamExt};
9use tycho_common::{
10 models::{token::Token, Chain},
11 simulation::protocol_sim::ProtocolSim,
12 Bytes,
13};
14
15use super::{
16 config::{
17 default_denied_pamms, default_served_pamms, PriceLevelStreamConfig,
18 DEFAULT_AUTO_DETECTED_GAS_COST,
19 },
20 state::{PriceLevelStreamQuote, PriceLevelStreamState},
21 titan::{
22 self, ConnectionSettings, TitanPairLevels, TitanPammLevels, TitanPriceLevel,
23 TitanPriceLevelMessage, TITAN_PRICE_LEVEL_URL,
24 },
25};
26use crate::protocol::models::{ProtocolComponent, Update};
27
28pub const PAMM_ADDRESS_ATTRIBUTE: &str = "pamm_address";
30
31pub struct PriceLevelStreamBuilder {
45 registry: HashMap<Bytes, PriceLevelStreamConfig>,
46 denied: HashSet<Bytes>,
47 tokens: HashMap<Bytes, Token>,
48 url: Option<String>,
49 auto_detect: bool,
50 auto_detected_gas_cost: Option<BigUint>,
51 connection: ConnectionSettings,
52 fallback_router: bool,
55}
56
57impl Default for PriceLevelStreamBuilder {
58 fn default() -> Self {
59 Self {
60 registry: HashMap::new(),
61 denied: HashSet::new(),
62 tokens: HashMap::new(),
63 url: None,
64 auto_detect: false,
65 auto_detected_gas_cost: None,
66 connection: ConnectionSettings::default(),
67 fallback_router: true,
68 }
69 }
70}
71
72impl PriceLevelStreamBuilder {
73 pub fn new() -> Self {
74 Self::default()
75 }
76
77 pub fn auto_detect(mut self, enabled: bool) -> Self {
89 self.auto_detect = enabled;
90 self
91 }
92
93 pub fn auto_detected_gas_cost(mut self, gas_cost: BigUint) -> Self {
98 self.auto_detected_gas_cost = Some(gas_cost);
99 self
100 }
101
102 pub fn endpoint(mut self, url: impl Into<String>) -> Self {
105 self.url = Some(url.into());
106 self
107 }
108
109 pub fn connect_timeout(mut self, timeout: Duration) -> Self {
112 self.connection.connect_timeout = timeout;
113 self
114 }
115
116 pub fn read_idle_timeout(mut self, timeout: Duration) -> Self {
120 self.connection.read_idle_timeout = timeout;
121 self
122 }
123
124 pub fn max_backoff(mut self, max_backoff: Duration) -> Self {
127 self.connection.max_backoff = max_backoff;
128 self
129 }
130
131 pub fn add_pamm(mut self, config: PriceLevelStreamConfig) -> Self {
138 self.denied.remove(&config.address);
139 self.registry
140 .insert(config.address.clone(), config);
141 self
142 }
143
144 pub fn deny_pamm(mut self, address: Bytes) -> Self {
153 self.registry.remove(&address);
154 self.denied.insert(address);
155 self
156 }
157
158 pub fn with_known_pamms(mut self) -> Self {
165 for config in default_served_pamms() {
166 if self.denied.contains(&config.address) {
167 continue;
168 }
169 self.registry
170 .entry(config.address.clone())
171 .or_insert(config);
172 }
173 for address in default_denied_pamms() {
174 if self.registry.contains_key(&address) {
175 continue;
176 }
177 self.denied.insert(address);
178 }
179 self
180 }
181
182 pub fn with_tokens(mut self, tokens: HashMap<Bytes, Token>) -> Self {
185 self.tokens = tokens;
186 self
187 }
188
189 pub fn without_fallback_router(mut self) -> Self {
196 self.fallback_router = false;
197 self
198 }
199
200 pub fn build(self) -> impl Stream<Item = Update> + Send {
219 let Self {
220 registry,
221 denied,
222 tokens,
223 url,
224 auto_detect,
225 auto_detected_gas_cost,
226 connection,
227 fallback_router,
228 } = self;
229 if registry.is_empty() && !auto_detect {
230 tracing::warn!(
231 "No pAMMs registered and auto-detection is off; the stream will never produce \
232 an update"
233 );
234 }
235 if tokens.is_empty() {
236 tracing::warn!(
237 "No token metadata provided; every streamed pair will be skipped and the stream \
238 will never produce an update"
239 );
240 }
241 let url = url.unwrap_or_else(|| TITAN_PRICE_LEVEL_URL.to_string());
242 let auto_detected_gas_cost =
243 auto_detected_gas_cost.unwrap_or_else(|| BigUint::from(DEFAULT_AUTO_DETECTED_GAS_COST));
244
245 let mut tracker = SnapshotTracker::new(
246 registry,
247 denied,
248 tokens,
249 auto_detect,
250 auto_detected_gas_cost,
251 fallback_router,
252 );
253 titan::messages(url, connection).filter_map(move |message| tracker.process(message))
254 }
255}
256
257struct SnapshotTracker {
260 registry: HashMap<Bytes, PriceLevelStreamConfig>,
261 denied: HashSet<Bytes>,
264 tokens: HashMap<Bytes, Token>,
265 auto_detect: bool,
268 auto_detected_gas_cost: BigUint,
270 via_fallback_router: bool,
273 components: HashMap<String, ProtocolComponent>,
278 newest_block: u64,
282}
283
284impl SnapshotTracker {
285 fn new(
286 registry: HashMap<Bytes, PriceLevelStreamConfig>,
287 denied: HashSet<Bytes>,
288 tokens: HashMap<Bytes, Token>,
289 auto_detect: bool,
290 auto_detected_gas_cost: BigUint,
291 via_fallback_router: bool,
292 ) -> Self {
293 Self {
294 registry,
295 denied,
296 tokens,
297 auto_detect,
298 auto_detected_gas_cost,
299 via_fallback_router,
300 components: HashMap::new(),
301 newest_block: 0,
302 }
303 }
304
305 fn process(&mut self, message: TitanPriceLevelMessage) -> Option<Update> {
309 if message.block_number < self.newest_block {
310 tracing::warn!(
311 block_number = message.block_number,
312 newest_block = self.newest_block,
313 "Skipping out-of-order price level frame"
314 );
315 return None;
316 }
317 self.newest_block = message.block_number;
318
319 let mut states: HashMap<String, Box<dyn ProtocolSim>> = HashMap::new();
320 let mut new_pairs = HashMap::new();
321 let mut previous = std::mem::take(&mut self.components);
324
325 for TitanPammLevels { pamm, pairs } in message.pamms {
326 let config = match self.registry.entry(pamm.clone()) {
327 Entry::Occupied(entry) => &*entry.into_mut(),
328 Entry::Vacant(entry) => {
329 if !self.auto_detect {
330 tracing::debug!(%pamm, "Skipping unregistered pAMM");
331 continue;
332 }
333 if self.denied.contains(&pamm) {
334 tracing::debug!(%pamm, "Skipping denied pAMM");
335 continue;
336 }
337 tracing::info!(%pamm, "Serving auto-detected pAMM");
338 &*entry.insert(PriceLevelStreamConfig::auto_detected(
339 pamm.clone(),
340 self.auto_detected_gas_cost.clone(),
341 ))
342 }
343 };
344
345 let mut merged_pairs: HashMap<(Bytes, Bytes), (Vec<_>, Vec<_>)> = HashMap::new();
347 for TitanPairLevels { token_in, token_out, order_book } in pairs {
348 if !self.tokens.contains_key(&token_in) || !self.tokens.contains_key(&token_out) {
349 tracing::debug!(%token_in, %token_out, "Skipping pair with unknown token");
350 continue;
351 }
352 let sells_token0 = token_in < token_out;
353 let key = if sells_token0 {
354 (token_in.clone(), token_out.clone())
355 } else {
356 (token_out.clone(), token_in.clone())
357 };
358 let quotes = order_book
359 .into_iter()
360 .map(|TitanPriceLevel { amount_in, amount_out }| {
361 PriceLevelStreamQuote::new(amount_in, amount_out)
362 })
363 .collect();
364 let entry = merged_pairs.entry(key).or_default();
365 if sells_token0 {
366 entry.0 = quotes;
367 } else {
368 entry.1 = quotes;
369 }
370 }
371
372 for ((token0, token1), (quotes_0_to_1, quotes_1_to_0)) in merged_pairs {
373 let id = component_id(&config.address, &token0, &token1);
374 let id_string = id.to_string();
375 let component = previous
376 .remove(&id_string)
377 .unwrap_or_else(|| {
378 let component = build_component(
379 &self.tokens,
380 config,
381 id,
382 &token0,
383 &token1,
384 self.via_fallback_router,
385 );
386 new_pairs.insert(id_string.clone(), component.clone());
387 component
388 });
389
390 let state = PriceLevelStreamState::new(
391 token0,
392 token1,
393 quotes_0_to_1,
394 quotes_1_to_0,
395 config.gas_cost.clone(),
396 );
397
398 states.insert(id_string.clone(), Box::new(state));
399 self.components
400 .insert(id_string, component);
401 }
402 }
403
404 let removed_pairs = previous;
407
408 if states.is_empty() && new_pairs.is_empty() && removed_pairs.is_empty() {
409 return None;
410 }
411
412 Some(
413 Update::new(message.block_number, states, new_pairs)
416 .set_is_partial(true)
417 .set_removed_pairs(removed_pairs),
418 )
419 }
420}
421
422fn build_component(
423 tokens: &HashMap<Bytes, Token>,
424 config: &PriceLevelStreamConfig,
425 id: Bytes,
426 token0: &Bytes,
427 token1: &Bytes,
428 via_router: bool,
429) -> ProtocolComponent {
430 let protocol_system =
431 if via_router { config.fallback_protocol_system() } else { config.protocol_system() };
432 ProtocolComponent::new(
433 id,
434 protocol_system.clone(),
435 protocol_system,
436 Chain::Ethereum,
438 vec![tokens[token0].clone(), tokens[token1].clone()],
439 vec![config.address.clone()],
440 HashMap::from([(PAMM_ADDRESS_ATTRIBUTE.to_string(), config.address.clone())]),
441 Bytes::default(),
442 Utc::now().naive_utc(),
443 )
444}
445
446fn component_id(pamm: &Bytes, token0: &Bytes, token1: &Bytes) -> Bytes {
448 Bytes::from([pamm.as_ref(), token0.as_ref(), token1.as_ref()].concat())
449}
450
451#[cfg(test)]
452mod tests {
453 use std::str::FromStr;
454
455 use num_bigint::BigUint;
456
457 use super::*;
458
459 const PAMM: &str = "0x5979458912f80b96d30d4220af8e2e4925a33320";
460 const WBTC: &str = "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599";
461 const USDC: &str = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48";
462 const WETH: &str = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2";
463
464 fn token(address: &str, symbol: &str, decimals: u32) -> Token {
465 Token::new(
466 &Bytes::from_str(address).unwrap(),
467 symbol,
468 decimals,
469 0,
470 &[Some(10_000)],
471 Chain::Ethereum,
472 100,
473 )
474 }
475
476 fn tokens() -> HashMap<Bytes, Token> {
477 [token(WBTC, "WBTC", 8), token(USDC, "USDC", 6), token(WETH, "WETH", 18)]
478 .into_iter()
479 .map(|token| (token.address.clone(), token))
480 .collect()
481 }
482
483 fn tracker() -> SnapshotTracker {
484 let config = PriceLevelStreamConfig::new(
485 "fermiswap",
486 Bytes::from_str(PAMM).unwrap(),
487 BigUint::from(120_000u64),
488 );
489 SnapshotTracker::new(
490 HashMap::from([(config.address.clone(), config)]),
491 HashSet::new(),
492 tokens(),
493 false,
494 BigUint::from(DEFAULT_AUTO_DETECTED_GAS_COST),
495 false,
496 )
497 }
498
499 fn level(amount_in: u64, amount_out: u64) -> TitanPriceLevel {
500 TitanPriceLevel {
501 amount_in: BigUint::from(amount_in),
502 amount_out: BigUint::from(amount_out),
503 }
504 }
505
506 fn pair_levels(
507 token_in: &str,
508 token_out: &str,
509 order_book: Vec<TitanPriceLevel>,
510 ) -> TitanPairLevels {
511 TitanPairLevels {
512 token_in: Bytes::from_str(token_in).unwrap(),
513 token_out: Bytes::from_str(token_out).unwrap(),
514 order_book,
515 }
516 }
517
518 fn message(block_number: u64, pairs: Vec<TitanPairLevels>) -> TitanPriceLevelMessage {
519 TitanPriceLevelMessage {
520 block_number,
521 pamms: vec![TitanPammLevels { pamm: Bytes::from_str(PAMM).unwrap(), pairs }],
522 }
523 }
524
525 fn wbtc_usdc_pairs() -> Vec<TitanPairLevels> {
526 vec![
527 pair_levels(WBTC, USDC, vec![level(100_000_000, 100_000_000_000)]),
528 pair_levels(USDC, WBTC, vec![level(100_000_000_000, 99_000_000)]),
529 ]
530 }
531
532 fn expected_id() -> String {
533 format!("{PAMM}{}{}", &WBTC[2..], &USDC[2..])
535 }
536
537 #[test]
538 fn first_snapshot_emits_new_pair_with_both_directions() {
539 let mut tracker = tracker();
540 let Update {
541 block_number_or_timestamp,
542 is_partial,
543 sync_states,
544 states,
545 new_pairs,
546 removed_pairs,
547 } = tracker
548 .process(message(100, wbtc_usdc_pairs()))
549 .expect("update expected");
550
551 assert_eq!(block_number_or_timestamp, 100);
552 assert!(is_partial);
553 assert!(sync_states.is_empty());
554 assert!(removed_pairs.is_empty());
555
556 let id = expected_id();
557 let component = &new_pairs[&id];
558 assert_eq!(component.protocol_system, "pricelevelstream:fermiswap");
559 assert_eq!(
560 component.static_attributes[PAMM_ADDRESS_ATTRIBUTE],
561 Bytes::from_str(PAMM).unwrap()
562 );
563
564 let PriceLevelStreamState { token0, token1, quotes_0_to_1, quotes_1_to_0, gas_cost } =
565 states[&id]
566 .as_any()
567 .downcast_ref::<PriceLevelStreamState>()
568 .expect("price level state");
569 assert_eq!(token0, &Bytes::from_str(WBTC).unwrap());
570 assert_eq!(token1, &Bytes::from_str(USDC).unwrap());
571 assert_eq!(quotes_0_to_1.len(), 1);
572 assert_eq!(quotes_1_to_0.len(), 1);
573 assert_eq!(quotes_0_to_1[0].amount_in, BigUint::from(100_000_000u64));
574 assert_eq!(gas_cost, &BigUint::from(120_000u64));
575 }
576
577 #[test]
578 fn repeated_snapshot_is_not_a_new_pair() {
579 let mut tracker = tracker();
580 tracker
581 .process(message(100, wbtc_usdc_pairs()))
582 .expect("update expected");
583 let update = tracker
584 .process(message(101, wbtc_usdc_pairs()))
585 .expect("update expected");
586
587 assert!(update.new_pairs.is_empty());
588 assert!(update.removed_pairs.is_empty());
589 assert!(update
590 .states
591 .contains_key(&expected_id()));
592 }
593
594 #[test]
595 fn dropped_pair_is_removed() {
596 let mut tracker = tracker();
597 tracker
598 .process(message(100, wbtc_usdc_pairs()))
599 .expect("update expected");
600 let weth_usdc =
601 vec![pair_levels(WETH, USDC, vec![level(1_000_000_000_000_000_000, 3_000_000_000)])];
602 let update = tracker
603 .process(message(101, weth_usdc))
604 .expect("update expected");
605
606 assert_eq!(update.removed_pairs.len(), 1);
607 assert!(update
608 .removed_pairs
609 .contains_key(&expected_id()));
610 assert_eq!(update.new_pairs.len(), 1);
611 assert_eq!(update.states.len(), 1);
612 }
613
614 #[test]
615 fn out_of_order_frame_is_skipped() {
616 let mut tracker = tracker();
617 tracker
618 .process(message(101, wbtc_usdc_pairs()))
619 .expect("update expected");
620
621 let stale =
624 vec![pair_levels(WETH, USDC, vec![level(1_000_000_000_000_000_000, 3_000_000_000)])];
625 assert!(tracker
626 .process(message(100, stale))
627 .is_none());
628
629 let update = tracker
632 .process(message(102, wbtc_usdc_pairs()))
633 .expect("update expected");
634 assert!(update.new_pairs.is_empty());
635 assert!(update.removed_pairs.is_empty());
636 }
637
638 #[test]
639 fn vanished_pamm_has_its_pairs_removed() {
640 let mut tracker = tracker();
641 tracker
642 .process(message(100, wbtc_usdc_pairs()))
643 .expect("update expected");
644
645 let update = tracker
648 .process(TitanPriceLevelMessage { block_number: 101, pamms: vec![] })
649 .expect("update expected");
650 assert!(update.states.is_empty());
651 assert!(update.new_pairs.is_empty());
652 assert_eq!(update.removed_pairs.len(), 1);
653 assert!(update
654 .removed_pairs
655 .contains_key(&expected_id()));
656
657 assert!(tracker
659 .process(TitanPriceLevelMessage { block_number: 102, pamms: vec![] })
660 .is_none());
661
662 let update = tracker
664 .process(message(103, wbtc_usdc_pairs()))
665 .expect("update expected");
666 assert!(update
667 .new_pairs
668 .contains_key(&expected_id()));
669 }
670
671 #[test]
672 fn unregistered_pamm_produces_no_update_without_auto_detection() {
673 let mut tracker = SnapshotTracker::new(
674 HashMap::new(),
675 HashSet::new(),
676 tokens(),
677 false,
678 BigUint::from(DEFAULT_AUTO_DETECTED_GAS_COST),
679 false,
680 );
681 assert!(tracker
682 .process(message(100, wbtc_usdc_pairs()))
683 .is_none());
684 }
685
686 #[test]
687 fn denied_pamm_is_not_auto_detected() {
688 let denied = HashSet::from([Bytes::from_str(PAMM).unwrap()]);
689 let mut tracker = SnapshotTracker::new(
690 HashMap::new(),
691 denied,
692 tokens(),
693 true,
694 BigUint::from(DEFAULT_AUTO_DETECTED_GAS_COST),
695 false,
696 );
697 assert!(tracker
698 .process(message(100, wbtc_usdc_pairs()))
699 .is_none());
700 }
701
702 #[test]
703 fn explicit_add_and_deny_are_last_wins() {
704 let address = Bytes::from_str(PAMM).unwrap();
705 let custom =
706 || PriceLevelStreamConfig::new("custom", Bytes::from_str(PAMM).unwrap(), 1u64.into());
707
708 let builder = PriceLevelStreamBuilder::new()
709 .add_pamm(custom())
710 .deny_pamm(address.clone());
711 assert!(!builder.registry.contains_key(&address));
712 assert!(builder.denied.contains(&address));
713
714 let builder = PriceLevelStreamBuilder::new()
715 .deny_pamm(address.clone())
716 .add_pamm(custom());
717 assert_eq!(builder.registry[&address].protocol, "custom");
718 assert!(builder.denied.is_empty());
719 }
720
721 #[test]
722 fn defaults_never_override_explicit_calls() {
723 let fermiswap_router = Bytes::from_str(PAMM).unwrap();
725 for builder in [
726 PriceLevelStreamBuilder::new()
727 .deny_pamm(fermiswap_router.clone())
728 .with_known_pamms(),
729 PriceLevelStreamBuilder::new()
730 .with_known_pamms()
731 .deny_pamm(fermiswap_router.clone()),
732 ] {
733 assert!(!builder
734 .registry
735 .contains_key(&fermiswap_router));
736 assert!(builder
737 .denied
738 .contains(&fermiswap_router));
739 assert!(!builder.registry.is_empty());
741 }
742
743 let Some(denied_venue) = default_denied_pamms().pop() else { return };
746 let custom = || PriceLevelStreamConfig::new("custom", denied_venue.clone(), 1u64.into());
747 for builder in [
748 PriceLevelStreamBuilder::new()
749 .add_pamm(custom())
750 .with_known_pamms(),
751 PriceLevelStreamBuilder::new()
752 .with_known_pamms()
753 .add_pamm(custom()),
754 ] {
755 assert_eq!(builder.registry[&denied_venue].protocol, "custom");
756 assert!(!builder.denied.contains(&denied_venue));
757 }
758 }
759
760 #[test]
761 fn auto_detected_pamm_is_served_under_its_address() {
762 let mut tracker = SnapshotTracker::new(
763 HashMap::new(),
764 HashSet::new(),
765 tokens(),
766 true,
767 BigUint::from(DEFAULT_AUTO_DETECTED_GAS_COST),
768 false,
769 );
770 let update = tracker
771 .process(message(100, wbtc_usdc_pairs()))
772 .expect("update expected");
773
774 let component = &update.new_pairs[&expected_id()];
775 assert_eq!(component.protocol_system, format!("pricelevelstream:{PAMM}"));
776 let state = update.states[&expected_id()]
777 .as_any()
778 .downcast_ref::<PriceLevelStreamState>()
779 .expect("price level state");
780 assert_eq!(state.gas_cost, BigUint::from(DEFAULT_AUTO_DETECTED_GAS_COST));
781
782 let update = tracker
784 .process(message(101, wbtc_usdc_pairs()))
785 .expect("update expected");
786 assert!(update.new_pairs.is_empty());
787 }
788
789 #[test]
790 fn auto_detected_gas_cost_override_applies() {
791 let mut tracker = SnapshotTracker::new(
792 HashMap::new(),
793 HashSet::new(),
794 tokens(),
795 true,
796 BigUint::from(42_000u64),
797 false,
798 );
799 let update = tracker
800 .process(message(100, wbtc_usdc_pairs()))
801 .expect("update expected");
802
803 let state = update.states[&expected_id()]
804 .as_any()
805 .downcast_ref::<PriceLevelStreamState>()
806 .expect("price level state");
807 assert_eq!(state.gas_cost, BigUint::from(42_000u64));
808 }
809
810 #[test]
811 fn with_known_pamms_registers_known_venues() {
812 let fermiswap_router = Bytes::from_str(PAMM).unwrap();
814
815 let builder = PriceLevelStreamBuilder::new();
816 assert!(builder.registry.is_empty());
817 assert!(builder.denied.is_empty());
818
819 let builder = builder.with_known_pamms();
820 assert_eq!(builder.registry[&fermiswap_router].protocol, "fermiswap");
821 assert_eq!(
823 builder.denied,
824 default_denied_pamms()
825 .into_iter()
826 .collect()
827 );
828 assert!(builder.denied.is_disjoint(
829 &builder
830 .registry
831 .keys()
832 .cloned()
833 .collect()
834 ));
835
836 let custom =
838 || PriceLevelStreamConfig::new("custom", fermiswap_router.clone(), BigUint::from(1u64));
839 for builder in [
840 PriceLevelStreamBuilder::new()
841 .add_pamm(custom())
842 .with_known_pamms(),
843 PriceLevelStreamBuilder::new()
844 .with_known_pamms()
845 .add_pamm(custom()),
846 ] {
847 assert_eq!(builder.registry[&fermiswap_router].protocol, "custom");
848 assert_eq!(builder.registry[&fermiswap_router].gas_cost, BigUint::from(1u64));
849 }
850 }
851
852 #[test]
855 fn venues_are_served_under_the_fallback_family() {
856 let config = PriceLevelStreamConfig::new(
857 "fermiswap",
858 Bytes::from_str(PAMM).unwrap(),
859 BigUint::from(120_000u64),
860 );
861 let mut tracker = SnapshotTracker::new(
862 HashMap::from([(config.address.clone(), config)]),
863 HashSet::new(),
864 tokens(),
865 false,
866 BigUint::from(DEFAULT_AUTO_DETECTED_GAS_COST),
867 true,
868 );
869
870 let update = tracker
871 .process(message(100, wbtc_usdc_pairs()))
872 .expect("update expected");
873
874 let component = &update.new_pairs[&expected_id()];
875 assert_eq!(component.protocol_system, "fallback:fermiswap");
876 assert_eq!(
877 component.static_attributes[PAMM_ADDRESS_ATTRIBUTE],
878 Bytes::from_str(PAMM).unwrap()
879 );
880 }
881
882 #[test]
884 fn auto_detected_venue_is_served_under_the_fallback_family() {
885 let mut tracker = SnapshotTracker::new(
886 HashMap::new(),
887 HashSet::new(),
888 tokens(),
889 true,
890 BigUint::from(DEFAULT_AUTO_DETECTED_GAS_COST),
891 true,
892 );
893
894 let update = tracker
895 .process(message(100, wbtc_usdc_pairs()))
896 .expect("update expected");
897
898 let component = &update.new_pairs[&expected_id()];
899 assert_eq!(component.protocol_system, format!("fallback:{PAMM}"));
900 }
901
902 #[test]
904 fn without_fallback_router_keeps_the_direct_family() {
905 let mut tracker = tracker();
906
907 let update = tracker
908 .process(message(100, wbtc_usdc_pairs()))
909 .expect("update expected");
910
911 assert_eq!(update.new_pairs[&expected_id()].protocol_system, "pricelevelstream:fermiswap");
912 }
913
914 #[test]
916 fn fallback_router_is_on_unless_opted_out() {
917 assert!(PriceLevelStreamBuilder::new().fallback_router);
918 assert!(
919 !PriceLevelStreamBuilder::new()
920 .without_fallback_router()
921 .fallback_router
922 );
923 }
924
925 #[test]
928 fn families_match_the_execution_side_prefixes() {
929 use tycho_execution::encoding::evm::{FALLBACK_PREFIX, PRICE_LEVEL_STREAM_PREFIX};
930
931 use super::super::config::{FALLBACK_FAMILY, PRICE_LEVEL_STREAM_FAMILY};
932
933 assert_eq!(format!("{PRICE_LEVEL_STREAM_FAMILY}:"), PRICE_LEVEL_STREAM_PREFIX);
934 assert_eq!(format!("{FALLBACK_FAMILY}:"), FALLBACK_PREFIX);
935 }
936
937 #[test]
938 fn unknown_tokens_are_skipped() {
939 let mut tracker = tracker();
940 let unknown = vec![pair_levels(
941 "0x1111111111111111111111111111111111111111",
942 USDC,
943 vec![level(1, 1)],
944 )];
945 assert!(tracker
946 .process(message(100, unknown))
947 .is_none());
948 }
949}