xrpl/models/ledger/objects/
credential.rs1use crate::models::ledger::objects::LedgerEntryType;
2use crate::models::FlagCollection;
3use crate::models::Model;
4use alloc::borrow::Cow;
5
6use serde::{Deserialize, Serialize};
7use serde_repr::{Deserialize_repr, Serialize_repr};
8
9use serde_with::skip_serializing_none;
10use strum_macros::{AsRefStr, Display, EnumIter};
11
12use super::{CommonFields, LedgerObject};
13
14#[derive(
15 Debug, Eq, PartialEq, Clone, Serialize_repr, Deserialize_repr, Display, AsRefStr, EnumIter,
16)]
17#[repr(u32)]
18pub enum CredentialFlag {
19 LsfAccepted = 0x00010000,
21}
22
23#[skip_serializing_none]
27#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
28#[serde(rename_all = "PascalCase")]
29pub struct Credential<'a> {
30 #[serde(flatten)]
32 pub common_fields: CommonFields<'a, CredentialFlag>,
33 pub subject: Cow<'a, str>,
35 pub issuer: Cow<'a, str>,
37 pub credential_type: Cow<'a, str>,
39 pub expiration: Option<u32>,
41 #[serde(rename = "URI")]
43 pub uri: Option<Cow<'a, str>>,
44 pub subject_node: Option<Cow<'a, str>>,
47 pub issuer_node: Cow<'a, str>,
49 #[serde(rename = "PreviousTxnID")]
51 pub previous_txn_id: Cow<'a, str>,
52 pub previous_txn_lgr_seq: u32,
54}
55
56impl<'a> Model for Credential<'a> {}
57
58impl<'a> LedgerObject<CredentialFlag> for Credential<'a> {
59 fn get_ledger_entry_type(&self) -> LedgerEntryType {
60 self.common_fields.get_ledger_entry_type()
61 }
62}
63
64impl<'a> Credential<'a> {
65 #[allow(clippy::too_many_arguments)]
66 pub fn new(
67 index: Option<Cow<'a, str>>,
68 ledger_index: Option<Cow<'a, str>>,
69 subject: Cow<'a, str>,
70 issuer: Cow<'a, str>,
71 credential_type: Cow<'a, str>,
72 expiration: Option<u32>,
73 uri: Option<Cow<'a, str>>,
74 subject_node: Option<Cow<'a, str>>,
75 issuer_node: Cow<'a, str>,
76 previous_txn_id: Cow<'a, str>,
77 previous_txn_lgr_seq: u32,
78 ) -> Self {
79 Self {
80 common_fields: CommonFields {
81 flags: FlagCollection::default(),
82 ledger_entry_type: LedgerEntryType::Credential,
83 index,
84 ledger_index,
85 },
86 subject,
87 issuer,
88 credential_type,
89 expiration,
90 uri,
91 subject_node,
92 issuer_node,
93 previous_txn_id,
94 previous_txn_lgr_seq,
95 }
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
104 fn test_serde() {
105 let credential = Credential::new(
106 Some(Cow::from(
107 "DD40031C6C21164E7673A47C35513D52A6B0F1349A873EE0D188D8994CD4D001",
108 )),
109 None,
110 Cow::from("rALICE1111111111111111111111111111"),
111 Cow::from("rISABEL111111111111111111111111111"),
112 Cow::from("4B5943"),
113 Some(789004799),
114 Some(Cow::from(
115 "69736162656C2E636F6D2F63726564656E7469616C732F6B79632F616C696365",
116 )),
117 Some(Cow::from("0000000000000000")),
118 Cow::from("0000000000000000"),
119 Cow::from("3E8964D5A86B3CD6B9ECB33310D4E073D64C865A5B866200AD2B7E29F8326702"),
120 8,
121 );
122 let serialized = serde_json::to_string(&credential).unwrap();
123
124 let deserialized: Credential = serde_json::from_str(&serialized).unwrap();
125
126 assert_eq!(credential, deserialized);
127 }
128
129 #[test]
130 fn test_serde_round_trip_all_fields() {
131 let credential = Credential {
133 common_fields: CommonFields {
134 flags: FlagCollection::default(),
135 ledger_entry_type: LedgerEntryType::Credential,
136 index: Some(Cow::from(
137 "DD40031C6C21164E7673A47C35513D52A6B0F1349A873EE0D188D8994CD4D001",
138 )),
139 ledger_index: Some(Cow::from("42")),
140 },
141 subject: Cow::from("rALICE1111111111111111111111111111"),
142 issuer: Cow::from("rISABEL111111111111111111111111111"),
143 credential_type: Cow::from("4B5943"),
144 expiration: Some(789004799),
145 uri: Some(Cow::from(
146 "69736162656C2E636F6D2F63726564656E7469616C732F6B79632F616C696365",
147 )),
148 subject_node: Some(Cow::from("0000000000000000")),
149 issuer_node: Cow::from("0000000000000001"),
150 previous_txn_id: Cow::from(
151 "3E8964D5A86B3CD6B9ECB33310D4E073D64C865A5B866200AD2B7E29F8326702",
152 ),
153 previous_txn_lgr_seq: 8,
154 };
155 let json = serde_json::to_string(&credential).unwrap();
156 let deserialized: Credential = serde_json::from_str(&json).unwrap();
157 assert_eq!(credential, deserialized);
158 }
159
160 #[test]
161 fn test_serde_round_trip_optional_fields_omitted() {
162 let credential = Credential {
164 common_fields: CommonFields {
165 flags: FlagCollection::default(),
166 ledger_entry_type: LedgerEntryType::Credential,
167 index: Some(Cow::from(
168 "DD40031C6C21164E7673A47C35513D52A6B0F1349A873EE0D188D8994CD4D001",
169 )),
170 ledger_index: None,
171 },
172 subject: Cow::from("rALICE1111111111111111111111111111"),
173 issuer: Cow::from("rISABEL111111111111111111111111111"),
174 credential_type: Cow::from("4B5943"),
175 expiration: None,
176 uri: None,
177 subject_node: Some(Cow::from("0000000000000000")),
178 issuer_node: Cow::from("0000000000000000"),
179 previous_txn_id: Cow::from(
180 "3E8964D5A86B3CD6B9ECB33310D4E073D64C865A5B866200AD2B7E29F8326702",
181 ),
182 previous_txn_lgr_seq: 5,
183 };
184 let json = serde_json::to_string(&credential).unwrap();
185 assert!(!json.contains("Expiration"));
187 assert!(!json.contains("URI"));
188
189 let deserialized: Credential = serde_json::from_str(&json).unwrap();
190 assert_eq!(credential, deserialized);
191 assert!(deserialized.expiration.is_none());
192 assert!(deserialized.uri.is_none());
193 }
194
195 #[test]
196 fn test_self_issued_credential_allows_missing_subject_node() {
197 let json = r#"{
198 "LedgerEntryType":"Credential",
199 "Flags":65536,
200 "Subject":"rSELF11111111111111111111111111111",
201 "Issuer":"rSELF11111111111111111111111111111",
202 "CredentialType":"4B5943",
203 "IssuerNode":"0000000000000000",
204 "PreviousTxnID":"3E8964D5A86B3CD6B9ECB33310D4E073D64C865A5B866200AD2B7E29F8326702",
205 "PreviousTxnLgrSeq":10
206 }"#;
207
208 let credential: Credential = serde_json::from_str(json).unwrap();
209 assert!(credential.subject_node.is_none());
210 assert_eq!(credential.subject, credential.issuer);
211 }
212
213 #[test]
214 fn test_lsf_accepted_flag_value() {
215 assert_eq!(CredentialFlag::LsfAccepted as u32, 0x00010000);
217 }
218
219 #[test]
220 fn test_serde_with_accepted_flag() {
221 let mut flags = FlagCollection::default();
223 flags.0.push(CredentialFlag::LsfAccepted);
224 let credential = Credential {
225 common_fields: CommonFields {
226 flags,
227 ledger_entry_type: LedgerEntryType::Credential,
228 index: Some(Cow::from(
229 "DD40031C6C21164E7673A47C35513D52A6B0F1349A873EE0D188D8994CD4D001",
230 )),
231 ledger_index: None,
232 },
233 subject: Cow::from("rALICE1111111111111111111111111111"),
234 issuer: Cow::from("rISABEL111111111111111111111111111"),
235 credential_type: Cow::from("4B5943"),
236 expiration: None,
237 uri: None,
238 subject_node: Some(Cow::from("0000000000000000")),
239 issuer_node: Cow::from("0000000000000000"),
240 previous_txn_id: Cow::from(
241 "3E8964D5A86B3CD6B9ECB33310D4E073D64C865A5B866200AD2B7E29F8326702",
242 ),
243 previous_txn_lgr_seq: 10,
244 };
245 let json = serde_json::to_string(&credential).unwrap();
246 let deserialized: Credential = serde_json::from_str(&json).unwrap();
247 assert_eq!(credential, deserialized);
248 }
249}