1use alloc::borrow::Cow;
2
3use serde::{Deserialize, Serialize};
4use serde_repr::{Deserialize_repr, Serialize_repr};
5use serde_with::skip_serializing_none;
6use strum_macros::{AsRefStr, Display, EnumIter};
7
8use crate::models::{ledger::objects::LedgerEntryType, Model, XRPLModelException, XRPLModelResult};
9
10use super::{CommonFields, LedgerObject};
11
12#[derive(
17 Debug, Eq, PartialEq, Clone, Serialize_repr, Deserialize_repr, Display, AsRefStr, EnumIter,
18)]
19#[repr(u32)]
20pub enum MPTokenFlag {
21 LsfMPTLocked = 0x0001,
23 LsfMPTAuthorized = 0x0002,
26 LsfMPTAMM = 0x0004,
29}
30
31#[skip_serializing_none]
36#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
37#[serde(rename_all = "PascalCase")]
38pub struct MPToken<'a> {
39 #[serde(flatten)]
41 pub common_fields: CommonFields<'a, MPTokenFlag>,
42 pub account: Cow<'a, str>,
44 #[serde(rename = "MPTokenIssuanceID")]
46 pub mptoken_issuance_id: Cow<'a, str>,
47 #[serde(rename = "MPTAmount")]
50 pub mpt_amount: Cow<'a, str>,
51 #[serde(rename = "PreviousTxnID")]
54 pub previous_txn_id: Cow<'a, str>,
55 pub previous_txn_lgr_seq: u32,
58 pub owner_node: Option<Cow<'a, str>>,
61 pub locked_amount: Option<Cow<'a, str>>,
64 pub confidential_balance_inbox: Option<Cow<'a, str>>,
68 pub confidential_balance_spending: Option<Cow<'a, str>>,
71 pub confidential_balance_version: Option<u32>,
74 pub issuer_encrypted_balance: Option<Cow<'a, str>>,
77 pub auditor_encrypted_balance: Option<Cow<'a, str>>,
80 pub holder_encryption_key: Option<Cow<'a, str>>,
83}
84
85impl<'a> Model for MPToken<'a> {
86 fn get_errors(&self) -> XRPLModelResult<()> {
87 if self.common_fields.index.is_none() && self.common_fields.ledger_index.is_none() {
88 return Err(XRPLModelException::MissingField(
89 "index or ledger_index".into(),
90 ));
91 }
92 Ok(())
93 }
94}
95
96impl<'a> LedgerObject<MPTokenFlag> for MPToken<'a> {
97 fn get_ledger_entry_type(&self) -> LedgerEntryType {
98 self.common_fields.get_ledger_entry_type()
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use alloc::borrow::Cow;
105 use alloc::vec;
106
107 use crate::models::FlagCollection;
108
109 use super::*;
110 use crate::utils::testing::test_constants::*;
111
112 #[test]
113 fn test_serde() {
114 let mptoken = MPToken {
115 common_fields: CommonFields {
116 flags: FlagCollection(vec![MPTokenFlag::LsfMPTAuthorized]),
117 ledger_entry_type: LedgerEntryType::MPToken,
118 index: Some(Cow::from(
119 "BFA9BE27383FA315651E26FDE1FA30815C5A5D0544EE10EC33D3E92532993769",
120 )),
121 ledger_index: None,
122 },
123 account: ACCOUNT_GENESIS.into(),
124 mptoken_issuance_id: "00000001A407AF5856CEFBF81F3D4A0000000000A407AF58".into(),
125 mpt_amount: "1000".into(),
126 previous_txn_id: "E3FE6EA3D48F0C2B639448020EA4F03D4F4F8FFDB243A852A0F59177921B4879"
127 .into(),
128 previous_txn_lgr_seq: 123456,
129 owner_node: Some("0".into()),
130 locked_amount: None,
131 confidential_balance_inbox: None,
132 confidential_balance_spending: None,
133 confidential_balance_version: None,
134 issuer_encrypted_balance: None,
135 auditor_encrypted_balance: None,
136 holder_encryption_key: None,
137 };
138
139 let serialized = serde_json::to_string(&mptoken).unwrap();
140 let deserialized: MPToken = serde_json::from_str(&serialized).unwrap();
141 assert_eq!(mptoken, deserialized);
142 }
143
144 #[test]
145 fn test_serde_with_locked_amount() {
146 let mptoken = MPToken {
147 common_fields: CommonFields {
148 flags: FlagCollection(vec![]),
149 ledger_entry_type: LedgerEntryType::MPToken,
150 index: Some(Cow::from(
151 "BFA9BE27383FA315651E26FDE1FA30815C5A5D0544EE10EC33D3E92532993769",
152 )),
153 ledger_index: None,
154 },
155 account: ACCOUNT_GENESIS.into(),
156 mptoken_issuance_id: "00000001A407AF5856CEFBF81F3D4A0000000000A407AF58".into(),
157 mpt_amount: "500".into(),
158 previous_txn_id: "E3FE6EA3D48F0C2B639448020EA4F03D4F4F8FFDB243A852A0F59177921B4879"
159 .into(),
160 previous_txn_lgr_seq: 123456,
161 owner_node: None,
162 locked_amount: Some("250".into()),
163 confidential_balance_inbox: None,
164 confidential_balance_spending: None,
165 confidential_balance_version: None,
166 issuer_encrypted_balance: None,
167 auditor_encrypted_balance: None,
168 holder_encryption_key: None,
169 };
170
171 let serialized = serde_json::to_string(&mptoken).unwrap();
172 assert!(
173 serialized.contains("\"LockedAmount\":\"250\""),
174 "LockedAmount must serialize as PascalCase: {serialized}"
175 );
176 let deserialized: MPToken = serde_json::from_str(&serialized).unwrap();
177 assert_eq!(mptoken, deserialized);
178 }
179
180 #[test]
181 fn test_missing_index_and_ledger_index_error() {
182 let mptoken = MPToken {
183 common_fields: CommonFields {
184 flags: FlagCollection(vec![]),
185 ledger_entry_type: LedgerEntryType::MPToken,
186 index: None,
187 ledger_index: None,
188 },
189 account: ACCOUNT_GENESIS.into(),
190 mptoken_issuance_id: "00000001A407AF5856CEFBF81F3D4A0000000000A407AF58".into(),
191 mpt_amount: "0".into(),
192 previous_txn_id: "E3FE6EA3D48F0C2B639448020EA4F03D4F4F8FFDB243A852A0F59177921B4879"
193 .into(),
194 previous_txn_lgr_seq: 0,
195 owner_node: None,
196 locked_amount: None,
197 confidential_balance_inbox: None,
198 confidential_balance_spending: None,
199 confidential_balance_version: None,
200 issuer_encrypted_balance: None,
201 auditor_encrypted_balance: None,
202 holder_encryption_key: None,
203 };
204
205 assert!(mptoken.validate().is_err());
206 }
207
208 #[test]
209 fn test_validate_ok() {
210 let mptoken = MPToken {
211 common_fields: CommonFields {
212 flags: FlagCollection(vec![]),
213 ledger_entry_type: LedgerEntryType::MPToken,
214 index: Some(Cow::from(
215 "BFA9BE27383FA315651E26FDE1FA30815C5A5D0544EE10EC33D3E92532993769",
216 )),
217 ledger_index: None,
218 },
219 account: ACCOUNT_GENESIS.into(),
220 mptoken_issuance_id: "00000001A407AF5856CEFBF81F3D4A0000000000A407AF58".into(),
221 mpt_amount: "0".into(),
222 previous_txn_id: "E3FE6EA3D48F0C2B639448020EA4F03D4F4F8FFDB243A852A0F59177921B4879"
223 .into(),
224 previous_txn_lgr_seq: 0,
225 owner_node: None,
226 locked_amount: None,
227 confidential_balance_inbox: None,
228 confidential_balance_spending: None,
229 confidential_balance_version: None,
230 issuer_encrypted_balance: None,
231 auditor_encrypted_balance: None,
232 holder_encryption_key: None,
233 };
234 assert!(mptoken.validate().is_ok());
235 }
236
237 #[test]
238 fn test_ledger_entry_type() {
239 let mptoken = MPToken {
240 common_fields: CommonFields {
241 flags: FlagCollection(vec![]),
242 ledger_entry_type: LedgerEntryType::MPToken,
243 index: Some(Cow::from(
244 "CF9421C5E0A80C7BC5F52A3566CCBD2E8F14C3DA1E65F3F3AB1EC5B5A3BDFEA",
245 )),
246 ledger_index: Some(Cow::from("5000000")),
247 },
248 account: ACCOUNT_GENESIS.into(),
249 mptoken_issuance_id: "00000001A407AF5856CEFBF81F3D4A0000000000A407AF58".into(),
250 mpt_amount: "0".into(),
251 previous_txn_id: "E3FE6EA3D48F0C2B639448020EA4F03D4F4F8FFDB243A852A0F59177921B4879"
252 .into(),
253 previous_txn_lgr_seq: 4999998,
254 owner_node: None,
255 locked_amount: None,
256 confidential_balance_inbox: None,
257 confidential_balance_spending: None,
258 confidential_balance_version: None,
259 issuer_encrypted_balance: None,
260 auditor_encrypted_balance: None,
261 holder_encryption_key: None,
262 };
263
264 assert_eq!(mptoken.get_ledger_entry_type(), LedgerEntryType::MPToken);
265 }
266
267 #[test]
268 fn test_lsf_mpt_amm_round_trip() {
269 let json = r#"{
273 "LedgerEntryType": "MPToken",
274 "Flags": 4,
275 "index": "BFA9BE27383FA315651E26FDE1FA30815C5A5D0544EE10EC33D3E92532993769",
276 "Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
277 "MPTokenIssuanceID": "00000001A407AF5856CEFBF81F3D4A0000000000A407AF58",
278 "MPTAmount": "0",
279 "PreviousTxnID": "E3FE6EA3D48F0C2B639448020EA4F03D4F4F8FFDB243A852A0F59177921B4879",
280 "PreviousTxnLgrSeq": 0
281 }"#;
282
283 let mptoken: MPToken = serde_json::from_str(json).unwrap();
284 assert!(
285 mptoken
286 .common_fields
287 .flags
288 .0
289 .contains(&MPTokenFlag::LsfMPTAMM),
290 "expected LsfMPTAMM in flags, got {:?}",
291 mptoken.common_fields.flags
292 );
293
294 let reserialized = serde_json::to_string(&mptoken).unwrap();
296 assert!(
297 reserialized.contains("\"Flags\":4"),
298 "expected Flags:4 after round-trip, got: {reserialized}"
299 );
300 }
301}