123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222 |
- 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<Amount> for AnyAmount {
- type Error = Error;
- fn try_into(self) -> Result<Amount, Self::Error> {
- 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
- 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<AmountCents, D::Error>
- where
- D: de::Deserializer<'de>,
- {
- let s = String::deserialize(deserializer)?;
- s.parse::<AmountCents>().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<Self> {
- 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<String> for &Amount {
- type Error = Error;
- fn try_into(self) -> Result<String, Error> {
- 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");
- }
- }
|