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}