diff --git a/Cargo.lock b/Cargo.lock index 05ad1a27c..2cf607998 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3334,6 +3334,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint" +version = "0.3.1" +source = "registry+/~https://github.com/rust-lang/crates.io-index" +checksum = "5e9a41747ae4633fce5adffb4d2e81ffc5e89593cb19917f8fb2cc5ff76507bf" +dependencies = [ + "autocfg 1.0.0", + "num-integer", + "num-traits", +] + [[package]] name = "num-complex" version = "0.2.4" @@ -3361,7 +3372,7 @@ source = "registry+/~https://github.com/rust-lang/crates.io-index" checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" dependencies = [ "autocfg 1.0.0", - "num-bigint", + "num-bigint 0.2.6", "num-integer", "num-traits", ] @@ -3505,6 +3516,8 @@ version = "1.0.0" dependencies = [ "frame-support", "frame-system", + "num-bigint 0.3.1", + "num-traits", "parity-scale-codec", "runtime-interfaces", "sp-core", diff --git a/pallets/cash/Cargo.toml b/pallets/cash/Cargo.toml index eab280a3e..96c61be31 100644 --- a/pallets/cash/Cargo.toml +++ b/pallets/cash/Cargo.toml @@ -21,6 +21,8 @@ version = '1.3.4' frame-support = { default-features = false, version = '2.0.0' } frame-system = { default-features = false, version = '2.0.0' } runtime-interfaces = { default-features = false, version = '1.0.0', path="../runtime-interfaces"} +num-bigint = { default-features = false, version = "0.3" } +num-traits = { default-features = false, version = "0.2" } [dev-dependencies] sp-core = { default-features = false, version = '2.0.0' } diff --git a/pallets/cash/src/fixed_precision.rs b/pallets/cash/src/fixed_precision.rs new file mode 100644 index 000000000..166b353b3 --- /dev/null +++ b/pallets/cash/src/fixed_precision.rs @@ -0,0 +1,104 @@ +use num_bigint::BigUint; + +/// The type of the decimal field. +pub type DecimalType = u8; + +/// The type of the mantissa field +pub type MantissaType = BigUint; + +/// Represents a decimal number in base 10 with fixed precision. The number can be as precise as +/// the maximum value of the DecimalType and there is no upper bound on the numbers size. +/// +/// For example, if the mantissa is 123456789 and decimals is 4 the number that is represented is +/// 12345.6789. +#[derive(Clone, PartialEq, Debug)] +pub struct FixedPrecision { + mantissa: MantissaType, + decimals: DecimalType, +} + +/// Error type for fixed precision math. +#[derive(Copy, Clone, PartialEq, Debug)] +pub enum MathError { + PrecisionMismatch, +} + +impl FixedPrecision { + /// Create a new FixedPrecision number from parts. The mantissa is used "raw" and not scaled + /// in any way + pub fn new>(mantissa: T, decimals: DecimalType) -> Self { + let mantissa = mantissa.into(); + FixedPrecision { mantissa, decimals } + } + + /// A helper function for downstream math functions, returns an error if and only if the + /// number of decimals do not match + fn check_decimals(self: &Self, other: &Self) -> Result<(), MathError> { + if self.decimals == other.decimals { + Ok(()) + } else { + Err(MathError::PrecisionMismatch) + } + } + + /// Add two FixedPrecision numbers together. Note the signature uses borrowed values this is + /// because the underlying storage is arbitrarily large and we do not want to copy the values. + pub fn add(self: &Self, rhs: &Self) -> Result { + self.check_decimals(rhs)?; + // note - this cannot fail with BigUint but that will change based on underlying storage + let new_mantissa = &self.mantissa + &rhs.mantissa; + + Ok(Self::new(new_mantissa, self.decimals)) + } + + /// Create the representation of 1 in the number of decimals requested. For example one(3) + /// will return a fixed precision number with 1000 as the mantissa and 3 as the number of decimals + pub fn one + Copy>(decimals: T) -> Self { + let ten: MantissaType = 10u8.into(); + let decimals: DecimalType = decimals.into(); + let new_mantissa = ten.pow(decimals as u32); + Self::new(new_mantissa, decimals) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_one() { + let expected = FixedPrecision::new(1000u32, 3); + let actual = FixedPrecision::one(3); + assert_eq!(expected, actual); + } + + #[test] + fn test_add_happy_path() -> Result<(), MathError> { + let a = FixedPrecision::one(2); + let b = FixedPrecision::one(2); + // note - automatic borrow of `a` here (rust elides the (&a).add for you + let actual = a.add(&b)?; + + // make sure nothing has changed + assert_eq!(a, b); + assert_eq!(a, FixedPrecision::one(2)); + assert_eq!(b, FixedPrecision::one(2)); + + let expected = FixedPrecision::new(200u8, 2); + assert_eq!(actual, expected); + + Ok(()) + } + + #[test] + fn test_add_decimal_mismatch() { + let a = FixedPrecision::one(2); + let b = FixedPrecision::one(3); + // note - automatic borrow of `a` here (rust elides the (&a).add for you + let actual = a.add(&b); + assert!(actual.is_err()); + let actual = actual.err().unwrap(); + let expected = MathError::PrecisionMismatch; + assert_eq!(actual, expected); + } +} diff --git a/pallets/cash/src/lib.rs b/pallets/cash/src/lib.rs index 4196c64bc..1a0897ed7 100644 --- a/pallets/cash/src/lib.rs +++ b/pallets/cash/src/lib.rs @@ -9,6 +9,7 @@ use frame_system::ensure_signed; #[cfg(test)] mod mock; +mod fixed_precision; #[cfg(test)] mod tests;