Skip to main content

fynd_core/derived/
store.rs

1//! Typed storage for derived data.
2
3use std::{any::Any, collections::HashMap, str::FromStr, sync::Arc};
4
5use tokio::sync::RwLock;
6use tycho_simulation::tycho_common::models::Address;
7
8use super::{
9    computation::{ComputationId, DerivedComputation, FailedItem, FailedItemError},
10    computations::{ComponentDepthComputation, SpotPriceComputation, TokenGasPriceComputation},
11    types::{
12        ComponentDepthKey, ComponentDepths, SpotPriceKey, SpotPrices, TokenGasPriceKey,
13        TokenGasPrices, TokenPricesWithDeps,
14    },
15};
16use crate::derived::SharedDerivedDataRef;
17
18/// A computed value paired with the block it was computed for.
19#[derive(Debug)]
20struct ComputedValue<T> {
21    data: T,
22    block: u64,
23}
24
25/// A type-erased computation output paired with the block it was computed for.
26struct ComputedSlot {
27    data: Box<dyn Any + Send + Sync>,
28    block: u64,
29}
30
31impl std::fmt::Debug for ComputedSlot {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        f.debug_struct("ComputedSlot")
34            .field("block", &self.block)
35            .finish_non_exhaustive()
36    }
37}
38
39/// Typed storage for derived data computations.
40///
41/// Computation outputs live in a type-keyed slot map written erased and read back
42/// typed by the per-computation getters below. The persistent failure maps stay
43/// typed because their merge logic is specific to each keyed output.
44#[derive(Debug, Default)]
45pub struct DerivedData {
46    /// Computation outputs keyed by [`ComputationId`], stored type-erased.
47    slots: HashMap<ComputationId, ComputedSlot>,
48    /// Persistent failure map: key → (block, error). Merged on incremental runs, replaced on full.
49    token_prices_failed: HashMap<TokenGasPriceKey, (u64, FailedItemError)>,
50    /// Token prices with path dependency tracking for incremental computation.
51    token_prices_deps: Option<ComputedValue<TokenPricesWithDeps>>,
52    /// Persistent failure map: key → (block, error). Merged on incremental runs, replaced on full.
53    component_depths_failed: HashMap<ComponentDepthKey, (u64, FailedItemError)>,
54    /// Persistent failure map: key → (block, error). Merged on incremental runs, replaced on full.
55    spot_prices_failed: HashMap<SpotPriceKey, (u64, FailedItemError)>,
56}
57
58/// Parses `"component_id/token_in/token_out"` into a typed `(ComponentId, Address, Address)` key.
59fn parse_pair_key(s: &str) -> Option<(String, Address, Address)> {
60    let mut parts = s.rsplitn(3, '/');
61    let token_out_str = parts.next()?;
62    let token_in_str = parts.next()?;
63    let component_id = parts.next()?;
64    let token_in = Address::from_str(token_in_str).ok()?;
65    let token_out = Address::from_str(token_out_str).ok()?;
66    Some((component_id.to_string(), token_in, token_out))
67}
68
69impl DerivedData {
70    /// Creates an empty store.
71    pub fn new() -> Self {
72        Self::default()
73    }
74
75    /// Creates a new shared derived data store for async computation tests that is wrapped in an
76    /// `Arc<RwLock<>>`.
77    pub fn new_shared() -> SharedDerivedDataRef {
78        Arc::new(RwLock::new(Self::new()))
79    }
80
81    /// Stores a computation's output under its id, type-erased, replacing any prior value.
82    pub(crate) fn set_output<T: Any + Send + Sync>(
83        &mut self,
84        id: ComputationId,
85        data: T,
86        block: u64,
87    ) {
88        self.slots
89            .insert(id, ComputedSlot { data: Box::new(data), block });
90    }
91
92    /// Returns the output stored under `id` downcast to `T`, or `None` if absent.
93    ///
94    /// Each id maps to a single output type, so reading an existing slot as the wrong `T`
95    /// is a programmer error: it trips a debug assertion and otherwise returns `None`.
96    pub(crate) fn output<T: Any>(&self, id: ComputationId) -> Option<&T> {
97        let slot = self.slots.get(id)?;
98        debug_assert!(slot.data.is::<T>(), "derived output {id} read as the wrong type");
99        slot.data.downcast_ref::<T>()
100    }
101
102    /// Returns the block at which the output under `id` was last computed.
103    pub(crate) fn output_block(&self, id: ComputationId) -> Option<u64> {
104        self.slots
105            .get(id)
106            .map(|slot| slot.block)
107    }
108
109    /// Removes the output stored under `id`.
110    fn clear_output(&mut self, id: ComputationId) {
111        self.slots.remove(id);
112    }
113
114    /// Returns `true` if all derived data types has been computed at least once.
115    pub fn derived_data_ready(&self) -> bool {
116        self.token_prices_block().is_some() &&
117            self.token_prices_deps_block().is_some() &&
118            self.component_depths_block().is_some() &&
119            self.spot_prices_block().is_some()
120    }
121
122    // -------------------------------------------------------------------------
123    // Token Prices
124    // -------------------------------------------------------------------------
125
126    /// Returns token prices if computed.
127    pub fn token_prices(&self) -> Option<&TokenGasPrices> {
128        self.output(TokenGasPriceComputation::ID)
129    }
130
131    /// Returns the block at which token prices were last computed.
132    pub fn token_prices_block(&self) -> Option<u64> {
133        self.output_block(TokenGasPriceComputation::ID)
134    }
135
136    /// Sets token prices, merging failures for incremental runs.
137    ///
138    /// For full recomputes, the failure map is replaced entirely. For incremental runs,
139    /// failures are merged: existing entries for keys that now succeed are removed, new
140    /// failures are inserted, and entries for keys not attempted this run are preserved.
141    pub fn set_token_prices(
142        &mut self,
143        prices: TokenGasPrices,
144        failed_items: Vec<FailedItem>,
145        block: u64,
146        is_full_recompute: bool,
147    ) {
148        let new_failures: HashMap<TokenGasPriceKey, (u64, FailedItemError)> = failed_items
149            .into_iter()
150            .filter_map(|f| {
151                Address::from_str(&f.key)
152                    .ok()
153                    .map(|k| (k, (block, f.error)))
154            })
155            .collect();
156
157        if is_full_recompute {
158            self.token_prices_failed = new_failures;
159        } else {
160            self.token_prices_failed
161                .retain(|k, _| !prices.contains_key(k));
162            self.token_prices_failed
163                .extend(new_failures);
164        }
165
166        self.set_output(TokenGasPriceComputation::ID, prices, block);
167    }
168
169    /// Returns `(block, error)` for this token address if it failed in a past
170    /// computation, or `None` if it succeeded or was not attempted.
171    pub fn token_price_failure(&self, key: &TokenGasPriceKey) -> Option<(u64, &FailedItemError)> {
172        self.token_prices_failed
173            .get(key)
174            .map(|(block, error)| (*block, error))
175    }
176
177    /// Clears token prices and their failure map.
178    pub fn clear_token_prices(&mut self) {
179        self.clear_output(TokenGasPriceComputation::ID);
180        self.token_prices_failed.clear();
181    }
182
183    // -------------------------------------------------------------------------
184    // Token Prices with Dependencies (for incremental computation)
185    // -------------------------------------------------------------------------
186
187    /// Returns token prices with path dependencies if computed.
188    pub fn token_prices_deps(&self) -> Option<&TokenPricesWithDeps> {
189        self.token_prices_deps
190            .as_ref()
191            .map(|v| &v.data)
192    }
193
194    /// Returns the block at which token prices with dependencies were last computed.
195    pub fn token_prices_deps_block(&self) -> Option<u64> {
196        self.token_prices_deps
197            .as_ref()
198            .map(|v| v.block)
199    }
200
201    /// Sets token prices with path dependencies.
202    pub fn set_token_prices_deps(&mut self, prices: TokenPricesWithDeps, block: u64) {
203        self.token_prices_deps = Some(ComputedValue { data: prices, block });
204    }
205
206    /// Clears token prices with dependencies.
207    pub fn clear_token_prices_deps(&mut self) {
208        self.token_prices_deps = None;
209    }
210
211    // -------------------------------------------------------------------------
212    // Component Depths
213    // -------------------------------------------------------------------------
214
215    /// Returns component depths if computed.
216    pub fn component_depths(&self) -> Option<&ComponentDepths> {
217        self.output(ComponentDepthComputation::ID)
218    }
219
220    /// Returns the block at which component depths were last computed.
221    pub fn component_depths_block(&self) -> Option<u64> {
222        self.output_block(ComponentDepthComputation::ID)
223    }
224
225    /// Sets component depths, merging failures for incremental runs.
226    ///
227    /// For full recomputes, the failure map is replaced entirely. For incremental runs,
228    /// failures are merged: existing entries for keys that now succeed are removed, new
229    /// failures are inserted, and entries for keys not attempted this run are preserved.
230    pub fn set_component_depths(
231        &mut self,
232        depths: ComponentDepths,
233        failed_items: Vec<FailedItem>,
234        block: u64,
235        is_full_recompute: bool,
236    ) {
237        let new_failures: HashMap<ComponentDepthKey, (u64, FailedItemError)> = failed_items
238            .into_iter()
239            .filter_map(|f| parse_pair_key(&f.key).map(|k| (k, (block, f.error))))
240            .collect();
241
242        if is_full_recompute {
243            self.component_depths_failed = new_failures;
244        } else {
245            self.component_depths_failed
246                .retain(|k, _| !depths.contains_key(k));
247            self.component_depths_failed
248                .extend(new_failures);
249        }
250
251        self.set_output(ComponentDepthComputation::ID, depths, block);
252    }
253
254    /// Returns `(block, error)` for this key if it failed in a past component depth
255    /// computation, or `None` if it succeeded or was not attempted.
256    ///
257    /// Key format: `(component_id, token_in, token_out)`
258    pub fn component_depth_failure(
259        &self,
260        key: &ComponentDepthKey,
261    ) -> Option<(u64, &FailedItemError)> {
262        self.component_depths_failed
263            .get(key)
264            .map(|(block, error)| (*block, error))
265    }
266
267    /// Clears component depths and their failure map.
268    pub fn clear_component_depths(&mut self) {
269        self.clear_output(ComponentDepthComputation::ID);
270        self.component_depths_failed.clear();
271    }
272
273    // -------------------------------------------------------------------------
274    // Spot Prices
275    // -------------------------------------------------------------------------
276
277    /// Returns spot prices if computed.
278    pub fn spot_prices(&self) -> Option<&SpotPrices> {
279        self.output(SpotPriceComputation::ID)
280    }
281
282    /// Returns the block at which spot prices were last computed.
283    pub fn spot_prices_block(&self) -> Option<u64> {
284        self.output_block(SpotPriceComputation::ID)
285    }
286
287    /// Sets spot prices, merging failures for incremental runs.
288    ///
289    /// For full recomputes, the failure map is replaced entirely. For incremental runs,
290    /// failures are merged: existing entries for keys that now succeed are removed, new
291    /// failures are inserted, and entries for keys not attempted this run are preserved.
292    pub fn set_spot_prices(
293        &mut self,
294        prices: SpotPrices,
295        failed_items: Vec<FailedItem>,
296        block: u64,
297        is_full_recompute: bool,
298    ) {
299        let new_failures: HashMap<SpotPriceKey, (u64, FailedItemError)> = failed_items
300            .into_iter()
301            .filter_map(|f| parse_pair_key(&f.key).map(|k| (k, (block, f.error))))
302            .collect();
303
304        if is_full_recompute {
305            self.spot_prices_failed = new_failures;
306        } else {
307            self.spot_prices_failed
308                .retain(|k, _| !prices.contains_key(k));
309            self.spot_prices_failed
310                .extend(new_failures);
311        }
312
313        self.set_output(SpotPriceComputation::ID, prices, block);
314    }
315
316    /// Returns `(block, error)` for this key if it failed in a past spot price
317    /// computation, or `None` if it succeeded or was not attempted.
318    ///
319    /// Key format: `(component_id, token_in, token_out)`
320    pub fn spot_price_failure(&self, key: &SpotPriceKey) -> Option<(u64, &FailedItemError)> {
321        self.spot_prices_failed
322            .get(key)
323            .map(|(block, error)| (*block, error))
324    }
325
326    /// Clears spot prices and their failure map.
327    pub fn clear_spot_prices(&mut self) {
328        self.clear_output(SpotPriceComputation::ID);
329        self.spot_prices_failed.clear();
330    }
331
332    // -------------------------------------------------------------------------
333    // Bulk Operations
334    // -------------------------------------------------------------------------
335
336    /// Clears all stored data, including all failure maps.
337    pub fn clear_all(&mut self) {
338        self.slots.clear();
339        self.token_prices_failed.clear();
340        self.token_prices_deps = None;
341        self.component_depths_failed.clear();
342        self.spot_prices_failed.clear();
343    }
344}
345
346#[cfg(test)]
347mod tests {
348    use super::*;
349    use crate::{algorithm::test_utils::addr, derived::types::SpotPrices};
350
351    fn failed(key: &str, error: FailedItemError) -> FailedItem {
352        FailedItem { key: key.to_string(), error }
353    }
354
355    fn pair_key(comp: &str, b_in: u8, b_out: u8) -> SpotPriceKey {
356        (comp.to_string(), addr(b_in), addr(b_out))
357    }
358
359    #[test]
360    fn test_token_prices_block_tracks_independently() {
361        let mut store = DerivedData::new();
362        assert_eq!(store.token_prices_block(), None);
363
364        store.set_token_prices(Default::default(), vec![], 42, true);
365        assert_eq!(store.token_prices_block(), Some(42));
366
367        // Other computations not set yet
368        assert_eq!(store.spot_prices_block(), None);
369        assert_eq!(store.component_depths_block(), None);
370    }
371
372    #[test]
373    fn test_spot_prices_block_tracks_independently() {
374        let mut store = DerivedData::new();
375        store.set_spot_prices(Default::default(), vec![], 10, true);
376        assert_eq!(store.spot_prices_block(), Some(10));
377        assert_eq!(store.token_prices_block(), None);
378    }
379
380    #[test]
381    fn test_component_depths_block_tracks_independently() {
382        let mut store = DerivedData::new();
383        store.set_component_depths(Default::default(), vec![], 7, true);
384        assert_eq!(store.component_depths_block(), Some(7));
385        assert_eq!(store.token_prices_block(), None);
386    }
387
388    #[test]
389    fn test_derived_data_ready() {
390        let mut store = DerivedData::new();
391        assert!(!store.derived_data_ready());
392
393        store.set_spot_prices(Default::default(), vec![], 5, true);
394        assert!(!store.derived_data_ready());
395
396        store.set_token_prices(Default::default(), vec![], 10, true);
397        assert!(!store.derived_data_ready());
398
399        store.set_token_prices_deps(Default::default(), 10);
400        assert!(!store.derived_data_ready());
401
402        store.set_component_depths(Default::default(), vec![], 9, true);
403        assert!(store.derived_data_ready());
404    }
405
406    #[test]
407    fn test_clear_all_resets_all_fields() {
408        let mut store = DerivedData::new();
409        store.set_token_prices(Default::default(), vec![], 1, true);
410        store.set_spot_prices(Default::default(), vec![], 1, true);
411        store.set_component_depths(Default::default(), vec![], 1, true);
412
413        store.clear_all();
414
415        assert!(store.token_prices().is_none());
416        assert!(store.spot_prices().is_none());
417        assert!(store.component_depths().is_none());
418        assert!(!store.derived_data_ready());
419    }
420
421    #[test]
422    fn test_token_price_failure_stored_with_block() {
423        let token_addr = addr(0xab);
424        let key_str = format!("{token_addr}");
425        let mut store = DerivedData::new();
426        store.set_token_prices(
427            Default::default(),
428            vec![failed(&key_str, FailedItemError::SimulationFailed("sim error".into()))],
429            42,
430            true,
431        );
432        assert_eq!(
433            store.token_price_failure(&token_addr),
434            Some((42, &FailedItemError::SimulationFailed("sim error".into())))
435        );
436        assert_eq!(store.token_price_failure(&addr(0xcd)), None);
437    }
438
439    #[test]
440    fn test_spot_price_failure_stored_with_block() {
441        let key = pair_key("component1", 0x01, 0x02);
442        let key_str = format!("component1/{}/{}", addr(0x01), addr(0x02));
443        let mut store = DerivedData::new();
444        store.set_spot_prices(
445            Default::default(),
446            vec![failed(&key_str, FailedItemError::SimulationFailed("sim error".into()))],
447            10,
448            true,
449        );
450        assert_eq!(
451            store.spot_price_failure(&key),
452            Some((10, &FailedItemError::SimulationFailed("sim error".into())))
453        );
454        assert_eq!(store.spot_price_failure(&pair_key("component1", 0x01, 0x03)), None);
455    }
456
457    #[test]
458    fn test_component_depth_failure_stored_with_block() {
459        let key: ComponentDepthKey = pair_key("component1", 0x01, 0x02);
460        let key_str = format!("component1/{}/{}", addr(0x01), addr(0x02));
461        let mut store = DerivedData::new();
462        store.set_component_depths(
463            Default::default(),
464            vec![failed(&key_str, FailedItemError::SimulationFailed("depth error".into()))],
465            7,
466            true,
467        );
468        assert_eq!(
469            store.component_depth_failure(&key),
470            Some((7, &FailedItemError::SimulationFailed("depth error".into())))
471        );
472        assert_eq!(store.component_depth_failure(&pair_key("component2", 0x01, 0x02)), None);
473    }
474
475    #[test]
476    fn test_rerunning_with_empty_failures_clears_old_reasons() {
477        let key = pair_key("component1", 0x01, 0x02);
478        let key_str = format!("component1/{}/{}", addr(0x01), addr(0x02));
479        let mut store = DerivedData::new();
480        store.set_spot_prices(
481            Default::default(),
482            vec![failed(&key_str, FailedItemError::MissingSimulationState)],
483            1,
484            true,
485        );
486        assert!(store.spot_price_failure(&key).is_some());
487
488        // Full re-run with no failures clears the map
489        store.set_spot_prices(Default::default(), vec![], 2, true);
490        assert_eq!(store.spot_price_failure(&key), None);
491    }
492
493    #[test]
494    fn test_clear_token_prices_clears_failure_map() {
495        let token_addr = addr(0xab);
496        let key_str = format!("{token_addr}");
497        let mut store = DerivedData::new();
498        store.set_token_prices(
499            Default::default(),
500            vec![failed(&key_str, FailedItemError::AllSimulationPathsFailed)],
501            1,
502            true,
503        );
504        store.clear_token_prices();
505        assert_eq!(store.token_price_failure(&token_addr), None);
506    }
507
508    #[test]
509    fn test_clear_spot_prices_clears_failure_map() {
510        let key = pair_key("component1", 0x01, 0x02);
511        let key_str = format!("component1/{}/{}", addr(0x01), addr(0x02));
512        let mut store = DerivedData::new();
513        store.set_spot_prices(
514            Default::default(),
515            vec![failed(&key_str, FailedItemError::MissingSimulationState)],
516            1,
517            true,
518        );
519        store.clear_spot_prices();
520        assert_eq!(store.spot_price_failure(&key), None);
521    }
522
523    #[test]
524    fn test_clear_component_depths_clears_failure_map() {
525        let key: ComponentDepthKey = pair_key("component1", 0x01, 0x02);
526        let key_str = format!("component1/{}/{}", addr(0x01), addr(0x02));
527        let mut store = DerivedData::new();
528        store.set_component_depths(
529            Default::default(),
530            vec![failed(&key_str, FailedItemError::MissingSpotPrice)],
531            1,
532            true,
533        );
534        store.clear_component_depths();
535        assert_eq!(store.component_depth_failure(&key), None);
536    }
537
538    #[test]
539    fn test_incremental_run_preserves_failures_for_unattempted_items() {
540        let key_a = pair_key("component_a", 0x01, 0x02);
541        let key_a_str = format!("component_a/{}/{}", addr(0x01), addr(0x02));
542        let key_b = pair_key("component_b", 0x03, 0x04);
543        let key_b_str = format!("component_b/{}/{}", addr(0x03), addr(0x04));
544
545        let mut store = DerivedData::new();
546
547        // Full recompute at block 10: both keys fail
548        store.set_spot_prices(
549            Default::default(),
550            vec![
551                failed(&key_a_str, FailedItemError::MissingSimulationState),
552                failed(&key_b_str, FailedItemError::MissingTokenMetadata),
553            ],
554            10,
555            true,
556        );
557        assert_eq!(
558            store.spot_price_failure(&key_a),
559            Some((10, &FailedItemError::MissingSimulationState))
560        );
561        assert_eq!(
562            store.spot_price_failure(&key_b),
563            Some((10, &FailedItemError::MissingTokenMetadata))
564        );
565
566        // Incremental run at block 11: only component_b is attempted and succeeds
567        let mut prices = SpotPrices::default();
568        prices.insert(key_b.clone(), 1.0);
569        store.set_spot_prices(prices, vec![], 11, false);
570
571        // component_a was not attempted — failure is preserved from block 10
572        assert_eq!(
573            store.spot_price_failure(&key_a),
574            Some((10, &FailedItemError::MissingSimulationState))
575        );
576        // component_b succeeded — failure is cleared
577        assert_eq!(store.spot_price_failure(&key_b), None);
578    }
579
580    #[test]
581    fn test_incremental_run_updates_block_on_repeated_failure() {
582        let key = pair_key("component_a", 0x01, 0x02);
583        let key_str = format!("component_a/{}/{}", addr(0x01), addr(0x02));
584
585        let mut store = DerivedData::new();
586
587        store.set_spot_prices(
588            Default::default(),
589            vec![failed(&key_str, FailedItemError::MissingSimulationState)],
590            10,
591            true,
592        );
593        assert_eq!(
594            store.spot_price_failure(&key),
595            Some((10, &FailedItemError::MissingSimulationState))
596        );
597
598        // Incremental run at block 11: component_a fails again with a new error
599        store.set_spot_prices(
600            Default::default(),
601            vec![failed(&key_str, FailedItemError::MissingTokenMetadata)],
602            11,
603            false,
604        );
605        assert_eq!(
606            store.spot_price_failure(&key),
607            Some((11, &FailedItemError::MissingTokenMetadata))
608        );
609    }
610
611    #[test]
612    fn test_clear_all_clears_all_failure_maps() {
613        let token_addr = addr(0xab);
614        let token_str = format!("{token_addr}");
615        let pair = pair_key("component1", 0x01, 0x02);
616        let pair_str = format!("component1/{}/{}", addr(0x01), addr(0x02));
617
618        let mut store = DerivedData::new();
619        store.set_token_prices(
620            Default::default(),
621            vec![failed(&token_str, FailedItemError::AllSimulationPathsFailed)],
622            1,
623            true,
624        );
625        store.set_spot_prices(
626            Default::default(),
627            vec![failed(&pair_str, FailedItemError::MissingSimulationState)],
628            1,
629            true,
630        );
631        store.set_component_depths(
632            Default::default(),
633            vec![failed(&pair_str, FailedItemError::MissingSpotPrice)],
634            1,
635            true,
636        );
637
638        store.clear_all();
639
640        assert_eq!(store.token_price_failure(&token_addr), None);
641        assert_eq!(store.spot_price_failure(&pair), None);
642        assert_eq!(store.component_depth_failure(&pair), None);
643    }
644}