Skip to main content

fynd_core/graph/
petgraph.rs

1//! Petgraph's StableDiGraph implementation of GraphManager.
2//!
3//! This module provides PetgraphStableDiGraphManager, which implements GraphManager for
4//! petgraph::stable_graph::StableDiGraph, providing a reusable implementation for algorithms that
5//! use petgraph.
6//!
7//! A stable graph is a graph that maintains the indices of its edges even after removals. This is
8//! useful for optimising the graph manager's performance by allowing for O(1) edge and node
9//! lookups.
10
11use async_trait::async_trait;
12pub use petgraph::graph::EdgeIndex;
13use petgraph::{graph::NodeIndex, stable_graph};
14use rustc_hash::{FxHashMap, FxHashSet};
15use tracing::{debug, trace};
16use tycho_simulation::tycho_common::models::Address;
17
18use super::GraphManager;
19use crate::{
20    feed::{
21        events::{EventError, MarketEvent, MarketEventHandler},
22        market_data::MarketDataView,
23    },
24    graph::GraphError,
25    types::ComponentId,
26};
27
28/// Data stored on each edge of the graph.
29///
30/// Contains the component ID (which component this edge represents) and
31/// optional algorithm-specific data. The type `D` is generic to allow
32/// different algorithms to store their own scoring data.
33///
34/// # Type Parameters
35/// - `D`: Algorithm-specific data type. Defaults to `()` for no extra data.
36///
37/// # Examples
38/// ```ignore
39/// // For MostLiquid algorithm with depth/price data:
40/// use crate::algorithm::most_liquid::DepthAndPrice;
41/// type MostLiquidEdge = EdgeData<DepthAndPrice>;
42///
43/// // For algorithms that don't need extra data:
44/// type SimpleEdge = EdgeData<()>;
45/// ```
46#[derive(Debug, Clone, Default)]
47pub struct EdgeData<D = ()> {
48    /// The component ID that enables this swap.
49    pub component_id: ComponentId,
50    /// Algorithm-specific data. None if not yet computed.
51    pub data: Option<D>,
52}
53
54impl<M> EdgeData<M> {
55    /// Creates a new EdgeData with the given component ID and no data set.
56    pub fn new(component_id: ComponentId) -> Self {
57        Self { component_id, data: None }
58    }
59
60    /// Creates a new EdgeData with the given component ID and data.
61    pub fn with_data(component_id: ComponentId, data: M) -> Self {
62        Self { component_id, data: Some(data) }
63    }
64}
65
66/// A stable directed graph with token addresses as nodes and [`EdgeData`] as edge weights.
67pub type StableDiGraph<D> = stable_graph::StableDiGraph<Address, EdgeData<D>>;
68
69/// Petgraph implementation of GraphManager.
70///
71/// This struct implements GraphManager for petgraph::stable_graph::StableDiGraph.
72///
73/// The graph manager maintains the graph internally and updates it based on market events.
74/// Using StableDiGraph ensures edge indices remain valid after removals, making edge_map viable.
75pub struct PetgraphStableDiGraphManager<D: Clone> {
76    // Stable directed graph with token addresses as nodes and edge data (component id + weight) as
77    // edges. Using StableDiGraph ensures edge indices remain valid after removals, making
78    // edge_map viable.
79    graph: StableDiGraph<D>,
80    // Map from ComponentId to edge indices for fast removal and weight updates.
81    edge_map: FxHashMap<ComponentId, Vec<EdgeIndex>>,
82    // Map from token address to node index for fast node lookups.
83    node_map: FxHashMap<Address, NodeIndex>,
84}
85
86impl<D: Clone> PetgraphStableDiGraphManager<D> {
87    /// Creates a new empty graph manager.
88    pub fn new() -> Self {
89        Self {
90            graph: StableDiGraph::default(),
91            edge_map: FxHashMap::default(),
92            node_map: FxHashMap::default(),
93        }
94    }
95
96    /// Helper function to find a node index by address
97    pub(crate) fn find_node(&self, addr: &Address) -> Result<NodeIndex, GraphError> {
98        self.node_map
99            .get(addr)
100            .copied()
101            .ok_or_else(|| GraphError::TokenNotFound(addr.clone()))
102    }
103
104    /// Helper function to get or create a node for the given address.
105    /// Returns the node index, creating the node if it doesn't exist.
106    fn get_or_create_node(&mut self, addr: &Address) -> NodeIndex {
107        // Check if node already exists
108        match self.find_node(addr) {
109            Ok(node_idx) => node_idx,
110            Err(_) => {
111                let node_idx = self.graph.add_node(addr.clone());
112                self.node_map
113                    .insert(addr.clone(), node_idx);
114                node_idx
115            }
116        }
117    }
118
119    /// Helper function to add an edge to the graph.
120    ///
121    /// # Arguments
122    ///
123    /// * `from_idx` - The index of the from node.
124    /// * `to_idx` - The index of the to node.
125    /// * `component_id` - The ID of the component represented by this edge.
126    fn add_edge(&mut self, from_idx: NodeIndex, to_idx: NodeIndex, component_id: &ComponentId) {
127        let edge_idx = self
128            .graph
129            .add_edge(from_idx, to_idx, EdgeData::new(component_id.clone()));
130        self.edge_map
131            .entry(component_id.clone())
132            .or_default()
133            .push(edge_idx);
134    }
135
136    /// Helper function to add edges for all token pairs in a component.
137    /// Takes a slice of node indices corresponding to the tokens.
138    fn add_component_edges(&mut self, component_id: &ComponentId, node_indices: &[NodeIndex]) {
139        // Create bidirectional edges for each token pair
140        node_indices
141            .iter()
142            .enumerate()
143            .flat_map(|(i, &from_idx)| {
144                node_indices
145                    .iter()
146                    .skip(i + 1)
147                    .map(move |&to_idx| (from_idx, to_idx))
148            })
149            .for_each(|(from_idx, to_idx)| {
150                // Create bidirectional edges A -> B and B -> A
151                self.add_edge(from_idx, to_idx, component_id);
152                self.add_edge(to_idx, from_idx, component_id);
153            });
154    }
155
156    /// Adds components to the graph.
157    ///
158    /// # Errors
159    ///
160    /// Returns an error if any components have too few tokens (components must have at least 2
161    /// tokens). All components not included in the error were successfully added.
162    ///
163    /// Arguments:
164    /// - components: A map of component IDs to their tokens.
165    fn add_components(
166        &mut self,
167        components: &FxHashMap<ComponentId, Vec<Address>>,
168    ) -> Result<(), GraphError> {
169        let mut invalid_components = Vec::new();
170        let mut skipped_duplicates = 0usize;
171
172        // Sort components for deterministic node/edge insertion order.
173        let mut sorted_components: Vec<_> = components.iter().collect();
174        sorted_components.sort_by_key(|(id, _)| *id);
175
176        for (comp_id, tokens) in sorted_components {
177            if self.edge_map.contains_key(comp_id) {
178                trace!(component_id = %comp_id, "skipping already-tracked component");
179                skipped_duplicates += 1;
180                continue;
181            }
182
183            if tokens.len() < 2 {
184                invalid_components.push(comp_id.clone());
185                continue;
186            }
187            let mut sorted_tokens: Vec<&Address> = tokens.iter().collect();
188            sorted_tokens.sort();
189            let node_indices: Vec<NodeIndex> = sorted_tokens
190                .iter()
191                .map(|token| self.get_or_create_node(token))
192                .collect();
193            self.add_component_edges(comp_id, &node_indices);
194        }
195
196        if skipped_duplicates > 0 {
197            debug!(skipped_duplicates, "skipped duplicate components during add");
198        }
199
200        // Return error if any components had too few tokens (less than 2)
201        if !invalid_components.is_empty() {
202            return Err(GraphError::InvalidComponents(invalid_components));
203        }
204
205        Ok(())
206    }
207
208    /// Removes components from the graph.
209    ///
210    /// # Errors
211    ///
212    /// Returns an error if any components are not found in the graph. All components not included
213    /// in the error were successfully removed.
214    ///
215    /// Arguments:
216    /// - components: A vector of component IDs to remove.
217    fn remove_components(&mut self, components: &[ComponentId]) -> Result<(), GraphError> {
218        let mut missing_components = Vec::new();
219
220        for comp_id in components {
221            // Use the edge_map for O(1) lookup instead of iterating all edges
222            if let Some(edge_indices) = self.edge_map.remove(comp_id) {
223                for edge_idx in edge_indices {
224                    self.graph.remove_edge(edge_idx);
225                }
226            } else {
227                // Component not found in edge_map
228                missing_components.push(comp_id.clone());
229            }
230        }
231
232        // Return error if any components were not found
233        if !missing_components.is_empty() {
234            return Err(GraphError::ComponentsNotFound(missing_components));
235        }
236
237        Ok(())
238    }
239
240    /// Sets the weight for edges between the specified tokens with the given component ID.
241    ///
242    /// # Errors
243    ///
244    /// Returns an error if the component is not found in the graph for the given token pair.
245    ///
246    /// Arguments:
247    /// - component_id: The ID of the component to update.
248    /// - token_in: The input token.
249    /// - token_out: The output token.
250    /// - weight: The weight to set.
251    /// - If `bidirectional` is `true`, updates edges in both directions (token_in -> token_out and
252    ///   token_out -> token_in).
253    /// - If `bidirectional` is `false`, updates only the forward direction (token_in -> token_out).
254    #[cfg(test)]
255    pub(crate) fn set_edge_weight(
256        &mut self,
257        component_id: &ComponentId,
258        token_in: &Address,
259        token_out: &Address,
260        data: D,
261        bidirectional: bool,
262    ) -> Result<(), GraphError> {
263        let from_idx = self.find_node(token_in)?;
264        let to_idx = self.find_node(token_out)?;
265
266        // Get all edges for this component
267        let edge_indices = self
268            .edge_map
269            .get(component_id)
270            .ok_or_else(|| GraphError::ComponentsNotFound(vec![component_id.clone()]))?;
271
272        let mut updated = false;
273        for &edge_idx in edge_indices {
274            // Skip current edge if not found in graph, continue checking next edge
275            let (edge_from, edge_to) = match self.graph.edge_endpoints(edge_idx) {
276                Some(endpoints) => endpoints,
277                None => continue,
278            };
279
280            // Determine if we should update this edge based on edge tokens and bidirectional flag
281            let should_update = if bidirectional {
282                // Update both directions
283                (edge_from == from_idx && edge_to == to_idx) ||
284                    (edge_from == to_idx && edge_to == from_idx)
285            } else {
286                // Update only forward direction
287                edge_from == from_idx && edge_to == to_idx
288            };
289
290            if should_update {
291                // Error if edge weight is not found (edge is not in graph)
292                let edge_data = self
293                    .graph
294                    .edge_weight_mut(edge_idx)
295                    .ok_or_else(|| GraphError::ComponentsNotFound(vec![component_id.clone()]))?;
296                // Verify the component ID matches
297                if edge_data.component_id == *component_id {
298                    edge_data.data = Some(data.clone());
299                    updated = true;
300                }
301            }
302        }
303
304        if !updated {
305            return Err(GraphError::MissingComponentBetweenTokens(
306                token_in.clone(),
307                token_out.clone(),
308                component_id.clone(),
309            ));
310        }
311
312        Ok(())
313    }
314}
315
316impl<D: Clone + super::EdgeWeightFromSimAndDerived> PetgraphStableDiGraphManager<D> {
317    /// Updates edge weights using simulation states and pre-computed derived data.
318    ///
319    /// Uses pre-computed derived data (spot prices, component depths, etc.) to update
320    /// edge weights. This is more accurate than computing from scratch as it uses
321    /// data computed with slippage thresholds via `query_pool_swap` or binary search.
322    ///
323    /// # Arguments
324    ///
325    /// * `market` - The market data containing simulation states and tokens
326    /// * `derived` - Pre-computed derived data (component depths, spot prices, etc.)
327    ///
328    /// # Returns
329    ///
330    /// The number of edges successfully updated.
331    pub fn update_edge_weights_with_derived(
332        &mut self,
333        market: MarketDataView<'_>,
334        derived: &crate::derived::DerivedData,
335    ) -> usize {
336        let tokens = market.token_registry_ref();
337
338        // First pass: collect edge info and compute weights (immutable borrow).
339        // `None` in the inner Option means derived data is unavailable — the edge weight will be
340        // cleared to prevent stale data from influencing routing scores.
341        let updates: Vec<(EdgeIndex, Option<D>)> = self
342            .graph
343            .edge_indices()
344            .filter_map(|edge_idx| {
345                let edge_data = self.graph.edge_weight(edge_idx)?;
346                let component_id = &edge_data.component_id;
347
348                let sim_state = market.get_simulation_state(component_id)?;
349
350                let (source_idx, target_idx) = self.graph.edge_endpoints(edge_idx)?;
351                let source_addr = &self.graph[source_idx];
352                let target_addr = &self.graph[target_idx];
353
354                let token_in = tokens.get(source_addr)?;
355                let token_out = tokens.get(target_addr)?;
356
357                Some((
358                    edge_idx,
359                    D::from_sim_and_derived(sim_state, component_id, token_in, token_out, derived),
360                ))
361            })
362            .collect();
363
364        // Second pass: apply updates and clears (mutable borrow).
365        let updated = updates
366            .iter()
367            .filter(|(_, w)| w.is_some())
368            .count();
369        for (edge_idx, weight) in updates {
370            if let Some(edge_data) = self.graph.edge_weight_mut(edge_idx) {
371                edge_data.data = weight;
372            }
373        }
374
375        updated
376    }
377}
378
379impl<D: Clone + super::EdgeWeightFromSimAndDerived> super::EdgeWeightUpdaterWithDerived
380    for PetgraphStableDiGraphManager<D>
381{
382    fn update_edge_weights_with_derived(
383        &mut self,
384        market: MarketDataView<'_>,
385        derived: &crate::derived::DerivedData,
386    ) -> usize {
387        self.update_edge_weights_with_derived(market, derived)
388    }
389}
390
391impl<D: Clone> Default for PetgraphStableDiGraphManager<D> {
392    fn default() -> Self {
393        Self::new()
394    }
395}
396
397impl<D: Clone + Send + Sync> GraphManager<StableDiGraph<D>> for PetgraphStableDiGraphManager<D> {
398    fn initialize_graph(&mut self, component_topology: &FxHashMap<ComponentId, Vec<Address>>) {
399        // Clear existing graph and component map
400        self.graph = StableDiGraph::default();
401        self.edge_map.clear();
402        self.node_map.clear();
403
404        // Sort tokens for deterministic NodeIndex assignment across processes
405        // given the same input. HashMap/HashSet iteration order varies per
406        // process (random SipHash seeds), which would otherwise give different
407        // graph structure each run.
408        let mut unique_tokens: Vec<Address> = component_topology
409            .values()
410            .flat_map(|v| v.iter())
411            .cloned()
412            .collect::<FxHashSet<_>>()
413            .into_iter()
414            .collect();
415        unique_tokens.sort();
416
417        for token in unique_tokens {
418            let node_idx = self.graph.add_node(token.clone());
419            self.node_map.insert(token, node_idx);
420        }
421
422        // Sort components for deterministic edge insertion order.
423        let mut sorted_components: Vec<_> = component_topology.iter().collect();
424        sorted_components.sort_by_key(|(id, _)| *id);
425
426        for (comp_id, tokens) in sorted_components {
427            let mut sorted_tokens: Vec<&Address> = tokens.iter().collect();
428            sorted_tokens.sort();
429            let node_indices: Vec<NodeIndex> = sorted_tokens
430                .iter()
431                .map(|token| self.node_map[*token])
432                .collect();
433            self.add_component_edges(comp_id, &node_indices);
434        }
435    }
436
437    fn graph(&self) -> &StableDiGraph<D> {
438        &self.graph
439    }
440}
441
442#[async_trait]
443impl<D: Clone + Send> MarketEventHandler for PetgraphStableDiGraphManager<D> {
444    async fn handle_event(&mut self, event: &MarketEvent) -> Result<(), EventError> {
445        match event {
446            MarketEvent::MarketUpdated { added_components, removed_components, .. } => {
447                // Process both operations and collect all errors
448                let mut errors = Vec::new();
449
450                // Try to add components, collect error if it fails
451                if let Err(e) = self.add_components(added_components) {
452                    errors.push(e);
453                }
454
455                // Try to remove components, collect error if it fails
456                if let Err(e) = self.remove_components(removed_components) {
457                    errors.push(e);
458                }
459
460                // Return errors if any occurred
461                match errors.len() {
462                    0 => Ok(()),
463                    _ => Err(EventError::GraphErrors(errors)),
464                }
465            }
466        }
467    }
468}
469
470#[cfg(test)]
471mod tests {
472    use std::str::FromStr;
473
474    use super::*;
475
476    /// Helper function to create a test address from a hex string.
477    fn addr(s: &str) -> Address {
478        Address::from_str(s).expect("Invalid address hex string")
479    }
480
481    #[test]
482    fn test_initialize_graph_empty() {
483        let mut manager = PetgraphStableDiGraphManager::<()>::new();
484        let topology = FxHashMap::default();
485
486        manager.initialize_graph(&topology);
487
488        let graph = manager.graph();
489        assert_eq!(graph.node_count(), 0);
490        assert_eq!(graph.edge_count(), 0);
491    }
492
493    #[test]
494    fn test_initialize_graph_comprehensive() {
495        let mut manager = PetgraphStableDiGraphManager::<()>::new();
496        let mut topology = FxHashMap::default();
497        let token_a = addr("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"); // WETH
498        let token_b = addr("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); // USDC
499        let token_c = addr("0x6B175474E89094C44Da98b954EedeAC495271d0F"); // DAI
500        let token_d = addr("0xdAC17F958D2ee523a2206206994597C13D831ec7"); // USDT
501
502        // Component 1: A-B-C (3-token component, fully connected)
503        topology.insert(
504            "component1".to_string(),
505            vec![token_a.clone(), token_b.clone(), token_c.clone()],
506        );
507        // Component 2: C-D (2-token component, overlapping with component 1)
508        topology.insert("component2".to_string(), vec![token_c.clone(), token_d.clone()]);
509
510        manager.initialize_graph(&topology);
511
512        let graph = manager.graph();
513        // 4 unique tokens
514        assert_eq!(graph.node_count(), 4);
515        // Component 1: 3 pairs × 2 directions = 6 edges (A-B, B-A, A-C, C-A, B-C, C-B)
516        // Component 2: 1 pair × 2 directions = 2 edges (C-D, D-C)
517        // Total: 8 edges
518        assert_eq!(graph.edge_count(), 8);
519
520        // Verify edge labels are correct by checking specific token pairs
521        let node_a = manager.find_node(&token_a).unwrap();
522        let node_b = manager.find_node(&token_b).unwrap();
523        let node_c = manager.find_node(&token_c).unwrap();
524        let node_d = manager.find_node(&token_d).unwrap();
525
526        // Component 1 edges: A-B, B-A, A-C, C-A, B-C, C-B (bidirectional)
527        assert_eq!(
528            graph
529                .edge_weight(graph.find_edge(node_a, node_b).unwrap())
530                .unwrap()
531                .component_id,
532            "component1".to_string()
533        );
534        assert_eq!(
535            graph
536                .edge_weight(graph.find_edge(node_b, node_a).unwrap())
537                .unwrap()
538                .component_id,
539            "component1".to_string()
540        );
541        assert_eq!(
542            graph
543                .edge_weight(graph.find_edge(node_a, node_c).unwrap())
544                .unwrap()
545                .component_id,
546            "component1".to_string()
547        );
548        assert_eq!(
549            graph
550                .edge_weight(graph.find_edge(node_c, node_a).unwrap())
551                .unwrap()
552                .component_id,
553            "component1".to_string()
554        );
555        assert_eq!(
556            graph
557                .edge_weight(graph.find_edge(node_b, node_c).unwrap())
558                .unwrap()
559                .component_id,
560            "component1".to_string()
561        );
562        assert_eq!(
563            graph
564                .edge_weight(graph.find_edge(node_c, node_b).unwrap())
565                .unwrap()
566                .component_id,
567            "component1".to_string()
568        );
569
570        // Component 2 edges: C-D, D-C (bidirectional)
571        assert_eq!(
572            graph
573                .edge_weight(graph.find_edge(node_c, node_d).unwrap())
574                .unwrap()
575                .component_id,
576            "component2".to_string()
577        );
578        assert_eq!(
579            graph
580                .edge_weight(graph.find_edge(node_d, node_c).unwrap())
581                .unwrap()
582                .component_id,
583            "component2".to_string()
584        );
585    }
586
587    #[test]
588    fn test_initialize_graph_multiple_edges_same_pair() {
589        let mut manager = PetgraphStableDiGraphManager::<()>::new();
590        let mut topology = FxHashMap::default();
591        let token_a = addr("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"); // WETH
592        let token_b = addr("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); // USDC
593
594        // Multiple components connecting the same token pair
595        topology.insert("component1".to_string(), vec![token_a.clone(), token_b.clone()]);
596        topology.insert("component2".to_string(), vec![token_a.clone(), token_b.clone()]);
597        topology.insert("component3".to_string(), vec![token_a.clone(), token_b.clone()]);
598
599        manager.initialize_graph(&topology);
600
601        let graph = manager.graph();
602        // 2 unique tokens
603        assert_eq!(graph.node_count(), 2);
604        // 3 components × 1 pair × 2 directions = 6 edges between A-B
605        assert_eq!(graph.edge_count(), 6);
606
607        let node_a = manager.find_node(&token_a).unwrap();
608        let node_b = manager.find_node(&token_b).unwrap();
609
610        // Verify all three edges exist with correct component IDs
611        let edges: Vec<_> = graph
612            .edges_connecting(node_a, node_b)
613            .collect();
614        assert_eq!(edges.len(), 3);
615
616        let component_ids: Vec<_> = edges
617            .iter()
618            .map(|e| &e.weight().component_id)
619            .collect();
620
621        // Verify all three component IDs are present
622        assert!(component_ids.contains(&&"component1".to_string()));
623        assert!(component_ids.contains(&&"component2".to_string()));
624        assert!(component_ids.contains(&&"component3".to_string()));
625    }
626
627    #[test]
628    fn test_add_components_shared_tokens() {
629        let mut manager = PetgraphStableDiGraphManager::<()>::new();
630        let mut components = FxHashMap::default();
631        let token_a = addr("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"); // WETH
632        let token_b = addr("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); // USDC
633
634        // Add first component with token A and B
635        components.insert("component1".to_string(), vec![token_a.clone(), token_b.clone()]);
636        manager
637            .add_components(&components)
638            .unwrap();
639
640        let initial_node_count = manager.graph().node_count();
641        assert_eq!(initial_node_count, 2);
642
643        // Add second component with overlapping token A
644        components.clear();
645        components.insert("component2".to_string(), vec![token_a.clone(), token_b.clone()]);
646        manager
647            .add_components(&components)
648            .unwrap();
649
650        // Should still have only 2 nodes, not 3
651        assert_eq!(manager.graph().node_count(), 2, "Should not create duplicate nodes");
652    }
653
654    #[test]
655    fn test_add_tokenless_components_error() {
656        let mut manager = PetgraphStableDiGraphManager::<()>::new();
657        let mut components = FxHashMap::default();
658        let token_a = addr("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"); // WETH
659        let token_b = addr("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); // USDC
660
661        // Mix valid and invalid components
662        components.insert("component1".to_string(), vec![token_a.clone(), token_b.clone()]);
663        components.insert("component2".to_string(), vec![]);
664        components.insert("component3".to_string(), vec![]);
665        let result = manager.add_components(&components);
666
667        assert!(result.is_err());
668        match result.unwrap_err() {
669            GraphError::InvalidComponents(ids) => {
670                assert_eq!(ids.len(), 2);
671                assert!(ids.contains(&"component2".to_string()));
672                assert!(ids.contains(&"component3".to_string()));
673            }
674            _ => panic!("Expected InvalidComponents error"),
675        }
676
677        // Verify valid component was still added
678        assert_eq!(manager.graph().node_count(), 2);
679        assert_eq!(manager.graph().edge_count(), 2); // A-B and B-A
680    }
681
682    #[test]
683    fn test_remove_components_not_found_error() {
684        let mut manager = PetgraphStableDiGraphManager::<()>::new();
685        let mut components = FxHashMap::default();
686        let token_a = addr("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"); // WETH
687        let token_b = addr("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); // USDC
688
689        // Add components first
690        components.insert("component1".to_string(), vec![token_a.clone(), token_b.clone()]);
691        components.insert("component2".to_string(), vec![token_a.clone(), token_b.clone()]);
692        manager
693            .add_components(&components)
694            .unwrap();
695
696        // Try to remove mix of existing and non-existing components
697        let result = manager.remove_components(&[
698            "component1".to_string(),
699            "component3".to_string(),
700            "component4".to_string(),
701        ]);
702
703        assert!(result.is_err());
704        match result.unwrap_err() {
705            GraphError::ComponentsNotFound(ids) => {
706                assert_eq!(ids.len(), 2, "Expected 2 missing components");
707                assert!(ids.contains(&"component3".to_string()));
708                assert!(ids.contains(&"component4".to_string()));
709            }
710            _ => panic!("Expected ComponentsNotFound error"),
711        }
712
713        // Verify only component2 edges remain
714        for edge in manager.graph().edge_indices() {
715            assert_eq!(
716                manager
717                    .graph()
718                    .edge_weight(edge)
719                    .unwrap()
720                    .component_id,
721                "component2".to_string()
722            );
723        }
724    }
725
726    #[test]
727    fn test_set_edge_weight_errors() {
728        let mut manager = PetgraphStableDiGraphManager::<()>::new();
729        let mut topology = FxHashMap::default();
730        let token_a = addr("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"); // WETH
731        let token_b = addr("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); // USDC
732        let token_c = addr("0x6B175474E89094C44Da98b954EedeAC495271d0F"); // DAI
733
734        // Initialize with component1 connecting A-B, and component2 connecting B-C
735        topology.insert("component1".to_string(), vec![token_a.clone(), token_b.clone()]);
736        topology.insert("component2".to_string(), vec![token_b.clone(), token_c.clone()]);
737        manager.initialize_graph(&topology);
738
739        // Test 1: Component not found
740        let result =
741            manager.set_edge_weight(&"component3".to_string(), &token_a, &token_b, (), true);
742        assert!(result.is_err());
743        match result.unwrap_err() {
744            GraphError::ComponentsNotFound(ids) => {
745                assert_eq!(ids, vec!["component3".to_string()]);
746            }
747            _ => panic!("Expected ComponentsNotFound error"),
748        }
749
750        // Test 2: Token not found
751        let non_existent_token = addr("0x0000000000000000000000000000000000000000");
752        let result = manager.set_edge_weight(
753            &"component1".to_string(),
754            &token_a,
755            &non_existent_token, // Non-existent token
756            (),
757            true,
758        );
759        assert!(result.is_err());
760        match result.unwrap_err() {
761            GraphError::TokenNotFound(found_addr) => {
762                assert_eq!(found_addr, non_existent_token);
763            }
764            _ => panic!("Expected TokenNotFound error"),
765        }
766
767        // Test 3: Component doesn't connect the specified tokens
768        let result = manager.set_edge_weight(
769            &"component1".to_string(),
770            &token_a,
771            &token_c, // component1 doesn't connect A-C, only A-B
772            (),
773            true,
774        );
775        assert!(result.is_err());
776        match result.unwrap_err() {
777            GraphError::MissingComponentBetweenTokens(in_token, out_token, comp_id) => {
778                assert_eq!(in_token, token_a);
779                assert_eq!(out_token, token_c);
780                assert_eq!(comp_id, "component1".to_string());
781            }
782            _ => panic!("Expected MissingComponentBetweenTokens error"),
783        }
784    }
785
786    #[tokio::test]
787    async fn test_handle_event_propagates_errors() {
788        let mut manager = PetgraphStableDiGraphManager::<()>::new();
789
790        use crate::feed::events::{EventError, MarketEvent};
791
792        // Create an event with both add and remove operations that will fail
793        let event = MarketEvent::MarketUpdated {
794            added_components: FxHashMap::from_iter([("component1".to_string(), vec![])]),
795            removed_components: vec!["component2".to_string()],
796            updated_components: vec![],
797        };
798
799        let result = manager.handle_event(&event).await;
800
801        // Should return multiple errors
802        assert!(result.is_err());
803        match result.unwrap_err() {
804            EventError::GraphErrors(errors) => {
805                assert_eq!(errors.len(), 2);
806                // Check that we have both error types
807                let has_add_error = errors
808                    .iter()
809                    .any(|e| matches!(e, GraphError::InvalidComponents(_)));
810                let has_remove_error = errors
811                    .iter()
812                    .any(|e| matches!(e, GraphError::ComponentsNotFound(_)));
813                assert!(has_add_error, "Should have InvalidComponents error");
814                assert!(has_remove_error, "Should have ComponentsNotFound error");
815            }
816        }
817    }
818
819    #[test]
820    fn test_add_components_skips_duplicates() {
821        let mut manager = PetgraphStableDiGraphManager::<()>::new();
822        let token_a = addr("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
823        let token_b = addr("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
824
825        let mut components = FxHashMap::default();
826        components.insert("component1".to_string(), vec![token_a.clone(), token_b.clone()]);
827
828        manager
829            .add_components(&components)
830            .unwrap();
831        let edge_count_after_first = manager.graph().edge_count();
832        assert_eq!(edge_count_after_first, 2); // A->B and B->A
833
834        // Add the same component again
835        manager
836            .add_components(&components)
837            .unwrap();
838        let edge_count_after_second = manager.graph().edge_count();
839        assert_eq!(
840            edge_count_after_first, edge_count_after_second,
841            "Edge count should not change when re-adding the same component"
842        );
843    }
844}