odos_sdk/types/
slippage.rs

1use std::fmt;
2
3use serde::{Deserialize, Serialize};
4
5/// Type-safe slippage percentage with validation
6///
7/// Ensures slippage values are within valid ranges and provides convenient
8/// constructors for both percentage and basis point representations.
9///
10/// # Examples
11///
12/// ```rust
13/// use odos_sdk::Slippage;
14///
15/// // Create from percentage (0.5% slippage)
16/// let slippage = Slippage::percent(0.5)?;
17/// assert_eq!(slippage.as_percent(), 0.5);
18///
19/// // Create from basis points (50 bps = 0.5%)
20/// let slippage = Slippage::bps(50)?;
21/// assert_eq!(slippage.as_percent(), 0.5);
22/// assert_eq!(slippage.as_bps(), 50);
23///
24/// // Validation prevents invalid values
25/// assert!(Slippage::percent(150.0).is_err());  // > 100%
26/// assert!(Slippage::percent(-1.0).is_err());   // < 0%
27/// assert!(Slippage::bps(15000).is_err());      // > 10000 bps
28/// # Ok::<(), Box<dyn std::error::Error>>(())
29/// ```
30#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize)]
31#[serde(transparent)]
32pub struct Slippage(f64);
33
34impl Slippage {
35    /// Create slippage from percentage value
36    ///
37    /// # Arguments
38    ///
39    /// * `percent` - Slippage as percentage (e.g., 0.5 for 0.5%, 1.0 for 1%)
40    ///
41    /// # Returns
42    ///
43    /// * `Ok(Slippage)` - Valid slippage value
44    /// * `Err(String)` - If percentage is < 0 or > 100
45    ///
46    /// # Examples
47    ///
48    /// ```rust
49    /// use odos_sdk::Slippage;
50    ///
51    /// let slippage = Slippage::percent(0.5)?;  // 0.5%
52    /// assert_eq!(slippage.as_percent(), 0.5);
53    ///
54    /// let slippage = Slippage::percent(1.0)?;  // 1%
55    /// assert_eq!(slippage.as_percent(), 1.0);
56    ///
57    /// // Validation
58    /// assert!(Slippage::percent(150.0).is_err());
59    /// assert!(Slippage::percent(-0.1).is_err());
60    /// # Ok::<(), Box<dyn std::error::Error>>(())
61    /// ```
62    pub fn percent(percent: f64) -> Result<Self, String> {
63        if percent < 0.0 {
64            return Err(format!("Slippage percentage cannot be negative: {percent}"));
65        }
66        if percent > 100.0 {
67            return Err(format!("Slippage percentage cannot exceed 100%: {percent}"));
68        }
69        Ok(Self(percent))
70    }
71
72    /// Create slippage from basis points
73    ///
74    /// # Arguments
75    ///
76    /// * `bps` - Slippage in basis points (e.g., 50 for 0.5%, 100 for 1%)
77    ///
78    /// # Returns
79    ///
80    /// * `Ok(Slippage)` - Valid slippage value
81    /// * `Err(String)` - If basis points > 10000 (100%)
82    ///
83    /// # Examples
84    ///
85    /// ```rust
86    /// use odos_sdk::Slippage;
87    ///
88    /// let slippage = Slippage::bps(50)?;   // 50 bps = 0.5%
89    /// assert_eq!(slippage.as_bps(), 50);
90    /// assert_eq!(slippage.as_percent(), 0.5);
91    ///
92    /// let slippage = Slippage::bps(100)?;  // 100 bps = 1%
93    /// assert_eq!(slippage.as_bps(), 100);
94    /// assert_eq!(slippage.as_percent(), 1.0);
95    ///
96    /// // Validation
97    /// assert!(Slippage::bps(15000).is_err());
98    /// # Ok::<(), Box<dyn std::error::Error>>(())
99    /// ```
100    pub fn bps(bps: u16) -> Result<Self, String> {
101        if bps > 10000 {
102            return Err(format!(
103                "Slippage basis points cannot exceed 10000 (100%): {bps}"
104            ));
105        }
106        Ok(Self(bps as f64 / 100.0))
107    }
108
109    /// Get slippage value as percentage
110    ///
111    /// # Examples
112    ///
113    /// ```rust
114    /// use odos_sdk::Slippage;
115    ///
116    /// let slippage = Slippage::percent(0.5)?;
117    /// assert_eq!(slippage.as_percent(), 0.5);
118    /// # Ok::<(), Box<dyn std::error::Error>>(())
119    /// ```
120    pub fn as_percent(&self) -> f64 {
121        self.0
122    }
123
124    /// Get slippage value as basis points
125    ///
126    /// # Examples
127    ///
128    /// ```rust
129    /// use odos_sdk::Slippage;
130    ///
131    /// let slippage = Slippage::percent(0.5)?;
132    /// assert_eq!(slippage.as_bps(), 50);
133    ///
134    /// let slippage = Slippage::bps(100)?;
135    /// assert_eq!(slippage.as_bps(), 100);
136    /// # Ok::<(), Box<dyn std::error::Error>>(())
137    /// ```
138    pub fn as_bps(&self) -> u16 {
139        (self.0 * 100.0).round() as u16
140    }
141
142    /// Common slippage values for convenience
143    pub const ZERO: Result<Self, &'static str> = Ok(Self(0.0));
144
145    /// 0.1% slippage (10 basis points)
146    pub fn low() -> Self {
147        Self(0.1)
148    }
149
150    /// 0.5% slippage (50 basis points) - recommended for most swaps
151    pub fn standard() -> Self {
152        Self(0.5)
153    }
154
155    /// 1% slippage (100 basis points)
156    pub fn medium() -> Self {
157        Self(1.0)
158    }
159
160    /// 3% slippage (300 basis points) - high slippage for volatile pairs
161    pub fn high() -> Self {
162        Self(3.0)
163    }
164}
165
166impl fmt::Display for Slippage {
167    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168        write!(f, "{:.2}%", self.0)
169    }
170}
171
172impl From<Slippage> for f64 {
173    fn from(slippage: Slippage) -> Self {
174        slippage.0
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181
182    #[test]
183    fn test_percent_constructor() {
184        let slippage = Slippage::percent(0.5).unwrap();
185        assert_eq!(slippage.as_percent(), 0.5);
186
187        let slippage = Slippage::percent(1.0).unwrap();
188        assert_eq!(slippage.as_percent(), 1.0);
189
190        let slippage = Slippage::percent(100.0).unwrap();
191        assert_eq!(slippage.as_percent(), 100.0);
192
193        // Edge case: 0%
194        let slippage = Slippage::percent(0.0).unwrap();
195        assert_eq!(slippage.as_percent(), 0.0);
196    }
197
198    #[test]
199    fn test_percent_validation() {
200        // Too high
201        assert!(Slippage::percent(100.1).is_err());
202        assert!(Slippage::percent(150.0).is_err());
203
204        // Negative
205        assert!(Slippage::percent(-0.1).is_err());
206        assert!(Slippage::percent(-50.0).is_err());
207    }
208
209    #[test]
210    fn test_bps_constructor() {
211        let slippage = Slippage::bps(50).unwrap();
212        assert_eq!(slippage.as_bps(), 50);
213        assert_eq!(slippage.as_percent(), 0.5);
214
215        let slippage = Slippage::bps(100).unwrap();
216        assert_eq!(slippage.as_bps(), 100);
217        assert_eq!(slippage.as_percent(), 1.0);
218
219        let slippage = Slippage::bps(10000).unwrap();
220        assert_eq!(slippage.as_bps(), 10000);
221        assert_eq!(slippage.as_percent(), 100.0);
222
223        // Edge case: 0 bps
224        let slippage = Slippage::bps(0).unwrap();
225        assert_eq!(slippage.as_bps(), 0);
226        assert_eq!(slippage.as_percent(), 0.0);
227    }
228
229    #[test]
230    fn test_bps_validation() {
231        // Too high
232        assert!(Slippage::bps(10001).is_err());
233        assert!(Slippage::bps(15000).is_err());
234        assert!(Slippage::bps(u16::MAX).is_err());
235    }
236
237    #[test]
238    fn test_convenience_methods() {
239        assert_eq!(Slippage::low().as_percent(), 0.1);
240        assert_eq!(Slippage::standard().as_percent(), 0.5);
241        assert_eq!(Slippage::medium().as_percent(), 1.0);
242        assert_eq!(Slippage::high().as_percent(), 3.0);
243    }
244
245    #[test]
246    fn test_display() {
247        let slippage = Slippage::percent(0.5).unwrap();
248        assert_eq!(format!("{slippage}"), "0.50%");
249
250        let slippage = Slippage::bps(100).unwrap();
251        assert_eq!(format!("{slippage}"), "1.00%");
252
253        let slippage = Slippage::high();
254        assert_eq!(format!("{slippage}"), "3.00%");
255    }
256
257    #[test]
258    fn test_conversions() {
259        let slippage = Slippage::percent(0.5).unwrap();
260
261        // as_percent
262        assert_eq!(slippage.as_percent(), 0.5);
263
264        // as_bps
265        assert_eq!(slippage.as_bps(), 50);
266
267        // Into f64
268        let value: f64 = slippage.into();
269        assert_eq!(value, 0.5);
270    }
271
272    #[test]
273    fn test_serialization() {
274        let slippage = Slippage::percent(0.5).unwrap();
275
276        // Serialize
277        let json = serde_json::to_string(&slippage).unwrap();
278        assert_eq!(json, "0.5");
279
280        // Deserialize
281        let deserialized: Slippage = serde_json::from_str(&json).unwrap();
282        assert_eq!(deserialized.as_percent(), 0.5);
283    }
284
285    #[test]
286    fn test_equality_and_ordering() {
287        let s1 = Slippage::percent(0.5).unwrap();
288        let s2 = Slippage::bps(50).unwrap();
289        let s3 = Slippage::percent(1.0).unwrap();
290
291        assert_eq!(s1, s2);
292        assert_ne!(s1, s3);
293        assert!(s1 < s3);
294        assert!(s3 > s1);
295    }
296}