hotshot_query_service/explorer/
currency.rs1use std::fmt::Display;
14
15use serde::{ser::SerializeStruct, Deserialize, Serialize, Serializer};
16
17use super::errors::ExplorerAPIError;
18
19#[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 fn code(&self) -> &str {
35 "CURRENCY_MISMATCH"
36 }
37}
38
39impl Display for CurrencyMismatchError {
40 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 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#[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 fn code(&self) -> &str {
78 "INVALID_CURRENCY_CODE"
79 }
80}
81
82impl Display for InvalidCurrencyCodeError {
83 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 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#[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 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 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 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 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}