defuse_fees/
lib.rs

1use core::{
2    fmt::{self, Display},
3    ops::{Add, Div, Mul, Not, Sub},
4};
5
6use defuse_num_utils::{CheckedAdd, CheckedMulDiv, CheckedSub};
7use near_sdk::{
8    borsh::{BorshDeserialize, BorshSchema, BorshSerialize},
9    near,
10};
11use thiserror::Error as ThisError;
12
13/// 1 pip == 1/100th of bip == 0.0001%
14#[near(serializers = [json])]
15#[serde(try_from = "u32")]
16#[derive(
17    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, BorshSerialize, BorshSchema,
18)]
19#[borsh(crate = "::near_sdk::borsh")]
20pub struct Pips(u32);
21
22impl Pips {
23    pub const ZERO: Self = Self(0);
24    pub const ONE_PIP: Self = Self(1);
25    pub const ONE_BIP: Self = Self(Self::ONE_PIP.as_pips() * 100);
26    pub const ONE_PERCENT: Self = Self(Self::ONE_BIP.as_pips() * 100);
27    pub const MAX: Self = Self(Self::ONE_PERCENT.as_pips() * 100);
28
29    #[inline]
30    pub const fn from_pips(pips: u32) -> Option<Self> {
31        if pips > Self::MAX.as_pips() {
32            return None;
33        }
34        Some(Self(pips))
35    }
36
37    #[inline]
38    pub const fn from_bips(bips: u32) -> Option<Self> {
39        Self::ONE_BIP.checked_mul(bips)
40    }
41
42    #[inline]
43    pub const fn from_percent(percent: u32) -> Option<Self> {
44        Self::ONE_PERCENT.checked_mul(percent)
45    }
46
47    #[inline]
48    pub const fn as_pips(self) -> u32 {
49        self.0
50    }
51
52    #[inline]
53    pub const fn as_bips(self) -> u32 {
54        self.as_pips() / Self::ONE_BIP.as_pips()
55    }
56
57    #[inline]
58    pub const fn as_percent(self) -> u32 {
59        self.as_pips() / Self::ONE_PERCENT.as_pips()
60    }
61
62    #[inline]
63    pub const fn is_zero(&self) -> bool {
64        self.0 == 0
65    }
66
67    #[inline]
68    pub fn as_f64(self) -> f64 {
69        f64::from(self.as_pips()) / f64::from(Self::MAX.as_pips())
70    }
71
72    pub const fn checked_add(self, rhs: Self) -> Option<Self> {
73        let Some(pips) = self.as_pips().checked_add(rhs.as_pips()) else {
74            return None;
75        };
76        Self::from_pips(pips)
77    }
78
79    pub const fn checked_sub(self, rhs: Self) -> Option<Self> {
80        let Some(pips) = self.as_pips().checked_sub(rhs.as_pips()) else {
81            return None;
82        };
83        Self::from_pips(pips)
84    }
85
86    #[inline]
87    pub const fn checked_mul(self, rhs: u32) -> Option<Self> {
88        let Some(pips) = self.as_pips().checked_mul(rhs) else {
89            return None;
90        };
91        Self::from_pips(pips)
92    }
93
94    #[inline]
95    pub const fn checked_div(self, rhs: u32) -> Option<Self> {
96        let Some(pips) = self.as_pips().checked_div(rhs) else {
97            return None;
98        };
99        Some(Self(pips))
100    }
101
102    #[must_use]
103    #[inline]
104    pub const fn invert(self) -> Self {
105        Self(Self::MAX.as_pips() - self.as_pips())
106    }
107
108    #[inline]
109    pub fn fee(self, amount: u128) -> u128 {
110        amount
111            .checked_mul_div(self.as_pips().into(), Self::MAX.as_pips().into())
112            .unwrap_or_else(|| unreachable!())
113    }
114
115    #[inline]
116    pub fn fee_ceil(self, amount: u128) -> u128 {
117        amount
118            .checked_mul_div_ceil(self.as_pips().into(), Self::MAX.as_pips().into())
119            .unwrap_or_else(|| unreachable!())
120    }
121}
122
123impl CheckedAdd for Pips {
124    #[inline]
125    fn checked_add(self, rhs: Self) -> Option<Self> {
126        self.checked_add(rhs)
127    }
128}
129
130impl Add for Pips {
131    type Output = Self;
132
133    #[inline]
134    fn add(self, rhs: Self) -> Self::Output {
135        self.checked_add(rhs).unwrap()
136    }
137}
138
139impl CheckedSub for Pips {
140    #[inline]
141    fn checked_sub(self, rhs: Self) -> Option<Self> {
142        self.checked_sub(rhs)
143    }
144}
145
146impl Sub for Pips {
147    type Output = Self;
148
149    #[inline]
150    fn sub(self, rhs: Self) -> Self::Output {
151        self.checked_sub(rhs).unwrap()
152    }
153}
154
155impl Mul<u32> for Pips {
156    type Output = Self;
157
158    #[inline]
159    fn mul(self, rhs: u32) -> Self::Output {
160        self.checked_mul(rhs).unwrap()
161    }
162}
163
164impl Div<u32> for Pips {
165    type Output = Self;
166
167    #[inline]
168    fn div(self, rhs: u32) -> Self::Output {
169        self.checked_div(rhs).unwrap()
170    }
171}
172
173impl Not for Pips {
174    type Output = Self;
175
176    fn not(self) -> Self::Output {
177        self.invert()
178    }
179}
180
181impl Display for Pips {
182    #[inline]
183    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184        write!(f, "{:.4}%", self.as_f64() * 100f64)
185    }
186}
187
188impl TryFrom<u32> for Pips {
189    type Error = PipsOutOfRange;
190
191    #[inline]
192    fn try_from(pips: u32) -> Result<Self, Self::Error> {
193        Self::from_pips(pips).ok_or(PipsOutOfRange)
194    }
195}
196
197#[derive(Debug, ThisError)]
198#[error("out of range: 0..={}", Pips::MAX.as_pips())]
199pub struct PipsOutOfRange;
200
201impl BorshDeserialize for Pips {
202    fn deserialize_reader<R: std::io::Read>(reader: &mut R) -> std::io::Result<Self> {
203        let pips = u32::deserialize_reader(reader)?;
204        Self::from_pips(pips).ok_or_else(|| {
205            std::io::Error::new(
206                std::io::ErrorKind::InvalidData,
207                format!("pips: {pips} is {PipsOutOfRange}"),
208            )
209        })
210    }
211}