perpl-sdk 0.2.4

Rust SDK for the Perpl decentralized perpetuals exchange
Documentation
use alloy::primitives::{I256, U256};
use fastnum::{
    bint,
    decimal::{Context, Decimal, RoundingMode, UnsignedDecimal},
};

/// Scale of on-chain fee rates: the exchange expresses every fee rate in
/// hundred-thousandths of the traded amount (`Per100K`, 1 = 0.1bps), exchange
/// wide and independent of the perpetual contract.
pub const FEE_SCALE: u8 = 5;

/// Converter for fee rates, see [`FEE_SCALE`].
pub fn fee_converter() -> Converter { Converter::new(FEE_SCALE) }

/// Fixed-point to decimal converter.
#[derive(Clone, Copy, Debug, Default)]
pub struct Converter {
    decimals: i32,
}

impl Converter {
    /// Fixed-point converter for `decimals` decimal places. `pub` to match the
    /// other public constructors, so callers can build one directly.
    pub fn new(decimals: u8) -> Self { Self { decimals: decimals as i32 } }

    pub fn decimals(&self) -> u8 { self.decimals as u8 }

    pub fn scale<const N: usize>(&self) -> UnsignedDecimal<N> {
        UnsignedDecimal::<N>::from_parts(
            bint::UInt::ONE,
            self.decimals,
            Context::default().with_rounding_mode(RoundingMode::Floor),
        )
    }

    pub fn from_unsigned<const N: usize>(&self, value: U256) -> UnsignedDecimal<N> {
        let unscaled = bint::UInt::<N>::from_le_slice(value.as_le_slice())
            .expect("Converter: U256 -> UInt::<N>");
        UnsignedDecimal::<N>::from_parts(
            unscaled,
            -self.decimals,
            Context::default().with_rounding_mode(RoundingMode::Floor),
        )
    }

    pub fn from_u64<const N: usize>(&self, value: u64) -> UnsignedDecimal<N> {
        UnsignedDecimal::<N>::from_parts(
            bint::UInt::from_u64(value),
            -self.decimals,
            Context::default().with_rounding_mode(RoundingMode::Floor),
        )
    }

    pub fn from_signed<const N: usize>(&self, value: I256) -> Decimal<N> {
        let unscaled = bint::UInt::<N>::from_le_slice(value.unsigned_abs().as_le_slice())
            .expect("Converter: abs(I256) -> UInt::<N>");
        Decimal::<N>::from_parts(
            unscaled,
            -self.decimals,
            match value.sign() {
                alloy::primitives::Sign::Negative => fastnum::decimal::Sign::Minus,
                alloy::primitives::Sign::Positive => fastnum::decimal::Sign::Plus,
            },
            Context::default().with_rounding_mode(RoundingMode::Floor),
        )
    }

    pub fn from_i64<const N: usize>(&self, value: i64) -> Decimal<N> {
        Decimal::<N>::from_parts(
            bint::UInt::from_u64(value.unsigned_abs()),
            -self.decimals,
            if value < 0 { fastnum::decimal::Sign::Minus } else { fastnum::decimal::Sign::Plus },
            Context::default().with_rounding_mode(RoundingMode::Floor),
        )
    }

    pub fn to_unsigned<const N: usize>(&self, value: UnsignedDecimal<N>) -> U256 {
        let rescaled = value.rescale(self.decimals as i16);
        U256::from_le_slice(rescaled.digits().to_radix_le(256).as_slice())
    }

    pub fn to_signed<const N: usize>(&self, value: Decimal<N>) -> I256 {
        let rescaled = value.rescale(self.decimals as i16);
        let mut res = I256::try_from_le_slice(rescaled.digits().to_radix_le(256).as_slice())
            .unwrap_or_default();
        if value.is_negative() {
            res = res.saturating_neg();
        }
        res
    }
}

#[cfg(test)]
mod tests {
    use fastnum::{dec256, udec256};

    use super::*;

    #[test]
    fn test_numeric_converter_from_unsigned() {
        assert_eq!(Converter::new(0).from_unsigned(U256::from(1234567890)), udec256!(1234567890));
        assert_eq!(Converter::new(6).from_unsigned(U256::from(1234567890)), udec256!(1234.56789));
        assert_eq!(
            Converter::new(12).from_unsigned(U256::from(1234567890)),
            udec256!(0.00123456789)
        );
    }

    #[test]
    fn test_numeric_converter_from_signed() {
        assert_eq!(
            Converter::new(0).from_signed(I256::try_from(1234567890).unwrap()),
            dec256!(1234567890)
        );
        assert_eq!(
            Converter::new(6).from_signed(I256::try_from(1234567890).unwrap()),
            dec256!(1234.56789)
        );
        assert_eq!(
            Converter::new(12).from_signed(I256::try_from(1234567890).unwrap()),
            dec256!(0.00123456789)
        );

        assert_eq!(
            Converter::new(0).from_signed(I256::try_from(-1234567890).unwrap()),
            dec256!(-1234567890)
        );
        assert_eq!(
            Converter::new(6).from_signed(I256::try_from(-1234567890).unwrap()),
            dec256!(-1234.56789)
        );
        assert_eq!(
            Converter::new(12).from_signed(I256::try_from(-1234567890).unwrap()),
            dec256!(-0.00123456789)
        );
    }

    #[test]
    fn test_numeric_converter_to_unsigned() {
        assert_eq!(Converter::new(0).to_unsigned(udec256!(1234567890)), U256::from(1234567890));
        assert_eq!(Converter::new(6).to_unsigned(udec256!(1234.56789)), U256::from(1234567890));
        assert_eq!(Converter::new(12).to_unsigned(udec256!(0.00123456789)), U256::from(1234567890));
    }

    #[test]
    fn test_numeric_converter_to_signed() {
        assert_eq!(
            Converter::new(0).to_signed(dec256!(1234567890)),
            I256::try_from(1234567890).unwrap(),
        );
        assert_eq!(
            Converter::new(6).to_signed(dec256!(1234.56789)),
            I256::try_from(1234567890).unwrap(),
        );
        assert_eq!(
            Converter::new(12).to_signed(dec256!(0.00123456789)),
            I256::try_from(1234567890).unwrap(),
        );

        assert_eq!(
            Converter::new(0).to_signed(dec256!(-1234567890)),
            I256::try_from(-1234567890).unwrap(),
        );
        assert_eq!(
            Converter::new(6).to_signed(dec256!(-1234.56789)),
            I256::try_from(-1234567890).unwrap(),
        );
        assert_eq!(
            Converter::new(12).to_signed(dec256!(-0.00123456789)),
            I256::try_from(-1234567890).unwrap(),
        );
    }
}