Skip to main content

xrpl/models/currency/
mod.rs

1pub mod issued_currency;
2pub mod mpt_currency;
3pub mod xrp;
4
5use crate::models::Model;
6use alloc::borrow::Cow;
7pub use issued_currency::*;
8pub use mpt_currency::*;
9use serde::{Deserialize, Deserializer, Serialize};
10use strum_macros::Display;
11pub use xrp::*;
12
13use super::{IssuedCurrencyAmount, MPTAmount, XRPAmount};
14
15pub trait ToAmount<'a, A> {
16    fn to_amount(&self, value: Cow<'a, str>) -> A;
17}
18
19#[derive(Debug, PartialEq, Eq, Clone, Serialize, Display)]
20#[serde(untagged)]
21pub enum Currency<'a> {
22    /// MPTCurrency variant must be checked first: object with only `mpt_issuance_id`
23    MPTCurrency(MPTCurrency<'a>),
24    /// IssuedCurrency variant (requires both currency and issuer fields)
25    IssuedCurrency(IssuedCurrency<'a>),
26    /// XRP variant (only requires currency field set to "XRP")
27    XRP(XRP<'a>),
28}
29
30impl<'de, 'a> Deserialize<'de> for Currency<'a> {
31    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
32    where
33        D: Deserializer<'de>,
34    {
35        let value = serde_json::Value::deserialize(deserializer)?;
36
37        // Check if it's an object (all Currency variants are objects)
38        if let Some(obj) = value.as_object() {
39            let has_currency = obj.contains_key("currency");
40            let has_issuer = obj.contains_key("issuer");
41
42            // Try MPTCurrency first: object with `mpt_issuance_id` and no currency/issuer keys.
43            // Guard prevents a hybrid object from silently discarding ICA fields.
44            if obj.contains_key("mpt_issuance_id")
45                && !obj.contains_key("currency")
46                && !obj.contains_key("issuer")
47            {
48                if let Ok(mpt) = serde_json::from_value::<MPTCurrency>(value.clone()) {
49                    return Ok(Currency::MPTCurrency(mpt));
50                }
51            }
52
53            // Try to deserialize as IssuedCurrency if issuer field exists (more specific variant)
54            if has_issuer {
55                if let Ok(ic) = serde_json::from_value::<IssuedCurrency>(value.clone()) {
56                    return Ok(Currency::IssuedCurrency(ic));
57                }
58            }
59
60            // Try to deserialize as XRP if currency field exists and equals "XRP"
61            if has_currency {
62                if let Ok(xrp) = serde_json::from_value::<XRP>(value.clone()) {
63                    // Validate that XRP currency is actually "XRP"
64                    if xrp.currency == "XRP" {
65                        return Ok(Currency::XRP(xrp));
66                    }
67                }
68            }
69
70            // If we got here with an object but it doesn't match any variant, error
71            return Err(serde::de::Error::custom(
72                "Invalid Currency object: must have 'mpt_issuance_id' for MPTCurrency, 'issuer' for IssuedCurrency, or 'currency'='XRP' for XRP"
73            ));
74        }
75
76        Err(serde::de::Error::custom("Currency must be a JSON object"))
77    }
78}
79
80impl<'a> Model for Currency<'a> {
81    fn get_errors(&self) -> crate::models::XRPLModelResult<()> {
82        match self {
83            Currency::MPTCurrency(mpt) => mpt.get_errors(),
84            Currency::IssuedCurrency(issued_currency) => issued_currency.get_errors(),
85            Currency::XRP(xrp) => xrp.get_errors(),
86        }
87    }
88}
89
90impl<'a> Default for Currency<'a> {
91    fn default() -> Self {
92        Self::XRP(XRP::new())
93    }
94}
95
96impl<'a> From<MPTCurrency<'a>> for Currency<'a> {
97    fn from(value: MPTCurrency<'a>) -> Self {
98        Self::MPTCurrency(value)
99    }
100}
101
102impl<'a> From<IssuedCurrency<'a>> for Currency<'a> {
103    fn from(value: IssuedCurrency<'a>) -> Self {
104        Self::IssuedCurrency(value)
105    }
106}
107
108impl<'a> From<XRP<'a>> for Currency<'a> {
109    fn from(value: XRP<'a>) -> Self {
110        Self::XRP(value)
111    }
112}
113
114impl<'a> From<IssuedCurrencyAmount<'a>> for Currency<'a> {
115    fn from(value: IssuedCurrencyAmount<'a>) -> Self {
116        IssuedCurrency::new(value.currency, value.issuer).into()
117    }
118}
119
120impl<'a> From<XRPAmount<'a>> for Currency<'a> {
121    fn from(_value: XRPAmount<'a>) -> Self {
122        XRP::new().into()
123    }
124}
125
126impl<'a> From<&MPTAmount<'a>> for Currency<'a> {
127    fn from(value: &MPTAmount<'a>) -> Self {
128        MPTCurrency::new(value.mpt_issuance_id.clone()).into()
129    }
130}
131
132impl<'a> From<&IssuedCurrencyAmount<'a>> for Currency<'a> {
133    fn from(value: &IssuedCurrencyAmount<'a>) -> Self {
134        IssuedCurrency::new(value.currency.clone(), value.issuer.clone()).into()
135    }
136}
137
138impl<'a> From<&XRPAmount<'a>> for Currency<'a> {
139    fn from(_value: &XRPAmount<'a>) -> Self {
140        XRP::new().into()
141    }
142}
143
144#[cfg(test)]
145mod tests_currency_enum {
146    use crate::models::Model;
147
148    use super::*;
149
150    const VALID_ID: &str = "00000001A407AF5856CEFBF81F3D4A0000000000A407AF58";
151
152    #[test]
153    fn test_currency_deserialize_mpt() {
154        let json = alloc::format!(r#"{{"mpt_issuance_id":"{VALID_ID}"}}"#);
155        let cur: Currency = serde_json::from_str(&json).unwrap();
156        assert!(matches!(cur, Currency::MPTCurrency(_)));
157    }
158
159    #[test]
160    fn test_currency_mpt_json_round_trip() {
161        let original = Currency::MPTCurrency(MPTCurrency::new(VALID_ID.into()));
162        let json = serde_json::to_string(&original).unwrap();
163        let decoded: Currency = serde_json::from_str(&json).unwrap();
164        assert_eq!(original, decoded);
165    }
166
167    #[test]
168    fn test_currency_deserialize_issued_not_mpt() {
169        let json = r#"{"currency":"USD","issuer":"rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd"}"#;
170        let cur: Currency = serde_json::from_str(json).unwrap();
171        assert!(matches!(cur, Currency::IssuedCurrency(_)));
172    }
173
174    #[test]
175    fn test_currency_mpt_get_errors_valid() {
176        let cur = Currency::MPTCurrency(MPTCurrency::new(VALID_ID.into()));
177        assert!(cur.get_errors().is_ok());
178    }
179
180    #[test]
181    fn test_currency_mpt_get_errors_bad_id() {
182        let cur = Currency::MPTCurrency(MPTCurrency::new("TOOSHORT".into()));
183        assert!(cur.get_errors().is_err());
184    }
185}