1use std::{
18 collections::{HashMap, HashSet},
19 sync::Arc,
20};
21
22use tokio::sync::RwLock;
23use tycho_simulation::{
24 tycho_client::feed::SynchronizerState,
25 tycho_common::{
26 models::{protocol::ProtocolComponent, token::Token, Address},
27 simulation::protocol_sim::ProtocolSim,
28 },
29 tycho_ethereum::gas::BlockGasPrice,
30};
31
32use crate::types::{BlockInfo, ComponentId};
33
34pub type StateLabel = String;
39
40pub type OverlayStates = Arc<HashMap<ComponentId, Box<dyn ProtocolSim>>>;
42
43pub struct OverlayEntry {
45 pub states: OverlayStates,
47 pub valid_until: u64,
50}
51
52type OverlayRegistry = Arc<RwLock<HashMap<StateLabel, OverlayEntry>>>;
54
55#[derive(Debug, thiserror::Error)]
57pub enum ReadLabeledError {
58 #[error("label not found: {0}")]
60 NotFound(StateLabel),
61}
62
63#[derive(Clone)]
68pub struct MarketData {
69 data: Arc<RwLock<MarketState>>,
70 overlays: OverlayRegistry,
73}
74
75impl MarketData {
76 pub fn new(data: Arc<RwLock<MarketState>>) -> Self {
78 Self { data, overlays: Arc::new(RwLock::new(HashMap::new())) }
79 }
80
81 pub fn new_shared() -> Self {
83 Self::new(Arc::new(RwLock::new(MarketState::new())))
84 }
85
86 pub async fn read(&self) -> MarketDataView<'_> {
88 MarketDataView { guard: self.data.read().await, overlay: None }
89 }
90
91 pub async fn read_labeled(
100 &self,
101 label: &StateLabel,
102 ) -> Result<MarketDataView<'_>, ReadLabeledError> {
103 let guard = self.data.read().await;
104 if let Some(e) = self.overlays.read().await.get(label) {
105 let states = Arc::clone(&e.states);
106 return Ok(MarketDataView { guard, overlay: Some((label.clone(), states)) });
107 }
108 if &guard.label == label {
109 return Ok(MarketDataView { guard, overlay: None });
110 }
111 Err(ReadLabeledError::NotFound(label.clone()))
112 }
113
114 pub async fn write(&self) -> tokio::sync::RwLockWriteGuard<'_, MarketState> {
116 self.data.write().await
117 }
118
119 pub fn try_read(&self) -> Option<tokio::sync::RwLockReadGuard<'_, MarketState>> {
123 self.data.try_read().ok()
124 }
125
126 pub fn try_write(&self) -> Option<tokio::sync::RwLockWriteGuard<'_, MarketState>> {
130 self.data.try_write().ok()
131 }
132
133 pub fn try_read_blocking(&self) -> Option<MarketDataView<'_>> {
140 self.data
141 .try_read()
142 .ok()
143 .map(|guard| MarketDataView { guard, overlay: None })
144 }
145
146 pub async fn register_labeled_state(
150 &self,
151 label: StateLabel,
152 states: HashMap<ComponentId, Box<dyn ProtocolSim>>,
153 valid_until: u64,
154 ) {
155 self.overlays
156 .write()
157 .await
158 .insert(label, OverlayEntry { states: Arc::new(states), valid_until });
159 }
160
161 pub async fn remove_labeled_state(&self, label: &StateLabel) {
163 self.overlays
164 .write()
165 .await
166 .remove(label);
167 }
168
169 pub async fn clear_labeled_states(&self) {
171 self.overlays.write().await.clear();
172 }
173
174 pub async fn apply_block_update(
181 &self,
182 new_block_number: u64,
183 update: impl FnOnce(&mut MarketState),
184 ) {
185 self.overlays
186 .write()
187 .await
188 .retain(|_, entry| entry.valid_until >= new_block_number);
189 let mut data = self.data.write().await;
190 data.label = new_block_number.to_string();
191 update(&mut data);
192 }
193
194 pub async fn labeled_state_ids(&self) -> Vec<StateLabel> {
196 self.overlays
197 .read()
198 .await
199 .keys()
200 .cloned()
201 .collect()
202 }
203}
204
205pub struct MarketDataView<'a> {
211 guard: tokio::sync::RwLockReadGuard<'a, MarketState>,
212 overlay: Option<(StateLabel, OverlayStates)>,
213}
214
215impl<'a> MarketDataView<'a> {
216 pub fn state_label(&self) -> Option<&StateLabel> {
218 self.overlay
219 .as_ref()
220 .map(|(label, _)| label)
221 }
222
223 pub fn get_simulation_state(&self, id: &str) -> Option<&dyn ProtocolSim> {
225 if let Some((_, ref states)) = self.overlay {
226 if let Some(s) = states.get(id) {
227 return Some(s.as_ref());
228 }
229 }
230 self.guard.get_simulation_state(id)
231 }
232
233 pub fn extract_subset_with_overlay(&self, component_ids: &HashSet<ComponentId>) -> MarketState {
238 let mut subset = self.guard.extract_subset(component_ids);
239 if let Some((ref label, ref states)) = self.overlay {
240 for (id, state) in states.iter() {
241 if subset
242 .simulation_states
243 .contains_key(id)
244 {
245 subset
246 .simulation_states
247 .insert(id.clone(), state.clone_box());
248 }
249 }
250 subset.label = label.clone();
251 }
252 subset
253 }
254
255 pub fn component_topology(&self) -> HashMap<ComponentId, Vec<Address>> {
257 self.guard.component_topology()
258 }
259
260 pub fn extract_subset(&self, component_ids: &HashSet<ComponentId>) -> MarketState {
262 self.guard.extract_subset(component_ids)
263 }
264
265 pub fn token_registry_ref(&self) -> &HashMap<Address, Token> {
267 self.guard.token_registry_ref()
268 }
269
270 pub fn gas_price(&self) -> Option<&BlockGasPrice> {
272 self.guard.gas_price()
273 }
274
275 pub fn last_updated(&self) -> Option<&BlockInfo> {
277 self.guard.last_updated()
278 }
279
280 pub fn get_token(&self, address: &Address) -> Option<&Token> {
282 self.guard.get_token(address)
283 }
284
285 pub fn get_component(&self, id: &str) -> Option<&ProtocolComponent> {
287 self.guard.get_component(id)
288 }
289
290 pub fn base_market_state(&self) -> &MarketState {
292 &self.guard
293 }
294}
295
296#[derive(Debug, Default)]
301pub struct MarketState {
302 label: StateLabel,
308 components: HashMap<ComponentId, ProtocolComponent>,
310 simulation_states: HashMap<ComponentId, Box<dyn ProtocolSim>>,
312 tokens: HashMap<Address, Token>,
314 gas_price: Option<BlockGasPrice>,
316 protocol_sync_status: HashMap<String, SynchronizerState>,
318 last_updated: Option<BlockInfo>,
321 pool_counts: HashMap<String, u64>,
324}
325
326impl MarketState {
327 pub fn new() -> Self {
329 Self {
330 label: String::new(),
331 components: HashMap::new(),
332 simulation_states: HashMap::new(),
333 tokens: HashMap::new(),
334 gas_price: None,
335 protocol_sync_status: HashMap::new(),
336 last_updated: None,
337 pool_counts: HashMap::new(),
338 }
339 }
340
341 pub fn label(&self) -> &StateLabel {
343 &self.label
344 }
345
346 pub fn last_updated(&self) -> Option<&BlockInfo> {
348 self.last_updated.as_ref()
349 }
350
351 pub fn component_count(&self) -> usize {
353 self.components.len()
354 }
355
356 pub fn token_count(&self) -> usize {
358 self.tokens.len()
359 }
360
361 pub fn pool_counts_by_protocol(&self) -> &HashMap<String, u64> {
366 &self.pool_counts
367 }
368
369 pub fn protocol_sync_states(&self) -> &HashMap<String, SynchronizerState> {
371 &self.protocol_sync_status
372 }
373
374 pub fn get_protocol_sync_status(&self, protocol_system: &String) -> Option<&SynchronizerState> {
376 self.protocol_sync_status
377 .get(protocol_system)
378 }
379
380 pub fn component_topology(&self) -> HashMap<ComponentId, Vec<Address>> {
383 self.components
384 .iter()
385 .map(|(id, component)| (id.clone(), component.tokens.clone()))
386 .collect()
387 }
388
389 pub fn get_component(&self, id: &str) -> Option<&ProtocolComponent> {
391 self.components.get(id)
392 }
393
394 pub fn get_simulation_state(&self, id: &str) -> Option<&dyn ProtocolSim> {
396 self.simulation_states
397 .get(id)
398 .map(|b| b.as_ref())
399 }
400
401 pub fn get_token(&self, address: &Address) -> Option<&Token> {
403 self.tokens.get(address)
404 }
405
406 pub fn gas_price(&self) -> Option<&BlockGasPrice> {
408 self.gas_price.as_ref()
409 }
410
411 pub fn token_registry_ref(&self) -> &HashMap<Address, Token> {
413 &self.tokens
414 }
415
416 pub fn upsert_components(&mut self, components: impl IntoIterator<Item = ProtocolComponent>) {
418 for component in components {
419 let protocol_system = component.protocol_system.clone();
420 let previous = self
421 .components
422 .insert(component.id.clone(), component);
423 if previous.is_none() {
424 *self
425 .pool_counts
426 .entry(protocol_system)
427 .or_default() += 1;
428 }
429 }
430 }
431
432 pub fn upsert_tokens(&mut self, tokens: impl IntoIterator<Item = Token>) {
434 for token in tokens {
435 self.tokens
436 .insert(token.address.clone(), token);
437 }
438 }
439
440 pub fn update_protocol_sync_status(
442 &mut self,
443 sync_states: impl IntoIterator<Item = (String, SynchronizerState)>,
444 ) {
445 for (protocol_system, status) in sync_states {
446 self.protocol_sync_status
447 .insert(protocol_system, status);
448 }
449 }
450
451 pub fn remove_components<'a>(&mut self, ids: impl IntoIterator<Item = &'a ComponentId>) {
453 for id in ids {
454 if let Some(component) = self.components.remove(id) {
455 if let Some(count) = self
456 .pool_counts
457 .get_mut(&component.protocol_system)
458 {
459 *count = count.saturating_sub(1);
460 }
461 }
462 self.simulation_states.remove(id);
463 }
464 }
465
466 pub fn update_states(
468 &mut self,
469 states: impl IntoIterator<Item = (ComponentId, Box<dyn ProtocolSim>)>,
470 ) {
471 for (id, state) in states {
472 self.simulation_states.insert(id, state);
473 }
474 }
475
476 pub fn update_gas_price(&mut self, gas_price: BlockGasPrice) {
478 self.gas_price = Some(gas_price);
479 }
480
481 pub fn update_last_updated(&mut self, block_info: BlockInfo) {
483 self.last_updated = Some(block_info);
484 }
485
486 pub fn extract_subset(&self, component_ids: &HashSet<ComponentId>) -> MarketState {
495 let components: HashMap<ComponentId, ProtocolComponent> = self
497 .components
498 .iter()
499 .filter(|(id, _)| component_ids.contains(*id))
500 .map(|(id, component)| (id.clone(), component.clone()))
501 .collect();
502
503 let token_addresses: HashSet<&Address> = components
505 .values()
506 .flat_map(|c| &c.tokens)
507 .collect();
508
509 let tokens: HashMap<Address, Token> = self
511 .tokens
512 .iter()
513 .filter(|(addr, _)| token_addresses.contains(addr))
514 .map(|(addr, token)| (addr.clone(), token.clone()))
515 .collect();
516
517 let simulation_states: HashMap<ComponentId, Box<dyn ProtocolSim>> = self
519 .simulation_states
520 .iter()
521 .filter(|(id, _)| component_ids.contains(*id))
522 .map(|(id, state)| (id.clone(), state.clone_box()))
523 .collect();
524
525 MarketState {
526 label: self.label.clone(),
527 components,
528 simulation_states,
529 tokens,
530 gas_price: self.gas_price.clone(),
531 protocol_sync_status: HashMap::new(), last_updated: self.last_updated.clone(),
533 pool_counts: HashMap::new(), }
535 }
536}
537
538#[cfg(test)]
539mod tests {
540 use num_bigint::BigUint;
541 use tycho_simulation::tycho_ethereum::gas::GasPrice;
542
543 use super::*;
544 use crate::algorithm::test_utils::{
545 component, component_with_protocol, token, MockProtocolSim,
546 };
547
548 #[test]
549 fn pool_counts_by_protocol_tracks_upserts_and_removals() {
550 let mut market = MarketState::new();
551 let pool_tokens = [token(0x0A, "A"), token(0x0B, "B")];
552
553 market.upsert_components([
554 component_with_protocol("pool_1", "uniswap_v2", &pool_tokens),
555 component_with_protocol("pool_2", "uniswap_v2", &pool_tokens),
556 component_with_protocol("pool_3", "uniswap_v3", &pool_tokens),
557 ]);
558 let counts = market.pool_counts_by_protocol();
559 assert_eq!(counts.get("uniswap_v2"), Some(&2));
560 assert_eq!(counts.get("uniswap_v3"), Some(&1));
561
562 market.upsert_components([component_with_protocol("pool_1", "uniswap_v2", &pool_tokens)]);
564 assert_eq!(
565 market
566 .pool_counts_by_protocol()
567 .get("uniswap_v2"),
568 Some(&2)
569 );
570
571 let removed_ids = ["pool_1".to_string(), "pool_3".to_string()];
574 market.remove_components(removed_ids.iter());
575 let counts = market.pool_counts_by_protocol();
576 assert_eq!(counts.get("uniswap_v2"), Some(&1));
577 assert_eq!(counts.get("uniswap_v3"), Some(&0));
578
579 let unknown_ids = ["unknown_pool".to_string()];
581 market.remove_components(unknown_ids.iter());
582 assert_eq!(
583 market
584 .pool_counts_by_protocol()
585 .get("uniswap_v2"),
586 Some(&1)
587 );
588 }
589
590 #[test]
591 fn extract_subset_filters_by_component_ids() {
592 let mut market = MarketState::new();
594
595 let token_a = token(0x0A, "A");
596 let token_b = token(0x0B, "B");
597 let token_c = token(0x0C, "C");
598
599 market.upsert_components([
600 component("pool_ab", &[token_a.clone(), token_b.clone()]),
601 component("pool_bc", &[token_b.clone(), token_c.clone()]),
602 ]);
603 market.upsert_tokens([token_a.clone(), token_b.clone(), token_c.clone()]);
604 market.update_states([
605 ("pool_ab".to_string(), Box::new(MockProtocolSim::new(2.0)) as Box<dyn ProtocolSim>),
606 ("pool_bc".to_string(), Box::new(MockProtocolSim::new(3.0)) as Box<dyn ProtocolSim>),
607 ]);
608 market.update_gas_price(BlockGasPrice {
609 block_number: 1,
610 block_hash: Default::default(),
611 block_timestamp: 0,
612 pricing: GasPrice::Legacy { gas_price: BigUint::from(1u64) },
613 });
614 market.update_last_updated(BlockInfo::new(12345, "0xabc".to_string(), 0));
615
616 let ids: HashSet<_> = ["pool_ab".to_string()]
618 .into_iter()
619 .collect();
620 let subset = market.extract_subset(&ids);
621
622 assert_eq!(subset.components.len(), 1);
624 assert!(subset
625 .components
626 .contains_key("pool_ab"));
627
628 assert_eq!(subset.tokens.len(), 2);
630 assert!(subset
631 .tokens
632 .contains_key(&token_a.address));
633 assert!(subset
634 .tokens
635 .contains_key(&token_b.address));
636 assert!(!subset
637 .tokens
638 .contains_key(&token_c.address));
639
640 assert_eq!(subset.simulation_states.len(), 1);
642 assert!(subset
643 .simulation_states
644 .contains_key("pool_ab"));
645
646 assert_eq!(subset.gas_price, market.gas_price);
648 assert!(subset.last_updated.is_some());
649
650 let empty_subset = market.extract_subset(&HashSet::new());
652 assert!(empty_subset.components.is_empty());
653 assert!(empty_subset.tokens.is_empty());
654 assert!(empty_subset
655 .simulation_states
656 .is_empty());
657 }
658
659 #[tokio::test]
662 async fn register_and_retrieve_overlay_via_labeled_read() {
663 let market_ref = MarketData::new_shared();
664
665 let label = "test_label".to_string();
666 let mut states: HashMap<ComponentId, Box<dyn ProtocolSim>> = HashMap::new();
667 states.insert(
668 "pool_ab".to_string(),
669 Box::new(MockProtocolSim::new(99.0)) as Box<dyn ProtocolSim>,
670 );
671
672 market_ref
673 .register_labeled_state(label.clone(), states, u64::MAX)
674 .await;
675
676 let guard = market_ref
677 .read_labeled(&label)
678 .await
679 .expect("label was just registered");
680 let sim = guard.get_simulation_state("pool_ab");
682 assert!(sim.is_some());
683 }
684
685 #[tokio::test]
686 async fn read_without_label_returns_no_overlay() {
687 let market_ref = MarketData::new_shared();
688
689 market_ref
690 .register_labeled_state(
691 "my_label".to_string(),
692 HashMap::from([(
693 "pool1".to_string(),
694 Box::new(MockProtocolSim::new(5.0)) as Box<dyn ProtocolSim>,
695 )]),
696 u64::MAX,
697 )
698 .await;
699
700 let guard = market_ref.read().await;
702 assert!(guard
703 .get_simulation_state("pool1")
704 .is_none());
705 }
706
707 #[tokio::test]
708 async fn remove_labeled_state_clears_overlay() {
709 let market_ref = MarketData::new_shared();
710 let label = "lbl".to_string();
711
712 market_ref
713 .register_labeled_state(
714 label.clone(),
715 HashMap::from([(
716 "pool".to_string(),
717 Box::new(MockProtocolSim::new(1.0)) as Box<dyn ProtocolSim>,
718 )]),
719 u64::MAX,
720 )
721 .await;
722
723 market_ref
724 .remove_labeled_state(&label)
725 .await;
726
727 let ids = market_ref.labeled_state_ids().await;
728 assert!(ids.is_empty());
729 }
730
731 #[tokio::test]
732 async fn clear_labeled_states_removes_all() {
733 let market_ref = MarketData::new_shared();
734
735 for i in 0..3u8 {
736 market_ref
737 .register_labeled_state(
738 format!("label_{i}"),
739 HashMap::from([(
740 format!("pool_{i}"),
741 Box::new(MockProtocolSim::new(f64::from(i))) as Box<dyn ProtocolSim>,
742 )]),
743 u64::MAX,
744 )
745 .await;
746 }
747
748 market_ref.clear_labeled_states().await;
749 assert!(market_ref
750 .labeled_state_ids()
751 .await
752 .is_empty());
753 }
754
755 #[tokio::test]
756 async fn clone_shares_overlay_registry() {
757 let base = MarketData::new_shared();
760 let clone_a = base.clone();
761 let clone_b = base.clone();
762
763 base.register_labeled_state(
764 "shared".to_string(),
765 HashMap::from([(
766 "pool_x".to_string(),
767 Box::new(MockProtocolSim::new(7.0)) as Box<dyn ProtocolSim>,
768 )]),
769 u64::MAX,
770 )
771 .await;
772
773 let label = "shared".to_string();
774 let guard_a = clone_a
775 .read_labeled(&label)
776 .await
777 .expect("label was just registered");
778 assert!(guard_a
779 .get_simulation_state("pool_x")
780 .is_some());
781 drop(guard_a);
782
783 let guard_b = clone_b
784 .read_labeled(&label)
785 .await
786 .expect("label was just registered");
787 assert!(guard_b
788 .get_simulation_state("pool_x")
789 .is_some());
790 }
791
792 #[tokio::test]
793 async fn extract_subset_with_overlay_replaces_matching_states() {
794 use crate::algorithm::test_utils::{component as mk_component, token as mk_token};
795
796 let market_ref = MarketData::new_shared();
797
798 let tok_a = mk_token(0x01, "A");
799 let tok_b = mk_token(0x02, "B");
800
801 {
802 let mut data = market_ref.write().await;
803 data.upsert_components([mk_component("pool_ab", &[tok_a.clone(), tok_b.clone()])]);
804 data.upsert_tokens([tok_a.clone(), tok_b.clone()]);
805 data.update_states([(
806 "pool_ab".to_string(),
807 Box::new(MockProtocolSim::new(2.0)) as Box<dyn ProtocolSim>,
808 )]);
809 }
810
811 let label = "overlay".to_string();
812 market_ref
813 .register_labeled_state(
814 label.clone(),
815 HashMap::from([(
816 "pool_ab".to_string(),
817 Box::new(MockProtocolSim::new(99.0)) as Box<dyn ProtocolSim>,
818 )]),
819 u64::MAX,
820 )
821 .await;
822
823 let guard = market_ref
824 .read_labeled(&label)
825 .await
826 .expect("label was just registered");
827 let ids: HashSet<ComponentId> = ["pool_ab".to_string()]
828 .into_iter()
829 .collect();
830 let subset = guard.extract_subset_with_overlay(&ids);
831
832 let sim = subset
833 .get_simulation_state("pool_ab")
834 .unwrap();
835 let mock = sim
836 .as_any()
837 .downcast_ref::<MockProtocolSim>()
838 .unwrap();
839 assert_eq!(mock.spot_price, 99.0, "overlay state should replace base state");
840 }
841
842 #[tokio::test]
843 async fn apply_block_update_evicts_stale_overlays() {
844 let market_ref = MarketData::new_shared();
845
846 market_ref
848 .register_labeled_state(
849 "stale".to_string(),
850 HashMap::from([(
851 "pool_stale".to_string(),
852 Box::new(MockProtocolSim::new(1.0)) as Box<dyn ProtocolSim>,
853 )]),
854 10,
855 )
856 .await;
857 market_ref
858 .register_labeled_state(
859 "fresh".to_string(),
860 HashMap::from([(
861 "pool_fresh".to_string(),
862 Box::new(MockProtocolSim::new(2.0)) as Box<dyn ProtocolSim>,
863 )]),
864 20,
865 )
866 .await;
867
868 market_ref
870 .apply_block_update(11, |_data| {})
871 .await;
872
873 let ids = market_ref.labeled_state_ids().await;
874 assert!(!ids.contains(&"stale".to_string()), "stale overlay must be evicted");
875 assert!(ids.contains(&"fresh".to_string()), "fresh overlay must survive");
876 }
877
878 #[tokio::test]
879 async fn apply_block_update_applies_mutation() {
880 let market_ref = MarketData::new_shared();
881
882 market_ref
883 .apply_block_update(1, |data| {
884 data.update_last_updated(BlockInfo::new(1, "0xabc".to_string(), 0));
885 })
886 .await;
887
888 let guard = market_ref.read().await;
889 assert_eq!(
890 guard
891 .last_updated()
892 .expect("last_updated must be set")
893 .number(),
894 1
895 );
896 }
897
898 #[tokio::test]
899 async fn component_and_token_counts_track_upserts_and_removals() {
900 let market = MarketData::new_shared();
901 let tok_a = token(1, "A");
902 let tok_b = token(2, "B");
903
904 market
905 .apply_block_update(1, |data| {
906 data.upsert_components([component("pool_ab", &[tok_a.clone(), tok_b.clone()])]);
907 data.upsert_tokens([tok_a.clone(), tok_b.clone()]);
908 })
909 .await;
910 {
911 let data = market.read().await;
912 assert_eq!(
913 data.base_market_state()
914 .component_count(),
915 1
916 );
917 assert_eq!(data.base_market_state().token_count(), 2);
918 }
919
920 market
921 .apply_block_update(2, |data| {
922 data.remove_components(["pool_ab".to_string()].iter());
923 })
924 .await;
925 let data = market.read().await;
926 assert_eq!(
927 data.base_market_state()
928 .component_count(),
929 0
930 );
931 assert_eq!(
932 data.base_market_state().token_count(),
933 2,
934 "tokens are not removed with their components"
935 );
936 }
937}