odos_sdk/types/
slippage.rs

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