use crate::Asset; use serde::{de, ser::SerializeStruct, Deserialize, Serialize, Serializer}; /// The raw storage for cents, the more the better pub type AmountCents = i128; #[derive(Clone, Debug, Eq, PartialEq, thiserror::Error, Serialize)] pub enum Error { #[error("{0} is not a valid number")] NoANumber(String), #[error("Overflow")] Overflow, #[error("Invalid asset name: {0}")] InvalidAssetName(String), } #[derive(Debug, Serialize, Deserialize)] /// Human amount /// /// This amount is used to represent the amount in a human readable way. It is not being used /// internally but it is defined to be used by any external interface. pub struct HumanAmount { asset: Asset, amount: String, } #[derive(Debug, Serialize, Deserialize)] #[serde(untagged)] /// Any amount /// /// This amount will parse/encode any amount, either in cents or in human readable format. pub enum AnyAmount { /// Amount in cents Cent(Amount), /// Amount in human readable format Human(HumanAmount), } impl TryInto for AnyAmount { type Error = Error; fn try_into(self) -> Result { match self { Self::Cent(a) => Ok(a), Self::Human(h) => h.asset.from_human(&h.amount), } } } /// Amount /// /// The cents are stored in their lowest denomination, or their "cents". For /// instance a Bitcoin would be represented with a `precision` of 8, or dollars /// in a 2 or 3. /// /// For instance, if the dollar Asset has a `precision` of `2`, an `cents` of /// 1255 will represent `12.55 USD`. /// /// Amount will abstract all the math operations that can be performed with the /// amounts, guarding that the `Asset` are the same before performing any /// operation. /// /// /// The `cents` and `Asset` must be used to store amounts in the storage /// layer. Float or string representations should be used to display #[derive( Clone, Debug, Eq, PartialEq, Deserialize, borsh::BorshSerialize, borsh::BorshDeserialize, )] pub struct Amount { asset: Asset, #[serde(deserialize_with = "deserialize_string_to_amount")] cents: AmountCents, } impl Serialize for Amount { fn serialize(&self, serializer: S) -> Result where S: Serializer, { let mut state = serializer.serialize_struct("amount", 3)?; state.serialize_field("asset", &self.asset)?; state.serialize_field("cents", &self.cents.to_string())?; state.serialize_field("human", &self.to_string())?; state.end() } } fn deserialize_string_to_amount<'de, D>(deserializer: D) -> Result where D: de::Deserializer<'de>, { let s = String::deserialize(deserializer)?; s.parse::().map_err(serde::de::Error::custom) } impl Amount { /// Creates a new amount from an asset and cents pub(crate) fn new(asset: Asset, cents: AmountCents) -> Self { Self { asset, cents } } #[inline] /// Returns the asset for this amount pub fn asset(&self) -> &Asset { &self.asset } #[inline] /// Return the cents pub fn cents(&self) -> AmountCents { self.cents } /// Attempts to do a checked addition of two amounts /// This will fails if assets are not the same or it overflows or underflows pub fn checked_add(&self, other: &Self) -> Option { if self.asset != other.asset { return None; } self.cents.checked_add(other.cents).map(|cents| Self { asset: self.asset.clone(), cents, }) } } impl ToString for Amount { fn to_string(&self) -> String { self.try_into().unwrap() } } impl TryInto for &Amount { type Error = Error; fn try_into(self) -> Result { let str = self.cents.abs().to_string(); let precision = self.asset.precision; let len = u8::try_from(str.len()).map_err(|_| Error::Overflow)?; let (str, len) = if len < precision.checked_add(1).ok_or(Error::Overflow)? { ( format!( "{}{}", "0".repeat( precision .checked_sub(len) .and_then(|x| x.checked_add(1)) .ok_or(Error::Overflow)? .into() ), str ), precision.checked_add(1).ok_or(Error::Overflow)?, ) } else { (str, len) }; let (left, right) = str.split_at(len.checked_sub(precision).ok_or(Error::Overflow)?.into()); Ok(format!( "{}{}.{}", if self.cents.is_negative() { "-" } else { "" }, left, right ) .trim_end_matches('0') .trim_end_matches('.') .to_owned()) } } #[cfg(test)] mod test { use super::*; #[test] fn dollar() { let usd: Asset = "USD/4".parse().expect("asset"); let amount = usd.new_amount(1022100); assert_eq!(amount.to_string(), "102.21"); } #[test] fn bitcoin() { let btc: Asset = "BTC/8".parse().expect("asset"); assert_eq!(btc.new_amount(1022100).to_string(), "0.010221"); assert_eq!(btc.new_amount(10).to_string(), "0.0000001"); assert_eq!(btc.new_amount(10000000).to_string(), "0.1"); assert_eq!(btc.new_amount(100000000).to_string(), "1"); assert_eq!( btc.new_amount(100000000) .checked_add(&btc.new_amount(100000000)) .unwrap() .to_string(), "2", ); assert_eq!(btc.new_amount(1000000000).to_string(), "10"); } #[test] fn from_human() { let btc: Asset = "BTC/8".parse().expect("asset"); let parsed_amount = btc.from_human("0.1").expect("valid amount"); assert_eq!(parsed_amount.to_string(), "0.1"); let parsed_amount = btc.from_human("-0.1").expect("valid amount"); assert_eq!(parsed_amount.to_string(), "-0.1"); let parsed_amount = btc.from_human("-0.000001").expect("valid amount"); assert_eq!(parsed_amount.to_string(), "-0.000001"); let parsed_amount = btc.from_human("-0.000000001").expect("valid amount"); assert_eq!(parsed_amount.to_string(), "0"); let parsed_amount = btc.from_human("0.000001").expect("valid amount"); assert_eq!(parsed_amount.to_string(), "0.000001"); let parsed_amount = btc.from_human("-0.000000100001").expect("valid amount"); assert_eq!(parsed_amount.to_string(), "-0.0000001"); let parsed_amount = btc.from_human("100000").expect("valid amount"); assert_eq!(parsed_amount.to_string(), "100000"); } }