etopay_sdk/types/
currencies.rs

1use super::error::{Result, TypeError};
2use api_types::api::{generic::ApiCryptoCurrency, viviswap::detail::SwapPaymentDetailKey};
3use serde::Serialize;
4
5/// Supported currencies (mirrors `api_types` but needed so we can implement the additional
6/// `coin_type` and `to_vivi_payment_method_key` function)
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
8pub enum Currency {
9    /// Iota token
10    Iota,
11    /// Ethereum token
12    Eth,
13}
14
15impl TryFrom<String> for Currency {
16    type Error = TypeError;
17    /// Convert from String to Currency, used at the API boundary to interface with the bindings.
18    fn try_from(currency: String) -> Result<Self> {
19        match currency.to_lowercase().as_str() {
20            "iota" => Ok(Self::Iota),
21            "eth" => Ok(Self::Eth),
22            _ => Err(TypeError::InvalidCurrency(currency)),
23        }
24    }
25}
26
27impl Currency {
28    /// Convert this [`Currency`] into a [`SwapPaymentDetailKey`]
29    pub fn to_vivi_payment_method_key(self) -> SwapPaymentDetailKey {
30        match self {
31            Self::Iota => SwapPaymentDetailKey::Iota,
32            Self::Eth => SwapPaymentDetailKey::Eth,
33        }
34    }
35}
36
37/// We want to convert from our internal Currency enum into the one used in the API.
38impl From<Currency> for ApiCryptoCurrency {
39    fn from(value: Currency) -> Self {
40        match value {
41            Currency::Iota => ApiCryptoCurrency::Iota,
42            Currency::Eth => ApiCryptoCurrency::Eth,
43        }
44    }
45}
46impl From<ApiCryptoCurrency> for Currency {
47    fn from(value: ApiCryptoCurrency) -> Self {
48        match value {
49            ApiCryptoCurrency::Iota => Currency::Iota,
50            ApiCryptoCurrency::Eth => Currency::Eth,
51        }
52    }
53}
54
55// the display implementation must be compatible with TryFrom<String> since it is part of the
56// public binding interface.
57impl std::fmt::Display for Currency {
58    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        match self {
60            Currency::Iota => write!(f, "Iota"),
61            Currency::Eth => write!(f, "Eth"),
62        }
63    }
64}
65
66/// A non-negative decimal value. Used as inputs to create purchases or sending a transaction.
67#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
68pub struct CryptoAmount(rust_decimal::Decimal);
69
70impl CryptoAmount {
71    /// The value of ZERO
72    pub const ZERO: Self = Self(rust_decimal::Decimal::ZERO);
73
74    /// Get the inner value of the amount
75    pub fn inner(&self) -> rust_decimal::Decimal {
76        self.0
77    }
78
79    /// Internal helper function to create values in consts during tests.
80    /// This is unsafe since it does not perform any non-negativity checks.
81    pub(crate) const unsafe fn new_unchecked(value: rust_decimal::Decimal) -> Self {
82        Self(value)
83    }
84}
85
86// From u64 is always possible and will yield a Non-negative value
87impl From<u64> for CryptoAmount {
88    fn from(value: u64) -> Self {
89        Self(rust_decimal::Decimal::from(value))
90    }
91}
92
93impl TryFrom<f64> for CryptoAmount {
94    type Error = crate::Error;
95
96    fn try_from(value: f64) -> std::result::Result<Self, Self::Error> {
97        Self::try_from(rust_decimal::Decimal::try_from(value)?)
98    }
99}
100
101impl TryFrom<rust_decimal::Decimal> for CryptoAmount {
102    type Error = crate::Error;
103
104    fn try_from(value: rust_decimal::Decimal) -> std::result::Result<Self, Self::Error> {
105        if value < rust_decimal::Decimal::ZERO {
106            return Err(crate::Error::NegativeAmount);
107        }
108        Ok(Self(value))
109    }
110}
111impl TryFrom<api_types::api::decimal::Decimal> for CryptoAmount {
112    type Error = crate::Error;
113
114    fn try_from(value: api_types::api::decimal::Decimal) -> std::result::Result<Self, Self::Error> {
115        Self::try_from(value.0)
116    }
117}
118
119impl From<CryptoAmount> for api_types::api::decimal::Decimal {
120    fn from(val: CryptoAmount) -> Self {
121        Self(val.0)
122    }
123}
124
125impl TryFrom<CryptoAmount> for f64 {
126    type Error = crate::Error;
127
128    fn try_from(value: CryptoAmount) -> std::result::Result<Self, Self::Error> {
129        Ok(value.0.try_into()?)
130    }
131}
132
133// Adding two NonNegativeAmounts will always result in a positive value so this is safe
134impl std::ops::Add for CryptoAmount {
135    type Output = Self;
136
137    fn add(self, rhs: Self) -> Self::Output {
138        Self(self.0 + rhs.0)
139    }
140}
141
142// Dividing two NonNegativeAmounts will always result in a positive value so this is safe
143impl std::ops::Div for CryptoAmount {
144    type Output = Self;
145
146    fn div(self, rhs: Self) -> Self::Output {
147        Self(self.0 / rhs.0)
148    }
149}
150
151// Multiplying two NonNegativeAmounts will always result in a positive value so this is safe
152impl std::ops::Mul for CryptoAmount {
153    type Output = Self;
154
155    fn mul(self, rhs: Self) -> Self::Output {
156        Self(self.0 * rhs.0)
157    }
158}
159
160#[cfg(test)]
161mod test {
162    use crate::types::currencies::Currency;
163
164    use rust_decimal_macros::dec;
165
166    use super::CryptoAmount;
167
168    #[rstest::rstest]
169    fn test_display_roundtrip(#[values(Currency::Iota, Currency::Eth)] c: Currency) {
170        assert_eq!(c, Currency::try_from(c.to_string()).unwrap());
171    }
172
173    #[rstest::rstest]
174    #[case(dec!(0.0), Some(CryptoAmount(dec!(0.0))))]
175    #[case(dec!(-0.0), Some(CryptoAmount(dec!(-0.0))))]
176    #[case(dec!(-1.0), None)]
177    #[case(dec!(-10.5), None)]
178    #[case(dec!(1.0), Some(CryptoAmount(dec!(1.0))))]
179    #[case(dec!(10.0), Some(CryptoAmount(dec!(10.0))))]
180    fn test_try_from_non_zero_dec(#[case] value: rust_decimal::Decimal, #[case] expected_value: Option<CryptoAmount>) {
181        let amount = CryptoAmount::try_from(value);
182
183        match (amount, expected_value) {
184            (Ok(amount), Some(expected)) => assert_eq!(amount, expected),
185            (Err(error), None) => assert!(matches!(error, crate::Error::NegativeAmount)),
186            (amount, expected) => panic!("expected {expected:?} but got {amount:?} for {value}"),
187        }
188    }
189
190    #[rstest::rstest]
191    #[case(0.0, Some(CryptoAmount(dec!(0.0))))]
192    #[case(-0.0, Some(CryptoAmount(dec!(0.0))))] // this is apparently also "negative"
193    #[case(-1.0, None)]
194    #[case(-10.5, None)]
195    #[case(1.0, Some(CryptoAmount(dec!(1.0))))]
196    #[case(10.0, Some(CryptoAmount(dec!(10.0))))]
197    fn test_try_from_non_zero_f64(#[case] value: f64, #[case] expected_value: Option<CryptoAmount>) {
198        let amount = CryptoAmount::try_from(value);
199
200        match (amount, expected_value) {
201            (Ok(amount), Some(expected)) => assert_eq!(amount, expected),
202            (Err(error), None) => assert!(matches!(error, crate::Error::NegativeAmount)),
203            (amount, expected) => panic!("expected {expected:?} but got {amount:?} for {value}"),
204        }
205    }
206}