// file: crates/ksp-offchain-transport-lib/src/market_price_decimal.rs // version: 4 /// Maximum accepted UTF-8 byte length for one textual decimal input. pub const MARKET_PRICE_DECIMAL_MAX_INPUT_BYTES: usize = 96; /// Maximum scale retained by the canonical exact decimal representation. pub const MARKET_PRICE_DECIMAL_MAX_SCALE: u8 = 18; /// Exact positive decimal value used for one successful V1 SOL/USD observation. /// /// The value is represented as a positive `u128` coefficient plus a bounded decimal scale. Trailing fractional zeroes are removed during construction, and /// Serde serialization always emits the canonical decimal string instead of an IEEE-754 number. #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub struct MarketPriceDecimal { coefficient: u128, scale: u8, } impl MarketPriceDecimal { /// Parses a positive decimal or bounded scientific-notation value without converting through `f64`. pub fn parse(source: &str) -> ksp_core_lib::Result { if source.is_empty() || source.len() > crate::MARKET_PRICE_DECIMAL_MAX_INPUT_BYTES || source.trim() != source { return std::result::Result::Err(invalid_decimal_error()); } let (significand, exponent) = match split_exponent(source) { std::result::Result::Ok(value) => value, std::result::Result::Err(()) => return std::result::Result::Err(invalid_decimal_error()), }; let (digits, fractional_digits) = match significand_digits(significand) { std::result::Result::Ok(value) => value, std::result::Result::Err(()) => return std::result::Result::Err(invalid_decimal_error()), }; let mut coefficient = match digits.parse::() { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return std::result::Result::Err(invalid_decimal_error()), }; if coefficient == 0 { return std::result::Result::Err(invalid_decimal_error()); } let mut effective_scale = i32::from(fractional_digits) - exponent; if effective_scale < 0 { let multiplication_power = match u32::try_from(-effective_scale) { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return std::result::Result::Err(invalid_decimal_error()), }; coefficient = match checked_multiply_power_of_ten(coefficient, multiplication_power) { std::option::Option::Some(value) => value, std::option::Option::None => return std::result::Result::Err(invalid_decimal_error()), }; effective_scale = 0; } if effective_scale > i32::from(crate::MARKET_PRICE_DECIMAL_MAX_SCALE) { return std::result::Result::Err(invalid_decimal_error()); } let mut scale = match u8::try_from(effective_scale) { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return std::result::Result::Err(invalid_decimal_error()), }; while scale > 0 && coefficient % 10 == 0 { coefficient /= 10; scale -= 1; } return std::result::Result::Ok(Self { coefficient, scale }); } /// Parses one provider JSON number or string while preserving the original numeric lexeme. pub(crate) fn parse_json_raw(raw: &serde_json::value::RawValue) -> ksp_core_lib::Result { let source = raw.get(); if source.starts_with('"') { let decoded = match serde_json::from_str::(source) { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return std::result::Result::Err(invalid_decimal_error()), }; return crate::MarketPriceDecimal::parse(decoded.as_str()); } return crate::MarketPriceDecimal::parse(source); } /// Returns the normalized integer coefficient. #[must_use] pub const fn coefficient(&self) -> u128 { return self.coefficient; } /// Returns the normalized decimal scale. #[must_use] pub const fn scale(&self) -> u8 { return self.scale; } /// Returns the canonical non-scientific decimal representation. #[must_use] pub fn to_canonical_string(&self) -> std::string::String { let digits = self.coefficient.to_string(); if self.scale == 0 { return digits; } let scale = usize::from(self.scale); if digits.len() > scale { let split = digits.len() - scale; return std::format!("{}.{}", &digits[..split], &digits[split..]); } let zero_count = scale - digits.len(); return std::format!("0.{}{}", "0".repeat(zero_count), digits); } } impl std::fmt::Display for MarketPriceDecimal { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { return formatter.write_str(self.to_canonical_string().as_str()); } } impl std::str::FromStr for MarketPriceDecimal { type Err = ksp_core_lib::Error; fn from_str(source: &str) -> std::result::Result { return crate::MarketPriceDecimal::parse(source); } } impl serde::Serialize for MarketPriceDecimal { fn serialize(&self, serializer: S) -> std::result::Result where S: serde::Serializer, { return serializer.serialize_str(self.to_canonical_string().as_str()); } } impl<'de> serde::Deserialize<'de> for MarketPriceDecimal { fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { return deserializer.deserialize_str(MarketPriceDecimalVisitor); } } struct MarketPriceDecimalVisitor; impl<'de> serde::de::Visitor<'de> for MarketPriceDecimalVisitor { type Value = crate::MarketPriceDecimal; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { return formatter.write_str("a canonicalizable positive decimal string"); } fn visit_str(self, value: &str) -> std::result::Result where E: serde::de::Error, { return match crate::MarketPriceDecimal::parse(value) { std::result::Result::Ok(decimal) => std::result::Result::Ok(decimal), std::result::Result::Err(_) => std::result::Result::Err(E::custom("invalid KSP market-price decimal")), }; } } fn checked_multiply_power_of_ten(mut value: u128, exponent: u32) -> std::option::Option { let mut remaining = exponent; while remaining > 0 { value = match value.checked_mul(10) { std::option::Option::Some(next) => next, std::option::Option::None => return std::option::Option::None, }; remaining -= 1; } return std::option::Option::Some(value); } fn invalid_decimal_error() -> ksp_core_lib::Error { return ksp_core_lib::Error::new(crate::ERROR_CODE_MARKET_PRICE_DECIMAL_INVALID, "invalid exact market-price decimal"); } fn significand_digits(significand: &str) -> std::result::Result<(std::string::String, u8), ()> { if significand.is_empty() || significand.starts_with('-') || significand.starts_with('+') { return std::result::Result::Err(()); } let mut digits = std::string::String::with_capacity(significand.len()); let mut fractional_digits: usize = 0; let mut decimal_seen = false; let mut digit_seen = false; for character in significand.chars() { if character.is_ascii_digit() { digits.push(character); digit_seen = true; if decimal_seen { fractional_digits += 1; } } else if character == '.' && !decimal_seen { decimal_seen = true; } else { return std::result::Result::Err(()); } } if !digit_seen || significand.ends_with('.') || significand.starts_with('.') { return std::result::Result::Err(()); } let fractional_digits = match u8::try_from(fractional_digits) { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return std::result::Result::Err(()), }; return std::result::Result::Ok((digits, fractional_digits)); } fn split_exponent(source: &str) -> std::result::Result<(&str, i32), ()> { let mut separator_index = std::option::Option::None; for (index, character) in source.char_indices() { if character == 'e' || character == 'E' { if separator_index.is_some() { return std::result::Result::Err(()); } separator_index = std::option::Option::Some(index); } } let index = match separator_index { std::option::Option::Some(value) => value, std::option::Option::None => return std::result::Result::Ok((source, 0)), }; let significand = &source[..index]; let exponent_source = &source[index + 1..]; if exponent_source.is_empty() || exponent_source.len() > 4 { return std::result::Result::Err(()); } let exponent = match exponent_source.parse::() { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return std::result::Result::Err(()), }; if !(-128..=128).contains(&exponent) { return std::result::Result::Err(()); } return std::result::Result::Ok((significand, exponent)); } #[cfg(test)] #[path = "../unit_tests/market_price_decimal.rs"] mod tests;