1use alloc::borrow::Cow;
2use alloc::vec::Vec;
3use serde::{Deserialize, Serialize};
4use serde_with::skip_serializing_none;
5
6use crate::models::amount::XRPAmount;
7use crate::models::transactions::CommonFields;
8use crate::models::{
9 transactions::{Memo, Signer, Transaction, TransactionType},
10 Model, XRPLModelException, XRPLModelResult,
11};
12use crate::models::{FlagCollection, NoFlags};
13
14use super::CommonTransactionBuilder;
15
16#[skip_serializing_none]
21#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq, Clone)]
22#[serde(rename_all = "PascalCase")]
23pub struct CredentialDelete<'a> {
24 #[serde(flatten)]
26 pub common_fields: CommonFields<'a, NoFlags>,
27 pub subject: Option<Cow<'a, str>>,
30 pub issuer: Option<Cow<'a, str>>,
33 pub credential_type: Cow<'a, str>,
35}
36
37impl<'a> Model for CredentialDelete<'a> {
38 fn get_errors(&self) -> XRPLModelResult<()> {
39 self._get_subject_or_issuer_error()?;
40 self._get_credential_type_error()
41 }
42}
43
44impl<'a> Transaction<'a, NoFlags> for CredentialDelete<'a> {
45 fn get_transaction_type(&self) -> &TransactionType {
46 self.common_fields.get_transaction_type()
47 }
48
49 fn get_common_fields(&self) -> &CommonFields<'_, NoFlags> {
50 self.common_fields.get_common_fields()
51 }
52
53 fn get_mut_common_fields(&mut self) -> &mut CommonFields<'a, NoFlags> {
54 self.common_fields.get_mut_common_fields()
55 }
56}
57
58impl<'a> CommonTransactionBuilder<'a, NoFlags> for CredentialDelete<'a> {
59 fn get_mut_common_fields(&mut self) -> &mut CommonFields<'a, NoFlags> {
60 &mut self.common_fields
61 }
62
63 fn into_self(self) -> Self {
64 self
65 }
66}
67
68impl<'a> CredentialDelete<'a> {
69 #[allow(clippy::too_many_arguments)]
70 pub fn new(
71 account: Cow<'a, str>,
72 account_txn_id: Option<Cow<'a, str>>,
73 fee: Option<XRPAmount<'a>>,
74 last_ledger_sequence: Option<u32>,
75 memos: Option<Vec<Memo>>,
76 sequence: Option<u32>,
77 signers: Option<Vec<Signer>>,
78 source_tag: Option<u32>,
79 ticket_sequence: Option<u32>,
80 subject: Option<Cow<'a, str>>,
81 issuer: Option<Cow<'a, str>>,
82 credential_type: Cow<'a, str>,
83 ) -> Self {
84 Self {
85 common_fields: CommonFields::new(
86 account,
87 TransactionType::CredentialDelete,
88 account_txn_id,
89 fee,
90 Some(FlagCollection::default()),
91 last_ledger_sequence,
92 memos,
93 None,
94 sequence,
95 signers,
96 None,
97 source_tag,
98 ticket_sequence,
99 None,
100 ),
101 subject,
102 issuer,
103 credential_type,
104 }
105 }
106}
107
108impl<'a> CredentialDeleteError for CredentialDelete<'a> {
109 fn _get_subject_or_issuer_error(&self) -> XRPLModelResult<()> {
110 if self.subject.is_none() && self.issuer.is_none() {
111 return Err(XRPLModelException::ExpectedOneOf(&["subject", "issuer"]));
112 }
113 Ok(())
120 }
121
122 fn _get_credential_type_error(&self) -> XRPLModelResult<()> {
123 super::validate_credential_type(&self.credential_type)
124 }
125}
126
127pub trait CredentialDeleteError {
128 fn _get_subject_or_issuer_error(&self) -> XRPLModelResult<()>;
129 fn _get_credential_type_error(&self) -> XRPLModelResult<()>;
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use crate::models::{Model, XRPLModelException};
136 use alloc::borrow::Cow;
137 use alloc::format;
138 use proptest::prelude::*;
139
140 #[test]
141 fn test_requires_subject_or_issuer() {
142 let tx = CredentialDelete {
143 common_fields: CommonFields {
144 account: "rSubmitter111111111111111111111111".into(),
145 transaction_type: TransactionType::CredentialDelete,
146 ..Default::default()
147 },
148 subject: None,
149 issuer: None,
150 credential_type: "4B5943".into(),
151 };
152 assert!(tx.get_errors().is_err());
153 }
154
155 #[test]
156 fn test_valid_with_subject() {
157 let tx = CredentialDelete {
158 common_fields: CommonFields {
159 account: "rSubject11111111111111111111111111".into(),
160 transaction_type: TransactionType::CredentialDelete,
161 ..Default::default()
162 },
163 subject: Some("rSubject11111111111111111111111111".into()),
164 issuer: None,
165 credential_type: "4B5943".into(),
166 };
167 assert!(tx.get_errors().is_ok());
168 }
169
170 #[test]
171 fn test_both_provided_allows_third_party_submitter() {
172 let tx = CredentialDelete {
176 common_fields: CommonFields {
177 account: "rSubmitter111111111111111111111111".into(),
178 transaction_type: TransactionType::CredentialDelete,
179 ..Default::default()
180 },
181 subject: Some("rSubject11111111111111111111111111".into()),
182 issuer: Some("rIssuer111111111111111111111111111".into()),
183 credential_type: "4B5943".into(),
184 };
185 assert!(tx.get_errors().is_ok());
186 }
187
188 #[test]
189 fn test_valid_with_issuer_only() {
190 let tx = CredentialDelete {
192 common_fields: CommonFields {
193 account: "rIssuer111111111111111111111111111".into(),
194 transaction_type: TransactionType::CredentialDelete,
195 ..Default::default()
196 },
197 subject: None,
198 issuer: Some("rIssuer111111111111111111111111111".into()),
199 credential_type: "4B5943".into(),
200 };
201 assert!(tx.get_errors().is_ok());
202 }
203
204 #[test]
205 fn test_valid_subject_only_account_is_implicit_issuer() {
206 let tx = CredentialDelete {
209 common_fields: CommonFields {
210 account: "rSubmitter111111111111111111111111".into(),
211 transaction_type: TransactionType::CredentialDelete,
212 ..Default::default()
213 },
214 subject: Some("rSubject11111111111111111111111111".into()),
215 issuer: None,
216 credential_type: "4B5943".into(),
217 };
218 assert!(tx.get_errors().is_ok());
219 }
220
221 #[test]
222 fn test_valid_issuer_only_account_is_implicit_subject() {
223 let tx = CredentialDelete {
225 common_fields: CommonFields {
226 account: "rSubmitter111111111111111111111111".into(),
227 transaction_type: TransactionType::CredentialDelete,
228 ..Default::default()
229 },
230 subject: None,
231 issuer: Some("rIssuer111111111111111111111111111".into()),
232 credential_type: "4B5943".into(),
233 };
234 assert!(tx.get_errors().is_ok());
235 }
236
237 #[test]
238 fn test_valid_both_provided_account_matches_subject() {
239 let tx = CredentialDelete {
240 common_fields: CommonFields {
241 account: "rSubject11111111111111111111111111".into(),
242 transaction_type: TransactionType::CredentialDelete,
243 ..Default::default()
244 },
245 subject: Some("rSubject11111111111111111111111111".into()),
246 issuer: Some("rIssuer111111111111111111111111111".into()),
247 credential_type: "4B5943".into(),
248 };
249 assert!(tx.get_errors().is_ok());
250 }
251
252 #[test]
253 fn test_valid_both_provided_account_matches_issuer() {
254 let tx = CredentialDelete {
255 common_fields: CommonFields {
256 account: "rIssuer111111111111111111111111111".into(),
257 transaction_type: TransactionType::CredentialDelete,
258 ..Default::default()
259 },
260 subject: Some("rSubject11111111111111111111111111".into()),
261 issuer: Some("rIssuer111111111111111111111111111".into()),
262 credential_type: "4B5943".into(),
263 };
264 assert!(tx.get_errors().is_ok());
265 }
266
267 #[test]
268 fn test_serde() {
269 let default_txn = CredentialDelete {
270 common_fields: CommonFields {
271 account: "rSubmitter111111111111111111111111".into(),
272 transaction_type: TransactionType::CredentialDelete,
273 fee: Some("10".into()),
274 sequence: Some(9),
275 signing_pub_key: Some("".into()),
276 ..Default::default()
277 },
278 subject: Some("rSubject11111111111111111111111111".into()),
279 issuer: None,
280 credential_type: "4B5943".into(),
281 };
282
283 let default_json_str = r#"{"Account":"rSubmitter111111111111111111111111","TransactionType":"CredentialDelete","Fee":"10","Flags":0,"Sequence":9,"SigningPubKey":"","Subject":"rSubject11111111111111111111111111","CredentialType":"4B5943"}"#;
284
285 let default_json_value = serde_json::to_value(default_json_str).unwrap();
286 let serialized_string = serde_json::to_string(&default_txn).unwrap();
287 let serialized_value = serde_json::to_value(&serialized_string).unwrap();
288 assert_eq!(serialized_value, default_json_value);
289
290 let deserialized: CredentialDelete = serde_json::from_str(default_json_str).unwrap();
291 assert_eq!(default_txn, deserialized);
292 }
293
294 #[test]
295 fn test_credential_type_empty_error() {
296 let tx = CredentialDelete {
297 common_fields: CommonFields {
298 account: "rSubject11111111111111111111111111".into(),
299 transaction_type: TransactionType::CredentialDelete,
300 ..Default::default()
301 },
302 subject: Some("rSubject11111111111111111111111111".into()),
303 issuer: None,
304 credential_type: Cow::from(""),
305 };
306 assert_eq!(
307 tx.get_errors().unwrap_err(),
308 XRPLModelException::ValueTooShort {
309 field: "credential_type".into(),
310 min: 1,
311 found: 0,
312 }
313 );
314 }
315
316 #[test]
317 fn test_credential_type_non_hex_error() {
318 let tx = CredentialDelete {
319 common_fields: CommonFields {
320 account: "rSubject11111111111111111111111111".into(),
321 transaction_type: TransactionType::CredentialDelete,
322 ..Default::default()
323 },
324 subject: Some("rSubject11111111111111111111111111".into()),
325 issuer: None,
326 credential_type: "NOTHEX".into(),
327 };
328 assert_eq!(
329 tx.get_errors().unwrap_err(),
330 XRPLModelException::InvalidValueFormat {
331 field: "credential_type".into(),
332 format: "hexadecimal".into(),
333 found: "NOTHEX".into(),
334 }
335 );
336 }
337
338 #[test]
339 fn test_credential_type_exceeds_128_hex_chars_error() {
340 let too_long: Cow<'_, str> = Cow::from("A".repeat(129));
341 let tx = CredentialDelete {
342 common_fields: CommonFields {
343 account: "rSubject11111111111111111111111111".into(),
344 transaction_type: TransactionType::CredentialDelete,
345 ..Default::default()
346 },
347 subject: Some("rSubject11111111111111111111111111".into()),
348 issuer: None,
349 credential_type: too_long,
350 };
351 assert_eq!(
352 tx.get_errors().unwrap_err(),
353 XRPLModelException::ValueTooLong {
354 field: "credential_type".into(),
355 max: 128,
356 found: 129,
357 }
358 );
359 }
360
361 #[test]
362 fn test_credential_type_at_max_128_ok() {
363 let max_hex: Cow<'_, str> = Cow::from("A".repeat(128));
364 let tx = CredentialDelete {
365 common_fields: CommonFields {
366 account: "rSubject11111111111111111111111111".into(),
367 transaction_type: TransactionType::CredentialDelete,
368 ..Default::default()
369 },
370 subject: Some("rSubject11111111111111111111111111".into()),
371 issuer: None,
372 credential_type: max_hex,
373 };
374 assert!(tx.get_errors().is_ok());
375 }
376
377 #[test]
378 fn test_self_issued_credential_delete_both_subject_and_issuer_equal_account() {
379 let tx = CredentialDelete {
381 common_fields: CommonFields {
382 account: "rSelfIssuer1111111111111111111111".into(),
383 transaction_type: TransactionType::CredentialDelete,
384 ..Default::default()
385 },
386 subject: Some("rSelfIssuer1111111111111111111111".into()),
387 issuer: Some("rSelfIssuer1111111111111111111111".into()),
388 credential_type: "4B5943".into(),
389 };
390 assert!(tx.get_errors().is_ok());
391 }
392
393 const ACCOUNTS: [&str; 3] = [
394 "rU4EE1FskCPJw5QkLx1iGgdWiJa6HeqYyb",
395 "rEhxGqkqPPSxQ3P25J66ft5TwpzV14k2de",
396 "rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH",
397 ];
398
399 proptest! {
400 #![proptest_config(ProptestConfig::with_cases(200))]
401
402 #[test]
403 fn prop_subject_only_any_submitter_valid(acct_idx in 0_usize..3) {
404 let tx = CredentialDelete {
405 common_fields: CommonFields {
406 account: ACCOUNTS[acct_idx].into(),
407 transaction_type: TransactionType::CredentialDelete,
408 ..Default::default()
409 },
410 subject: Some(ACCOUNTS[1].into()),
411 issuer: None,
412 credential_type: "4B5943".into(),
413 };
414 prop_assert!(tx.get_errors().is_ok());
415 }
416
417 #[test]
418 fn prop_issuer_only_any_submitter_valid(acct_idx in 0_usize..3) {
419 let tx = CredentialDelete {
420 common_fields: CommonFields {
421 account: ACCOUNTS[acct_idx].into(),
422 transaction_type: TransactionType::CredentialDelete,
423 ..Default::default()
424 },
425 subject: None,
426 issuer: Some(ACCOUNTS[0].into()),
427 credential_type: "4B5943".into(),
428 };
429 prop_assert!(tx.get_errors().is_ok());
430 }
431
432 #[test]
433 fn prop_both_any_submitter_valid(acct_idx in 0_usize..3) {
434 let tx = CredentialDelete {
435 common_fields: CommonFields {
436 account: ACCOUNTS[acct_idx].into(),
437 transaction_type: TransactionType::CredentialDelete,
438 ..Default::default()
439 },
440 subject: Some(ACCOUNTS[0].into()),
441 issuer: Some(ACCOUNTS[1].into()),
442 credential_type: "4B5943".into(),
443 };
444 prop_assert!(tx.get_errors().is_ok());
445 }
446
447 #[test]
448 fn prop_serde_roundtrip(
449 ct in "([0-9A-F]{2}){1,64}", has_subject in proptest::bool::ANY,
451 has_issuer in proptest::bool::ANY,
452 ) {
453 let subject = if has_subject || !has_issuer { Some(Cow::Borrowed(ACCOUNTS[0])) } else { None };
454 let issuer = if has_issuer { Some(Cow::Borrowed(ACCOUNTS[1])) } else { None };
455 let acct = if subject.is_some() { ACCOUNTS[0] } else { ACCOUNTS[1] };
456 let tx = CredentialDelete {
457 common_fields: CommonFields {
458 account: acct.into(),
459 transaction_type: TransactionType::CredentialDelete,
460 fee: Some("10".into()),
461 sequence: Some(7),
462 signing_pub_key: Some(Cow::Borrowed("")),
463 ..Default::default()
464 },
465 subject,
466 issuer,
467 credential_type: Cow::Owned(ct),
468 };
469 prop_assert!(tx.get_errors().is_ok());
470 let json = serde_json::to_string(&tx)
471 .map_err(|e| TestCaseError::fail(format!("serialize: {e}")))?;
472 let rt: CredentialDelete = serde_json::from_str(&json)
473 .map_err(|e| TestCaseError::fail(format!("deserialize: {e}")))?;
474 prop_assert_eq!(&tx, &rt);
475 }
476 }
477}