use alloc::borrow::Cow;
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use serde_with::skip_serializing_none;
use strum_macros::{AsRefStr, Display, EnumIter};
use crate::models::{ledger::objects::LedgerEntryType, Model, XRPLModelException, XRPLModelResult};
use super::{CommonFields, LedgerObject};
#[derive(
Debug, Eq, PartialEq, Clone, Serialize_repr, Deserialize_repr, Display, AsRefStr, EnumIter,
)]
#[repr(u32)]
pub enum MPTokenFlag {
LsfMPTLocked = 0x0001,
LsfMPTAuthorized = 0x0002,
LsfMPTAMM = 0x0004,
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
#[serde(rename_all = "PascalCase")]
pub struct MPToken<'a> {
#[serde(flatten)]
pub common_fields: CommonFields<'a, MPTokenFlag>,
pub account: Cow<'a, str>,
#[serde(rename = "MPTokenIssuanceID")]
pub mptoken_issuance_id: Cow<'a, str>,
#[serde(rename = "MPTAmount")]
pub mpt_amount: Cow<'a, str>,
#[serde(rename = "PreviousTxnID")]
pub previous_txn_id: Cow<'a, str>,
pub previous_txn_lgr_seq: u32,
pub owner_node: Option<Cow<'a, str>>,
pub locked_amount: Option<Cow<'a, str>>,
}
impl<'a> Model for MPToken<'a> {
fn get_errors(&self) -> XRPLModelResult<()> {
if self.common_fields.index.is_none() && self.common_fields.ledger_index.is_none() {
return Err(XRPLModelException::MissingField(
"index or ledger_index".into(),
));
}
Ok(())
}
}
impl<'a> LedgerObject<MPTokenFlag> for MPToken<'a> {
fn get_ledger_entry_type(&self) -> LedgerEntryType {
self.common_fields.get_ledger_entry_type()
}
}
#[cfg(test)]
mod tests {
use alloc::borrow::Cow;
use alloc::vec;
use crate::models::FlagCollection;
use super::*;
use crate::utils::testing::test_constants::*;
#[test]
fn test_serde() {
let mptoken = MPToken {
common_fields: CommonFields {
flags: FlagCollection(vec![MPTokenFlag::LsfMPTAuthorized]),
ledger_entry_type: LedgerEntryType::MPToken,
index: Some(Cow::from(
"BFA9BE27383FA315651E26FDE1FA30815C5A5D0544EE10EC33D3E92532993769",
)),
ledger_index: None,
},
account: ACCOUNT_GENESIS.into(),
mptoken_issuance_id: "00000001A407AF5856CEFBF81F3D4A0000000000A407AF58".into(),
mpt_amount: "1000".into(),
previous_txn_id: "E3FE6EA3D48F0C2B639448020EA4F03D4F4F8FFDB243A852A0F59177921B4879"
.into(),
previous_txn_lgr_seq: 123456,
owner_node: Some("0".into()),
locked_amount: None,
};
let serialized = serde_json::to_string(&mptoken).unwrap();
let deserialized: MPToken = serde_json::from_str(&serialized).unwrap();
assert_eq!(mptoken, deserialized);
}
#[test]
fn test_serde_with_locked_amount() {
let mptoken = MPToken {
common_fields: CommonFields {
flags: FlagCollection(vec![]),
ledger_entry_type: LedgerEntryType::MPToken,
index: Some(Cow::from(
"BFA9BE27383FA315651E26FDE1FA30815C5A5D0544EE10EC33D3E92532993769",
)),
ledger_index: None,
},
account: ACCOUNT_GENESIS.into(),
mptoken_issuance_id: "00000001A407AF5856CEFBF81F3D4A0000000000A407AF58".into(),
mpt_amount: "500".into(),
previous_txn_id: "E3FE6EA3D48F0C2B639448020EA4F03D4F4F8FFDB243A852A0F59177921B4879"
.into(),
previous_txn_lgr_seq: 123456,
owner_node: None,
locked_amount: Some("250".into()),
};
let serialized = serde_json::to_string(&mptoken).unwrap();
assert!(
serialized.contains("\"LockedAmount\":\"250\""),
"LockedAmount must serialize as PascalCase: {serialized}"
);
let deserialized: MPToken = serde_json::from_str(&serialized).unwrap();
assert_eq!(mptoken, deserialized);
}
#[test]
fn test_missing_index_and_ledger_index_error() {
let mptoken = MPToken {
common_fields: CommonFields {
flags: FlagCollection(vec![]),
ledger_entry_type: LedgerEntryType::MPToken,
index: None,
ledger_index: None,
},
account: ACCOUNT_GENESIS.into(),
mptoken_issuance_id: "00000001A407AF5856CEFBF81F3D4A0000000000A407AF58".into(),
mpt_amount: "0".into(),
previous_txn_id: "E3FE6EA3D48F0C2B639448020EA4F03D4F4F8FFDB243A852A0F59177921B4879"
.into(),
previous_txn_lgr_seq: 0,
owner_node: None,
locked_amount: None,
};
assert!(mptoken.validate().is_err());
}
#[test]
fn test_validate_ok() {
let mptoken = MPToken {
common_fields: CommonFields {
flags: FlagCollection(vec![]),
ledger_entry_type: LedgerEntryType::MPToken,
index: Some(Cow::from(
"BFA9BE27383FA315651E26FDE1FA30815C5A5D0544EE10EC33D3E92532993769",
)),
ledger_index: None,
},
account: ACCOUNT_GENESIS.into(),
mptoken_issuance_id: "00000001A407AF5856CEFBF81F3D4A0000000000A407AF58".into(),
mpt_amount: "0".into(),
previous_txn_id: "E3FE6EA3D48F0C2B639448020EA4F03D4F4F8FFDB243A852A0F59177921B4879"
.into(),
previous_txn_lgr_seq: 0,
owner_node: None,
locked_amount: None,
};
assert!(mptoken.validate().is_ok());
}
#[test]
fn test_ledger_entry_type() {
let mptoken = MPToken {
common_fields: CommonFields {
flags: FlagCollection(vec![]),
ledger_entry_type: LedgerEntryType::MPToken,
index: Some(Cow::from(
"CF9421C5E0A80C7BC5F52A3566CCBD2E8F14C3DA1E65F3F3AB1EC5B5A3BDFEA",
)),
ledger_index: Some(Cow::from("5000000")),
},
account: ACCOUNT_GENESIS.into(),
mptoken_issuance_id: "00000001A407AF5856CEFBF81F3D4A0000000000A407AF58".into(),
mpt_amount: "0".into(),
previous_txn_id: "E3FE6EA3D48F0C2B639448020EA4F03D4F4F8FFDB243A852A0F59177921B4879"
.into(),
previous_txn_lgr_seq: 4999998,
owner_node: None,
locked_amount: None,
};
assert_eq!(mptoken.get_ledger_entry_type(), LedgerEntryType::MPToken);
}
#[test]
fn test_lsf_mpt_amm_round_trip() {
let json = r#"{
"LedgerEntryType": "MPToken",
"Flags": 4,
"index": "BFA9BE27383FA315651E26FDE1FA30815C5A5D0544EE10EC33D3E92532993769",
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
"MPTokenIssuanceID": "00000001A407AF5856CEFBF81F3D4A0000000000A407AF58",
"MPTAmount": "0",
"PreviousTxnID": "E3FE6EA3D48F0C2B639448020EA4F03D4F4F8FFDB243A852A0F59177921B4879",
"PreviousTxnLgrSeq": 0
}"#;
let mptoken: MPToken = serde_json::from_str(json).unwrap();
assert!(
mptoken
.common_fields
.flags
.0
.contains(&MPTokenFlag::LsfMPTAMM),
"expected LsfMPTAMM in flags, got {:?}",
mptoken.common_fields.flags
);
let reserialized = serde_json::to_string(&mptoken).unwrap();
assert!(
reserialized.contains("\"Flags\":4"),
"expected Flags:4 after round-trip, got: {reserialized}"
);
}
}