1use bytemuck::{Pod, Zeroable};
11#[cfg(feature = "codama")]
12use codama::CodamaType;
13#[cfg(feature = "decimal")]
14use rust_decimal::Decimal;
15
16use crate::error::BasisPointsError;
17pub mod error;
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq, Zeroable, Pod)]
20#[cfg_attr(
21 feature = "anchor",
22 derive(
23 anchor_lang::InitSpace,
24 anchor_lang::prelude::AnchorSerialize,
25 anchor_lang::prelude::AnchorDeserialize
26 )
27)]
28#[cfg_attr(feature = "codama", derive(CodamaType))]
29#[repr(C)]
30pub struct BasisPoints(u16);
32
33impl BasisPoints {
34 pub const MAX: u16 = 10000;
36
37 pub fn new(bps: u16) -> Result<Self, BasisPointsError> {
41 if bps > Self::MAX {
42 return Err(BasisPointsError::InvalidBasisPoints);
43 }
44
45 Ok(Self(bps))
46 }
47
48 pub fn checked_add(self, rhs: Self) -> Option<Self> {
50 let lhs = u16::from(self);
51 let rhs = u16::from(rhs);
52 let result = lhs.checked_add(rhs)?;
53 Self::new(result).ok()
54 }
55
56 pub fn checked_sub(self, rhs: Self) -> Option<Self> {
58 let lhs = u16::from(self);
59 let rhs = u16::from(rhs);
60 let result = lhs.checked_sub(rhs)?;
61 Self::new(result).ok()
62 }
63
64 pub fn checked_mul(self, rhs: Self) -> Option<Self> {
66 let lhs = u16::from(self);
67 let rhs = u16::from(rhs);
68 let result = lhs.checked_mul(rhs)?;
69 Self::new(result).ok()
70 }
71
72 pub fn checked_div(self, rhs: Self) -> Option<Self> {
74 let lhs = u16::from(self);
75 let rhs = u16::from(rhs);
76 let result = lhs.checked_div(rhs)?;
77 Self::new(result).ok()
78 }
79}
80
81impl From<BasisPoints> for u16 {
82 fn from(bps: BasisPoints) -> Self {
83 bps.0
84 }
85}
86
87impl From<BasisPoints> for u32 {
88 fn from(bps: BasisPoints) -> Self {
89 u32::from(bps.0)
90 }
91}
92
93impl From<BasisPoints> for u64 {
94 fn from(bps: BasisPoints) -> Self {
95 u64::from(bps.0)
96 }
97}
98
99impl From<BasisPoints> for u128 {
100 fn from(bps: BasisPoints) -> Self {
101 u128::from(bps.0)
102 }
103}
104
105#[cfg(feature = "decimal")]
106impl From<BasisPoints> for Decimal {
107 fn from(bps: BasisPoints) -> Self {
109 Decimal::from(bps.0) / Decimal::from(BasisPoints::MAX)
110 }
111}
112
113#[cfg(feature = "decimal")]
114impl TryFrom<Decimal> for BasisPoints {
115 type Error = BasisPointsError;
116
117 fn try_from(decimal: Decimal) -> Result<Self, Self::Error> {
121 let bps = (decimal * Decimal::from(BasisPoints::MAX))
122 .try_into()
123 .map_err(|_| BasisPointsError::ConversionFailed)?;
124 BasisPoints::new(bps)
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn test_new() {
134 assert_eq!(BasisPoints::new(10000), Ok(BasisPoints(10000)));
135 }
136
137 #[test]
138 fn test_new_error() {
139 assert_eq!(
140 BasisPoints::new(10001),
141 Err(BasisPointsError::InvalidBasisPoints)
142 );
143 }
144
145 #[test]
146 fn test_checked_add() {
147 let lhs = BasisPoints::new(2500).unwrap();
148 let rhs = BasisPoints::new(2500).unwrap();
149
150 assert_eq!(lhs.checked_add(rhs), BasisPoints::new(5000).ok());
151 }
152
153 #[test]
154 fn test_checked_add_overflow() {
155 let lhs = BasisPoints::new(9000).unwrap();
156 let rhs = BasisPoints::new(2000).unwrap();
157
158 assert_eq!(lhs.checked_add(rhs), None);
159 }
160
161 #[test]
162 fn test_checked_sub() {
163 let lhs = BasisPoints::new(2500).unwrap();
164 let rhs = BasisPoints::new(500).unwrap();
165
166 assert_eq!(lhs.checked_sub(rhs), BasisPoints::new(2000).ok());
167 }
168
169 #[test]
170 fn test_checked_sub_underflow() {
171 let lhs = BasisPoints::new(500).unwrap();
172 let rhs = BasisPoints::new(2500).unwrap();
173
174 assert_eq!(lhs.checked_sub(rhs), None);
175 }
176
177 #[test]
178 fn test_checked_mul() {
179 let lhs = BasisPoints::new(100).unwrap();
180 let rhs = BasisPoints::new(50).unwrap();
181
182 assert_eq!(lhs.checked_mul(rhs), BasisPoints::new(5000).ok());
183 }
184
185 #[test]
186 fn test_checked_mul_overflow() {
187 let lhs = BasisPoints::new(200).unwrap();
188 let rhs = BasisPoints::new(100).unwrap();
189
190 assert_eq!(lhs.checked_mul(rhs), None);
191 }
192
193 #[test]
194 fn test_checked_div() {
195 let lhs = BasisPoints::new(5000).unwrap();
196 let rhs = BasisPoints::new(100).unwrap();
197
198 assert_eq!(lhs.checked_div(rhs), BasisPoints::new(50).ok());
199 }
200
201 #[test]
202 fn test_checked_div_by_zero() {
203 let lhs = BasisPoints::new(5000).unwrap();
204 let rhs = BasisPoints::new(0).unwrap();
205
206 assert_eq!(lhs.checked_div(rhs), None);
207 }
208
209 #[cfg(feature = "decimal")]
210 #[test]
211 fn test_from_basis_points_to_decimal() {
212 let bps = BasisPoints::new(1234).unwrap();
213 let decimal: Decimal = bps.into();
214
215 assert_eq!(decimal, Decimal::new(1234, 4));
216 }
217
218 #[cfg(feature = "decimal")]
219 #[test]
220 fn test_from_basis_points_max_to_decimal_one() {
221 let bps = BasisPoints::new(BasisPoints::MAX).unwrap();
222 let decimal: Decimal = bps.into();
223
224 assert_eq!(decimal, Decimal::ONE);
225 }
226
227 #[cfg(feature = "decimal")]
228 #[test]
229 fn test_try_from_decimal_to_basis_points() {
230 let decimal = Decimal::new(4321, 4);
231 let bps = BasisPoints::try_from(decimal);
232
233 assert_eq!(bps, Ok(BasisPoints(4321)));
234 }
235
236 #[cfg(feature = "decimal")]
237 #[test]
238 fn test_round_trip_basis_points_decimal_basis_points() {
239 let original = BasisPoints::new(9999).unwrap();
240 let decimal: Decimal = original.into();
241 let round_trip = BasisPoints::try_from(decimal);
242
243 assert_eq!(round_trip, Ok(original));
244 }
245
246 #[cfg(feature = "decimal")]
247 #[test]
248 fn test_try_from_decimal_fractional_truncates() {
249 let decimal = Decimal::new(12345, 5); let bps = BasisPoints::try_from(decimal);
251
252 assert_eq!(bps, Ok(BasisPoints(1234)));
253 }
254
255 #[cfg(feature = "decimal")]
256 #[test]
257 fn test_try_from_decimal_above_basis_points_max_error() {
258 let decimal = Decimal::new(10001, 4); let bps = BasisPoints::try_from(decimal);
260
261 assert_eq!(bps, Err(BasisPointsError::InvalidBasisPoints));
262 }
263
264 #[cfg(feature = "decimal")]
265 #[test]
266 fn test_try_from_decimal_u16_overflow_error() {
267 let decimal = Decimal::new(7, 0); let bps = BasisPoints::try_from(decimal);
269
270 assert_eq!(bps, Err(BasisPointsError::ConversionFailed));
271 }
272}