Skip to main content

xrpl/models/amount/
mpt_amount.rs

1use crate::models::transactions::mptoken_issuance_set::validate_mptoken_issuance_id;
2use crate::models::{Model, XRPLModelResult};
3use alloc::{borrow::Cow, string::ToString};
4use serde::{Deserialize, Serialize};
5
6/// An MPT (Multi-Purpose Token) amount.
7///
8/// MPT amounts represent a quantity of a specific Multi-Purpose Token,
9/// identified by its issuance ID.
10///
11/// JSON shape per XRPL:
12/// `{"value": "<u64 string>", "mpt_issuance_id": "<48-hex-char string>"}`
13///
14/// See MPToken:
15/// `<https://xrpl.org/docs/references/protocol/ledger-data/ledger-entry-types/mptoken>`
16#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Default)]
17#[serde(deny_unknown_fields)]
18pub struct MPTAmount<'a> {
19    /// The token quantity, expressed as a non-negative integer string.
20    pub value: Cow<'a, str>,
21    /// The MPTokenIssuanceID that identifies which MPT this amount belongs to.
22    /// Must be a 48-character ASCII hex string (24 bytes, Hash192).
23    pub mpt_issuance_id: Cow<'a, str>,
24}
25
26impl<'a> Model for MPTAmount<'a> {
27    fn get_errors(&self) -> XRPLModelResult<()> {
28        // MPT amounts are unsigned integer strings in [0, i64::MAX] per XLS-33 / rippled.
29        // Match xrpl.js' /^[0-9]+$/ sanity check instead of Rust's looser u64 parser,
30        // which would accept values such as "+1".
31        if self.value.is_empty() || !self.value.bytes().all(|b| b.is_ascii_digit()) {
32            return Err(crate::models::XRPLModelException::InvalidValueFormat {
33                field: "value".into(),
34                format: "unsigned integer string".into(),
35                found: self.value.to_string(),
36            });
37        }
38        let n: u64 = self.value.parse()?;
39        if n > i64::MAX as u64 {
40            return Err(crate::models::XRPLModelException::InvalidValue {
41                field: "value".into(),
42                expected: alloc::format!("MPT amount <= {} (i64::MAX)", i64::MAX),
43                found: self.value.to_string(),
44            });
45        }
46        validate_mptoken_issuance_id(self.mpt_issuance_id.as_ref())?;
47        Ok(())
48    }
49}
50
51impl<'a> MPTAmount<'a> {
52    pub fn new(value: Cow<'a, str>, mpt_issuance_id: Cow<'a, str>) -> Self {
53        Self {
54            value,
55            mpt_issuance_id,
56        }
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use crate::models::Model;
63
64    use super::*;
65
66    const VALID_ID: &str = "00000001A407AF5856CEFBF81F3D4A0000000000A407AF58";
67
68    #[test]
69    fn test_mpt_amount_serde_roundtrip() {
70        let amount = MPTAmount::new("100".into(), VALID_ID.into());
71        let json = serde_json::to_string(&amount).unwrap();
72        let decoded: MPTAmount = serde_json::from_str(&json).unwrap();
73        assert_eq!(amount, decoded);
74    }
75
76    #[test]
77    fn test_mpt_amount_get_errors_valid() {
78        let amount = MPTAmount::new("9223372036854775807".into(), VALID_ID.into());
79        assert!(amount.get_errors().is_ok());
80    }
81
82    #[test]
83    fn test_mpt_amount_get_errors_zero() {
84        let amount = MPTAmount::new("0".into(), VALID_ID.into());
85        assert!(amount.get_errors().is_ok());
86    }
87
88    #[test]
89    fn test_mpt_amount_get_errors_bad_value_decimal() {
90        let amount = MPTAmount::new("1.5".into(), VALID_ID.into());
91        assert!(amount.get_errors().is_err());
92    }
93
94    #[test]
95    fn test_mpt_amount_get_errors_bad_value_negative() {
96        let amount = MPTAmount::new("-1".into(), VALID_ID.into());
97        assert!(amount.get_errors().is_err());
98    }
99
100    #[test]
101    fn test_mpt_amount_get_errors_bad_value_plus_prefix() {
102        let amount = MPTAmount::new("+1".into(), VALID_ID.into());
103        assert!(amount.get_errors().is_err());
104    }
105
106    #[test]
107    fn test_mpt_amount_get_errors_rejects_above_i64_max() {
108        // i64::MAX + 1 = 9223372036854775808: parses as u64 but exceeds protocol limit
109        let amount = MPTAmount::new("9223372036854775808".into(), VALID_ID.into());
110        assert!(amount.get_errors().is_err());
111    }
112
113    #[test]
114    fn test_mpt_amount_get_errors_bad_id_too_short() {
115        let amount = MPTAmount::new("100".into(), "DEAD".into());
116        assert!(amount.get_errors().is_err());
117    }
118
119    #[test]
120    fn test_mpt_amount_get_errors_bad_id_non_hex() {
121        let bad_id = "Z".repeat(48);
122        let amount = MPTAmount::new("100".into(), bad_id.as_str().into());
123        assert!(amount.get_errors().is_err());
124    }
125}