Skip to main content

perpl_sdk/
num.rs

1use alloy::primitives::{I256, U256};
2use fastnum::{
3    bint,
4    decimal::{Context, Decimal, RoundingMode, UnsignedDecimal},
5};
6
7/// Scale of on-chain fee rates before v1.1.7.5: hundred-thousandths of the
8/// traded amount (`Per100K`, 1 = 0.1 bps), exchange wide and independent of the
9/// perpetual contract.
10///
11/// Still the unit of the per-order BUILDER fee at any contract version: the
12/// builder-code wire format is deliberately unchanged by the redenomination -
13/// the envelope, order storage, the views and every event carry `Per100K`, and
14/// the contract scales it internally at the point the fee is computed.
15pub const FEE_SCALE: u8 = 5;
16
17/// Scale of on-chain fee-SCHEDULE rates from v1.1.7.5: millionths of the traded
18/// amount (ppm, 1 = 0.01 bps).
19///
20/// Which of the two applies to a given deployment is
21/// [`crate::state::ContractFeatures::fee_rate_converter`], not a compile-time
22/// choice - the SDK indexes contracts on both sides of the upgrade.
23pub const FEE_SCALE_PPM: u8 = 6;
24
25/// Converter for `Per100K` rates, see [`FEE_SCALE`].
26pub fn fee_converter() -> Converter { Converter::new(FEE_SCALE) }
27
28/// Converter for ppm rates, see [`FEE_SCALE_PPM`].
29pub fn ppm_fee_converter() -> Converter { Converter::new(FEE_SCALE_PPM) }
30
31/// Fixed-point to decimal converter.
32#[derive(Clone, Copy, Debug, Default)]
33pub struct Converter {
34    decimals: i32,
35}
36
37impl Converter {
38    /// Fixed-point converter for `decimals` decimal places. `pub` to match the
39    /// other public constructors, so callers can build one directly.
40    pub fn new(decimals: u8) -> Self { Self { decimals: decimals as i32 } }
41
42    pub fn decimals(&self) -> u8 { self.decimals as u8 }
43
44    pub fn scale<const N: usize>(&self) -> UnsignedDecimal<N> {
45        UnsignedDecimal::<N>::from_parts(
46            bint::UInt::ONE,
47            self.decimals,
48            Context::default().with_rounding_mode(RoundingMode::Floor),
49        )
50    }
51
52    pub fn from_unsigned<const N: usize>(&self, value: U256) -> UnsignedDecimal<N> {
53        let unscaled = bint::UInt::<N>::from_le_slice(value.as_le_slice())
54            .expect("Converter: U256 -> UInt::<N>");
55        UnsignedDecimal::<N>::from_parts(
56            unscaled,
57            -self.decimals,
58            Context::default().with_rounding_mode(RoundingMode::Floor),
59        )
60    }
61
62    pub fn from_u64<const N: usize>(&self, value: u64) -> UnsignedDecimal<N> {
63        UnsignedDecimal::<N>::from_parts(
64            bint::UInt::from_u64(value),
65            -self.decimals,
66            Context::default().with_rounding_mode(RoundingMode::Floor),
67        )
68    }
69
70    pub fn from_signed<const N: usize>(&self, value: I256) -> Decimal<N> {
71        let unscaled = bint::UInt::<N>::from_le_slice(value.unsigned_abs().as_le_slice())
72            .expect("Converter: abs(I256) -> UInt::<N>");
73        Decimal::<N>::from_parts(
74            unscaled,
75            -self.decimals,
76            match value.sign() {
77                alloy::primitives::Sign::Negative => fastnum::decimal::Sign::Minus,
78                alloy::primitives::Sign::Positive => fastnum::decimal::Sign::Plus,
79            },
80            Context::default().with_rounding_mode(RoundingMode::Floor),
81        )
82    }
83
84    pub fn from_i64<const N: usize>(&self, value: i64) -> Decimal<N> {
85        Decimal::<N>::from_parts(
86            bint::UInt::from_u64(value.unsigned_abs()),
87            -self.decimals,
88            if value < 0 { fastnum::decimal::Sign::Minus } else { fastnum::decimal::Sign::Plus },
89            Context::default().with_rounding_mode(RoundingMode::Floor),
90        )
91    }
92
93    pub fn to_unsigned<const N: usize>(&self, value: UnsignedDecimal<N>) -> U256 {
94        let rescaled = value.rescale(self.decimals as i16);
95        U256::from_le_slice(rescaled.digits().to_radix_le(256).as_slice())
96    }
97
98    pub fn to_signed<const N: usize>(&self, value: Decimal<N>) -> I256 {
99        let rescaled = value.rescale(self.decimals as i16);
100        let mut res = I256::try_from_le_slice(rescaled.digits().to_radix_le(256).as_slice())
101            .unwrap_or_default();
102        if value.is_negative() {
103            res = res.saturating_neg();
104        }
105        res
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use fastnum::{dec256, udec256};
112
113    use super::*;
114
115    #[test]
116    fn test_numeric_converter_from_unsigned() {
117        assert_eq!(Converter::new(0).from_unsigned(U256::from(1234567890)), udec256!(1234567890));
118        assert_eq!(Converter::new(6).from_unsigned(U256::from(1234567890)), udec256!(1234.56789));
119        assert_eq!(
120            Converter::new(12).from_unsigned(U256::from(1234567890)),
121            udec256!(0.00123456789)
122        );
123    }
124
125    #[test]
126    fn test_numeric_converter_from_signed() {
127        assert_eq!(
128            Converter::new(0).from_signed(I256::try_from(1234567890).unwrap()),
129            dec256!(1234567890)
130        );
131        assert_eq!(
132            Converter::new(6).from_signed(I256::try_from(1234567890).unwrap()),
133            dec256!(1234.56789)
134        );
135        assert_eq!(
136            Converter::new(12).from_signed(I256::try_from(1234567890).unwrap()),
137            dec256!(0.00123456789)
138        );
139
140        assert_eq!(
141            Converter::new(0).from_signed(I256::try_from(-1234567890).unwrap()),
142            dec256!(-1234567890)
143        );
144        assert_eq!(
145            Converter::new(6).from_signed(I256::try_from(-1234567890).unwrap()),
146            dec256!(-1234.56789)
147        );
148        assert_eq!(
149            Converter::new(12).from_signed(I256::try_from(-1234567890).unwrap()),
150            dec256!(-0.00123456789)
151        );
152    }
153
154    #[test]
155    fn test_numeric_converter_to_unsigned() {
156        assert_eq!(Converter::new(0).to_unsigned(udec256!(1234567890)), U256::from(1234567890));
157        assert_eq!(Converter::new(6).to_unsigned(udec256!(1234.56789)), U256::from(1234567890));
158        assert_eq!(Converter::new(12).to_unsigned(udec256!(0.00123456789)), U256::from(1234567890));
159    }
160
161    #[test]
162    fn test_numeric_converter_to_signed() {
163        assert_eq!(
164            Converter::new(0).to_signed(dec256!(1234567890)),
165            I256::try_from(1234567890).unwrap(),
166        );
167        assert_eq!(
168            Converter::new(6).to_signed(dec256!(1234.56789)),
169            I256::try_from(1234567890).unwrap(),
170        );
171        assert_eq!(
172            Converter::new(12).to_signed(dec256!(0.00123456789)),
173            I256::try_from(1234567890).unwrap(),
174        );
175
176        assert_eq!(
177            Converter::new(0).to_signed(dec256!(-1234567890)),
178            I256::try_from(-1234567890).unwrap(),
179        );
180        assert_eq!(
181            Converter::new(6).to_signed(dec256!(-1234.56789)),
182            I256::try_from(-1234567890).unwrap(),
183        );
184        assert_eq!(
185            Converter::new(12).to_signed(dec256!(-0.00123456789)),
186            I256::try_from(-1234567890).unwrap(),
187        );
188    }
189}