circles_pathfinder/
hub.rs

1//! # Circles Hub Contract Integration
2//!
3//! This module provides standard Circles Hub contract types and utilities for
4//! converting pathfinding results into contract-compatible formats.
5//!
6//! The types defined here match the exact ABI of the Circles Hub smart contract,
7//! ensuring seamless integration with contract calls.
8//!
9//! ## Usage
10//!
11//! ```rust,no_run
12//! use circles_pathfinder::{find_path_with_params, FindPathParams, PathData};
13//! use alloy_primitives::{Address, U256};
14//! use alloy_primitives::aliases::U192;
15//!
16//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
17//! let target_flow = U192::from(1000u64);
18//! let params = FindPathParams {
19//!     from: Address::ZERO,
20//!     to: Address::from([1u8; 20]),
21//!     target_flow: U256::from(target_flow),
22//!     use_wrapped_balances: Some(true),
23//!     from_tokens: None,
24//!     to_tokens: None,
25//!     exclude_from_tokens: None,
26//!     exclude_to_tokens: None,
27//!     simulated_balances: None,
28//!     max_transfers: None,
29//! };
30//!
31//! // Find the path
32//! let transfers = find_path_with_params("https://rpc.aboutcircles.com", params.clone()).await?;
33//!
34//! // Create PathData and convert to contract types
35//! let path_data = PathData::from_transfers(&transfers, params.from, params.to, target_flow)?;
36//! let (vertices, edges, streams, coords) = path_data.to_contract_params();
37//!
38//! // Ready for contract calls!
39//! // contract.some_function(vertices, edges, streams, coords).send().await?;
40//! # Ok(())
41//! # }
42//! ```
43
44use crate::{FlowEdge, FlowMatrix, PathfinderError, Stream, create_flow_matrix};
45use alloy_primitives::aliases::U192;
46use alloy_primitives::{Address, Bytes, U256};
47use circles_types::TransferStep;
48
49/// Simplified pathfinding result data structure
50///
51/// This struct contains the raw pathfinding results in a format that can be
52/// easily converted to contract-compatible types. It eliminates the need for
53/// manual field-by-field conversion between different type layers.
54#[derive(Debug, Clone)]
55pub struct PathData {
56    /// Sorted list of all addresses involved in the flow
57    pub flow_vertices: Vec<Address>,
58    /// Flow edges as (stream_sink_id, amount) tuples
59    pub flow_edges: Vec<FlowEdge>, // Vec<(u16, U192)>,
60    /// Streams as (source_coordinate, flow_edge_ids, data) tuples
61    pub streams: Vec<Stream>, // Vec<(u16, Vec<u16>, Vec<u8>)>,
62    /// Packed coordinates as raw bytes
63    pub packed_coordinates: Vec<u8>,
64    /// Source coordinate index
65    pub source_coordinate: U256,
66}
67
68impl PathData {
69    /// Create PathData from transfer steps
70    ///
71    /// This is the main constructor that takes the output from pathfinding
72    /// and creates a PathData structure ready for contract conversion.
73    ///
74    /// # Arguments
75    /// * `transfers` - Vector of transfer steps from pathfinding
76    /// * `from` - Source address
77    /// * `to` - Destination address
78    /// * `target_flow` - Target flow amount
79    ///
80    /// # Returns
81    /// A PathData structure ready for contract conversion
82    ///
83    /// # Errors
84    /// Returns PathfinderError if flow matrix creation fails
85    pub fn from_transfers(
86        transfers: &[TransferStep],
87        from: Address,
88        to: Address,
89        target_flow: U192,
90    ) -> Result<Self, PathfinderError> {
91        // Calculate actual available flow
92        let actual_flow: U192 = transfers
93            .iter()
94            .filter(|t| t.to_address == to)
95            .map(|t| t.value)
96            .sum();
97
98        // Use the smaller of target_flow or actual available flow
99        let flow_amount = if actual_flow < target_flow {
100            actual_flow
101        } else {
102            target_flow
103        };
104
105        // Create flow matrix
106        let matrix = create_flow_matrix(from, to, flow_amount, transfers)?;
107
108        Ok(Self::from_flow_matrix(matrix))
109    }
110
111    /// Create PathData from a FlowMatrix
112    ///
113    /// Internal constructor for converting from the core FlowMatrix type.
114    fn from_flow_matrix(matrix: FlowMatrix) -> Self {
115        Self {
116            flow_vertices: matrix.flow_vertices,
117            flow_edges: matrix.flow_edges,
118            streams: matrix.streams,
119            packed_coordinates: matrix.packed_coordinates,
120            source_coordinate: matrix.source_coordinate,
121        }
122    }
123
124    /// Convert to standard Circles Hub FlowEdge types
125    ///
126    /// Returns a vector of FlowEdge structs with the exact field names
127    /// and types expected by the Circles Hub smart contract.
128    ///
129    /// # Example
130    /// ```rust,no_run
131    /// # use circles_pathfinder::hub::PathData;
132    /// # use alloy_primitives::aliases::U192;
133    /// # use alloy_primitives::U256;
134    /// # use circles_pathfinder::FlowEdge;
135    /// # let path_data = PathData {
136    /// #     flow_vertices: vec![],
137    /// #     flow_edges: vec![FlowEdge { streamSinkId: 1, amount: U192::from(1000u64) }],
138    /// #     streams: vec![],
139    /// #     packed_coordinates: vec![],
140    /// #     source_coordinate: U256::from(0),
141    /// # };
142    /// let edges = path_data.to_flow_edges();
143    /// assert_eq!(edges[0].streamSinkId, 1);
144    /// assert_eq!(edges[0].amount, U192::from(1000u64));
145    /// ```
146    pub fn to_flow_edges(&self) -> Vec<FlowEdge> {
147        self.flow_edges
148            .iter()
149            .map(|flow_edge| FlowEdge {
150                streamSinkId: flow_edge.streamSinkId,
151                amount: flow_edge.amount,
152            })
153            .collect()
154    }
155
156    /// Convert to standard Circles Hub Stream types
157    ///
158    /// Returns a vector of Stream structs with the exact field names
159    /// and types expected by the Circles Hub smart contract.
160    ///
161    /// # Example
162    /// ```rust,no_run
163    /// # use circles_pathfinder::hub::PathData;
164    /// # use circles_pathfinder::Stream;
165    /// # use alloy_primitives::U256;
166    /// # let path_data = PathData {
167    /// #     flow_vertices: vec![],
168    /// #     flow_edges: vec![],
169    /// #     streams: vec![Stream { sourceCoordinate:0, flowEdgeIds: vec![1, 2], data: vec![0x01, 0x02].into(),}],
170    /// #     packed_coordinates: vec![],
171    /// #     source_coordinate: U256::from(0),
172    /// # };
173    /// let streams = path_data.to_streams();
174    /// assert_eq!(streams[0].sourceCoordinate, 0);
175    /// assert_eq!(streams[0].flowEdgeIds, vec![1, 2]);
176    /// ```
177    pub fn to_streams(&self) -> Vec<Stream> {
178        self.streams
179            .iter()
180            .map(|stream| Stream {
181                sourceCoordinate: stream.sourceCoordinate,
182                flowEdgeIds: stream.flowEdgeIds.clone(),
183                data: stream.data.clone(),
184            })
185            .collect()
186    }
187
188    /// Get all contract call parameters in one tuple
189    ///
190    /// This convenience method returns all the parameters needed for most
191    /// Circles Hub contract calls in the correct order and format.
192    ///
193    /// # Returns
194    /// A tuple of (flow_vertices, flow_edges, streams, packed_coordinates)
195    /// ready to use in contract function calls.
196    ///
197    /// # Example
198    /// ```rust,no_run
199    /// # use circles_pathfinder::hub::PathData;
200    /// # use alloy_primitives::U256;
201    /// # let path_data = PathData {
202    /// #     flow_vertices: vec![],
203    /// #     flow_edges: vec![],
204    /// #     streams: vec![],
205    /// #     packed_coordinates: vec![0x01, 0x02],
206    /// #     source_coordinate: U256::from(0),
207    /// # };
208    /// let (vertices, edges, streams, coords) = path_data.to_contract_params();
209    ///
210    /// // Ready for contract calls:
211    /// // contract.transferFlow(vertices, edges, streams, coords).send().await?;
212    /// ```
213    pub fn to_contract_params(&self) -> (Vec<Address>, Vec<FlowEdge>, Vec<Stream>, Bytes) {
214        (
215            self.flow_vertices.clone(),
216            self.to_flow_edges(),
217            self.to_streams(),
218            Bytes::from(self.packed_coordinates.clone()),
219        )
220    }
221
222    /// Get packed coordinates as Bytes
223    ///
224    /// Convenience method to get the packed coordinates in the Bytes format
225    /// expected by contract calls.
226    pub fn to_packed_coordinates(&self) -> Bytes {
227        Bytes::from(self.packed_coordinates.clone())
228    }
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234    use alloy_primitives::aliases::U192;
235
236    #[test]
237    fn test_flow_edge_creation() {
238        let edge = FlowEdge {
239            streamSinkId: 1,
240            amount: U192::from(1000u64),
241        };
242
243        assert_eq!(edge.streamSinkId, 1);
244        assert_eq!(edge.amount, U192::from(1000u64));
245    }
246
247    #[test]
248    fn test_stream_creation() {
249        let stream = Stream {
250            sourceCoordinate: 0,
251            flowEdgeIds: vec![1, 2, 3],
252            data: Bytes::from(vec![0x01, 0x02, 0x03]),
253        };
254
255        assert_eq!(stream.sourceCoordinate, 0);
256        assert_eq!(stream.flowEdgeIds, vec![1, 2, 3]);
257        assert_eq!(stream.data, Bytes::from(vec![0x01, 0x02, 0x03]));
258    }
259
260    // #[test]
261    // fn test_path_data_conversions() {
262    //     let path_data = PathData {
263    //         flow_vertices: vec![Address::ZERO],
264    //         flow_edges: vec![(1, U192::from(1000u64))],
265    //         streams: vec![(0, vec![0], vec![0x01, 0x02])],
266    //         packed_coordinates: vec![0x03, 0x04],
267    //         source_coordinate: 0,
268    //     };
269
270    //     // Test individual conversions
271    //     let edges = path_data.to_flow_edges();
272    //     assert_eq!(edges.len(), 1);
273    //     assert_eq!(edges[0].streamSinkId, 1);
274    //     assert_eq!(edges[0].amount, U192::from(1000u64));
275
276    //     let streams = path_data.to_streams();
277    //     assert_eq!(streams.len(), 1);
278    //     assert_eq!(streams[0].sourceCoordinate, 0);
279    //     assert_eq!(streams[0].flowEdgeIds, vec![0]);
280    //     assert_eq!(streams[0].data, Bytes::from(vec![0x01, 0x02]));
281
282    //     // Test combined conversion
283    //     let (vertices, edges, streams, coords) = path_data.to_contract_params();
284    //     assert_eq!(vertices, vec![Address::ZERO]);
285    //     assert_eq!(edges.len(), 1);
286    //     assert_eq!(streams.len(), 1);
287    //     assert_eq!(coords, Bytes::from(vec![0x03, 0x04]));
288    // }
289
290    #[test]
291    fn test_packed_coordinates_conversion() {
292        let path_data = PathData {
293            flow_vertices: vec![],
294            flow_edges: vec![],
295            streams: vec![],
296            packed_coordinates: vec![0x01, 0x02, 0x03],
297            source_coordinate: U256::from(0),
298        };
299
300        let coords = path_data.to_packed_coordinates();
301        assert_eq!(coords, Bytes::from(vec![0x01, 0x02, 0x03]));
302    }
303}