hotshot_query_service/explorer/
currency.rs

1// Copyright (c) 2022 Espresso Systems (espressosys.com)
2// This file is part of the HotShot Query Service library.
3//
4// This program is free software: you can redistribute it and/or modify it under the terms of the GNU
5// General Public License as published by the Free Software Foundation, either version 3 of the
6// License, or (at your option) any later version.
7// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
8// even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
9// General Public License for more details.
10// You should have received a copy of the GNU General Public License along with this program. If not,
11// see <https://www.gnu.org/licenses/>.
12
13use std::fmt::Display;
14
15use serde::{ser::SerializeStruct, Deserialize, Serialize, Serializer};
16
17use super::errors::ExplorerAPIError;
18
19/// CurrencyMismatchError is an error that occurs when two different currencies
20/// are attempted to be combined in any way that would result in an invalid
21/// state.
22///
23/// For example, attempting to add two different currencies together:
24/// USD 1.00 + EUR 1.00
25#[derive(Debug, Clone, Deserialize)]
26#[serde(tag = "code", rename = "CURRENCY_MISMATCH")]
27pub struct CurrencyMismatchError {
28    pub currency1: CurrencyCode,
29    pub currency2: CurrencyCode,
30}
31
32impl ExplorerAPIError for CurrencyMismatchError {
33    /// code returns a string identifier to uniquely identify the error
34    fn code(&self) -> &str {
35        "CURRENCY_MISMATCH"
36    }
37}
38
39impl Display for CurrencyMismatchError {
40    /// fmt formats the error into a human readable string
41    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42        write!(
43            f,
44            "attempt to add two different currencies: {:?} and {:?}",
45            self.currency1, self.currency2
46        )
47    }
48}
49
50impl Serialize for CurrencyMismatchError {
51    /// serialize converts the error into a struct representation
52    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
53    where
54        S: Serializer,
55    {
56        let mut st = serializer.serialize_struct("CurrencyMismatchError", 4)?;
57        st.serialize_field("code", &self.code())?;
58        st.serialize_field("currency1", &self.currency1)?;
59        st.serialize_field("currency2", &self.currency2)?;
60        st.serialize_field("message", &format!("{self}"))?;
61        st.end()
62    }
63}
64
65/// InvalidCurrencyCodeError is an error that occurs when an invalid currency
66/// code representation is encountered.  This should only occur when the
67/// currency is being decoded from a string representation.
68#[derive(Debug, Clone, Deserialize)]
69#[serde(tag = "code", rename = "INVALID_CURRENCY_CODE")]
70pub struct InvalidCurrencyCodeError {
71    pub currency: String,
72}
73
74impl ExplorerAPIError for InvalidCurrencyCodeError {
75    /// code returns a string identifier to uniquely identify the
76    /// [InvalidCurrencyCodeError]
77    fn code(&self) -> &str {
78        "INVALID_CURRENCY_CODE"
79    }
80}
81
82impl Display for InvalidCurrencyCodeError {
83    /// fmt formats the error into a human readable string
84    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85        write!(f, "invalid currency code: {}", self.currency)
86    }
87}
88
89impl Serialize for InvalidCurrencyCodeError {
90    /// serialize converts the error into a struct representation
91    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
92    where
93        S: Serializer,
94    {
95        let mut st = serializer.serialize_struct("InvalidCurrencyCodeError", 3)?;
96        st.serialize_field("code", &self.code())?;
97        st.serialize_field("currency", &self.currency)?;
98        st.serialize_field("message", &format!("{self}"))?;
99        st.end()
100    }
101}
102
103/// [CurrencyCode] represents an enumeration of all representable currency
104/// codes that are supported by the API.
105///
106/// [CurrencyCode] is an overloaded term for representing different types of
107/// currencies that could potentially be referenced / utilized within our
108/// system.  This list is currently not exhaustive, and is expected to grow
109/// as needed.  In fact, there may be too many entries in here as it stands.
110///
111/// The currency codes are annotated with a specific range that should map
112/// to the type that we'd expect.
113///
114/// Here's the allocated range for the various currency codes:
115/// - 1 - 999: Fiat Currency
116/// - 1001 - 9999: Crypto Currency
117/// - 10001 - 99999: Token Currency
118///
119/// For the Fiat Currencies, the [CurrencyCode] identifier is the Alpha3
120/// representation of the ISO4217 standard.  In addition, it's numeric value
121/// is mapped to the ISO4217 standard as well.
122///
123/// No such standard currently exists for Crypto Currencies or for the Token
124/// based currencies, so we just utilize an enumeration for these that is based
125/// on their ordering. This is not guaranteed to be stable, and code should
126/// not depend on their order.
127#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
128#[serde(try_from = "&str")]
129pub enum CurrencyCode {
130    FiatCurrencyStart = 0,
131    #[serde(rename = "JPY")]
132    Jpy = 392,
133    #[serde(rename = "GBP")]
134    Gbp = 826,
135    #[serde(rename = "USD")]
136    Usd = 840,
137    #[serde(rename = "EUR")]
138    Eur = 978,
139    #[serde(rename = "XXX")]
140    Xxx = 999,
141    FiatCurrencyEnd = 1000,
142
143    CryptoStart = 1001,
144    #[serde(rename = "ETH")]
145    Eth,
146    #[serde(rename = "XBT")]
147    Btc,
148    CryptoEnd = 10000,
149
150    TokenStart = 10001,
151    #[serde(rename = "ESP")]
152    Esp,
153    TokenEnd = 99999,
154}
155
156impl CurrencyCode {
157    pub fn is_fiat(&self) -> bool {
158        *self >= CurrencyCode::FiatCurrencyStart && *self <= CurrencyCode::FiatCurrencyEnd
159    }
160
161    pub fn is_crypto(&self) -> bool {
162        *self >= CurrencyCode::CryptoStart && *self <= CurrencyCode::CryptoEnd
163    }
164
165    pub fn is_token(&self) -> bool {
166        *self >= CurrencyCode::TokenStart && *self <= CurrencyCode::TokenEnd
167    }
168
169    /// significant_digits represents the total number of significant digits
170    /// that the currency in question utilizes.
171    ///
172    /// This is used to determine the precision of the currency when formatting
173    /// its representation as a string.
174    pub fn significant_digits(&self) -> usize {
175        match self {
176            Self::Jpy => 0,
177            Self::Gbp => 2,
178            Self::Usd => 2,
179            Self::Eur => 2,
180            Self::Btc => 8,
181            Self::Eth => 18,
182            _ => 0,
183        }
184    }
185}
186
187impl Display for CurrencyCode {
188    /// fmt formats the currency code into a human readable string
189    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190        match self {
191            CurrencyCode::Jpy => write!(f, "JPY"),
192            CurrencyCode::Gbp => write!(f, "GBP"),
193            CurrencyCode::Usd => write!(f, "USD"),
194            CurrencyCode::Eur => write!(f, "EUR"),
195            CurrencyCode::Eth => write!(f, "ETH"),
196            CurrencyCode::Btc => write!(f, "XBT"),
197            CurrencyCode::Xxx => write!(f, "XXX"),
198            CurrencyCode::Esp => write!(f, "ESP"),
199            _ => write!(f, "UnknownCurrency({})", *self as u16),
200        }
201    }
202}
203
204impl From<CurrencyCode> for u16 {
205    /// from converts the [CurrencyCode] into it's mapped numeric
206    /// representation.
207    fn from(currency: CurrencyCode) -> u16 {
208        currency as u16
209    }
210}
211
212impl TryFrom<&str> for CurrencyCode {
213    type Error = InvalidCurrencyCodeError;
214
215    fn try_from(value: &str) -> Result<Self, Self::Error> {
216        match value {
217            "JPY" => Ok(Self::Jpy),
218            "GBP" => Ok(Self::Gbp),
219            "USD" => Ok(Self::Usd),
220            "EUR" => Ok(Self::Eur),
221            "ETH" => Ok(Self::Eth),
222            "BTC" => Ok(Self::Btc),
223            "XBT" => Ok(Self::Btc),
224            "XXX" => Ok(Self::Xxx),
225            "ESP" => Ok(Self::Esp),
226            _ => Err(InvalidCurrencyCodeError {
227                currency: value.to_string(),
228            }),
229        }
230    }
231}
232
233impl From<CurrencyCode> for String {
234    /// from converts the [CurrencyCode] into a string representation utilizing
235    /// the fmt method for the [CurrencyCode].
236    fn from(currency: CurrencyCode) -> String {
237        currency.to_string()
238    }
239}
240
241#[cfg(test)]
242mod test {
243    #[test]
244    fn test_serialize_deserialize() {
245        #[derive(Debug, PartialEq)]
246        pub enum EncodeMatch {
247            Yes,
248            No,
249        }
250
251        let cases = [
252            (r#""JPY""#, super::CurrencyCode::Jpy, EncodeMatch::Yes),
253            (r#""GBP""#, super::CurrencyCode::Gbp, EncodeMatch::Yes),
254            (r#""USD""#, super::CurrencyCode::Usd, EncodeMatch::Yes),
255            (r#""EUR""#, super::CurrencyCode::Eur, EncodeMatch::Yes),
256            (r#""ETH""#, super::CurrencyCode::Eth, EncodeMatch::Yes),
257            (r#""BTC""#, super::CurrencyCode::Btc, EncodeMatch::No),
258            (r#""XBT""#, super::CurrencyCode::Btc, EncodeMatch::Yes),
259            (r#""XXX""#, super::CurrencyCode::Xxx, EncodeMatch::Yes),
260            (r#""ESP""#, super::CurrencyCode::Esp, EncodeMatch::Yes),
261        ];
262
263        for (json, expected, should_match) in cases.iter() {
264            {
265                let actual: super::CurrencyCode = serde_json::from_str(json).unwrap();
266                let have = actual;
267                let want = *expected;
268                assert_eq!(
269                    have, want,
270                    "decode json: {}: have: {have}, want: {want}",
271                    *json
272                );
273            }
274
275            if *should_match != EncodeMatch::Yes {
276                continue;
277            }
278
279            {
280                let encoded = serde_json::to_string(expected);
281                assert!(encoded.is_ok(), "encode json: {}", *json);
282
283                let encoded_json = encoded.unwrap();
284
285                let have = &encoded_json;
286                let want = *json;
287                assert_eq!(
288                    have, want,
289                    "encoded json for {expected} does not match expectation: have: {have}, want: {want}"
290                );
291            }
292        }
293    }
294}