1use bytemuck::{Pod, Zeroable};
2#[cfg(feature = "decimal")]
3use rust_decimal::Decimal;
4
5use crate::error::BasisPointsError;
6pub mod error;
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq, Zeroable, Pod)]
9#[cfg_attr(
10 feature = "anchor",
11 derive(
12 anchor_lang::InitSpace,
13 anchor_lang::prelude::AnchorSerialize,
14 anchor_lang::prelude::AnchorDeserialize
15 )
16)]
17#[repr(C)]
18pub struct BasisPoints(u16);
20
21impl BasisPoints {
22 pub const MAX: u16 = 10000;
24
25 pub fn new(bps: u16) -> Result<Self, BasisPointsError> {
29 if bps > Self::MAX {
30 return Err(BasisPointsError::InvalidBasisPoints);
31 }
32
33 Ok(Self(bps))
34 }
35}
36
37impl From<BasisPoints> for u16 {
38 fn from(bps: BasisPoints) -> Self {
39 bps.0
40 }
41}
42
43impl From<BasisPoints> for u32 {
44 fn from(bps: BasisPoints) -> Self {
45 u32::from(bps.0)
46 }
47}
48
49impl From<BasisPoints> for u64 {
50 fn from(bps: BasisPoints) -> Self {
51 u64::from(bps.0)
52 }
53}
54
55impl From<BasisPoints> for u128 {
56 fn from(bps: BasisPoints) -> Self {
57 u128::from(bps.0)
58 }
59}
60
61#[cfg(feature = "decimal")]
62impl From<BasisPoints> for Decimal {
63 fn from(bps: BasisPoints) -> Self {
64 Decimal::from(bps.0)
65 }
66}
67
68#[cfg(feature = "decimal")]
69impl TryFrom<Decimal> for BasisPoints {
70 type Error = BasisPointsError;
71
72 fn try_from(decimal: Decimal) -> Result<Self, Self::Error> {
73 let bps = decimal
74 .try_into()
75 .map_err(|_| BasisPointsError::ConversionFailed)?;
76 BasisPoints::new(bps)
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 #[test]
85 fn test_new() {
86 assert_eq!(BasisPoints::new(10000), Ok(BasisPoints(10000)));
87 }
88
89 #[test]
90 fn test_new_error() {
91 assert_eq!(
92 BasisPoints::new(10001),
93 Err(BasisPointsError::InvalidBasisPoints)
94 );
95 }
96
97 #[cfg(feature = "decimal")]
98 #[test]
99 fn test_from_basis_points_to_decimal() {
100 let bps = BasisPoints::new(1234).unwrap();
101 let decimal: Decimal = bps.into();
102
103 assert_eq!(decimal, Decimal::new(1234, 0));
104 }
105
106 #[cfg(feature = "decimal")]
107 #[test]
108 fn test_try_from_decimal_to_basis_points() {
109 let decimal = Decimal::new(4321, 0);
110 let bps = BasisPoints::try_from(decimal);
111
112 assert_eq!(bps, Ok(BasisPoints(4321)));
113 }
114
115 #[cfg(feature = "decimal")]
116 #[test]
117 fn test_round_trip_basis_points_decimal_basis_points() {
118 let original = BasisPoints::new(9999).unwrap();
119 let decimal: Decimal = original.into();
120 let round_trip = BasisPoints::try_from(decimal);
121
122 assert_eq!(round_trip, Ok(original));
123 }
124
125 #[cfg(feature = "decimal")]
126 #[test]
127 fn test_try_from_decimal_fractional_truncates() {
128 let decimal = Decimal::new(12345, 1); let bps = BasisPoints::try_from(decimal);
130
131 assert_eq!(bps, Ok(BasisPoints(1234)));
132 }
133
134 #[cfg(feature = "decimal")]
135 #[test]
136 fn test_try_from_decimal_above_basis_points_max_error() {
137 let decimal = Decimal::new(10001, 0);
138 let bps = BasisPoints::try_from(decimal);
139
140 assert_eq!(bps, Err(BasisPointsError::InvalidBasisPoints));
141 }
142
143 #[cfg(feature = "decimal")]
144 #[test]
145 fn test_try_from_decimal_u16_overflow_error() {
146 let decimal = Decimal::new(70000, 0);
147 let bps = BasisPoints::try_from(decimal);
148
149 assert_eq!(bps, Err(BasisPointsError::ConversionFailed));
150 }
151}