1use alloc::borrow::Cow;
2use alloc::collections::BTreeSet;
3use alloc::vec::Vec;
4use serde::{Deserialize, Serialize};
5use serde_with::skip_serializing_none;
6
7use crate::core::addresscodec::is_valid_classic_address;
8use crate::models::amount::XRPAmount;
9use crate::models::exceptions::XRPLModelException;
10use crate::models::{
11 transactions::{Credential, Memo, Signer, Transaction, TransactionType},
12 Model, ValidateCurrencies,
13};
14use crate::models::{FlagCollection, NoFlags};
15
16use super::{CommonFields, CommonTransactionBuilder};
17
18#[skip_serializing_none]
29#[derive(
30 Debug,
31 Default,
32 Serialize,
33 Deserialize,
34 PartialEq,
35 Eq,
36 Clone,
37 xrpl_rust_macros::ValidateCurrencies,
38)]
39#[serde(rename_all = "PascalCase")]
40pub struct PermissionedDomainSet<'a> {
41 #[serde(flatten)]
46 pub common_fields: CommonFields<'a, NoFlags>,
47 #[serde(rename = "DomainID")]
50 pub domain_id: Option<Cow<'a, str>>,
51 pub accepted_credentials: Vec<Credential>,
54}
55
56impl<'a> Model for PermissionedDomainSet<'a> {
57 fn get_errors(&self) -> crate::models::XRPLModelResult<()> {
58 validate_accepted_credentials(&self.accepted_credentials)?;
59 if let Some(domain_id) = &self.domain_id {
60 validate_domain_id(domain_id.as_ref())?;
61 }
62 self.validate_currencies()
63 }
64}
65
66pub(crate) fn validate_accepted_credentials(
75 credentials: &[Credential],
76) -> crate::models::XRPLModelResult<()> {
77 if credentials.is_empty() {
78 return Err(XRPLModelException::MissingField(
79 "AcceptedCredentials".into(),
80 ));
81 }
82 if credentials.len() > 10 {
83 return Err(XRPLModelException::ValueTooLong {
84 field: "AcceptedCredentials".into(),
85 max: 10,
86 found: credentials.len(),
87 });
88 }
89 let mut seen: BTreeSet<(alloc::string::String, alloc::string::String)> = BTreeSet::new();
90 for credential in credentials {
91 validate_credential(credential)?;
92 let key = (
93 credential.issuer.clone(),
94 credential.credential_type.to_uppercase(),
95 );
96 if !seen.insert(key) {
97 return Err(XRPLModelException::InvalidValue {
98 field: "AcceptedCredentials".into(),
99 expected: "unique Issuer/CredentialType pairs".into(),
100 found: alloc::format!("{}/{}", credential.issuer, credential.credential_type),
101 });
102 }
103 }
104 Ok(())
105}
106
107pub(crate) fn validate_domain_id(domain_id: &str) -> crate::models::XRPLModelResult<()> {
110 if domain_id.len() != 64
111 || !domain_id.chars().all(|c| c.is_ascii_hexdigit())
112 || domain_id.chars().all(|c| c == '0')
113 {
114 return Err(XRPLModelException::InvalidValue {
115 field: "DomainID".into(),
116 expected: "non-zero 64-character hex string".into(),
117 found: domain_id.into(),
118 });
119 }
120 Ok(())
121}
122
123pub(crate) fn validate_credential(credential: &Credential) -> crate::models::XRPLModelResult<()> {
128 if credential.issuer.is_empty() {
129 return Err(XRPLModelException::MissingField("Credential.Issuer".into()));
130 }
131 if !is_valid_classic_address(&credential.issuer) {
132 return Err(XRPLModelException::InvalidValue {
133 field: "Credential.Issuer".into(),
134 expected: "valid classic XRPL address".into(),
135 found: credential.issuer.clone(),
136 });
137 }
138 let ct = &credential.credential_type;
139 if ct.is_empty() {
140 return Err(XRPLModelException::MissingField(
141 "Credential.CredentialType".into(),
142 ));
143 }
144 if ct.len() > 128 {
145 return Err(XRPLModelException::ValueTooLong {
146 field: "Credential.CredentialType".into(),
147 max: 128,
148 found: ct.len(),
149 });
150 }
151 if !ct.len().is_multiple_of(2) || !ct.chars().all(|c| c.is_ascii_hexdigit()) {
152 return Err(XRPLModelException::InvalidValue {
153 field: "Credential.CredentialType".into(),
154 expected: "even-length hex string (<=128 chars)".into(),
155 found: ct.clone(),
156 });
157 }
158 Ok(())
159}
160
161impl<'a> Transaction<'a, NoFlags> for PermissionedDomainSet<'a> {
162 fn get_transaction_type(&self) -> &TransactionType {
163 self.common_fields.get_transaction_type()
164 }
165
166 fn get_common_fields(&self) -> &CommonFields<'_, NoFlags> {
167 self.common_fields.get_common_fields()
168 }
169
170 fn get_mut_common_fields(&mut self) -> &mut CommonFields<'a, NoFlags> {
171 self.common_fields.get_mut_common_fields()
172 }
173}
174
175impl<'a> CommonTransactionBuilder<'a, NoFlags> for PermissionedDomainSet<'a> {
176 fn get_mut_common_fields(&mut self) -> &mut CommonFields<'a, NoFlags> {
177 &mut self.common_fields
178 }
179
180 fn into_self(self) -> Self {
181 self
182 }
183}
184
185impl<'a> PermissionedDomainSet<'a> {
186 pub fn new(
187 account: Cow<'a, str>,
188 account_txn_id: Option<Cow<'a, str>>,
189 fee: Option<XRPAmount<'a>>,
190 last_ledger_sequence: Option<u32>,
191 memos: Option<Vec<Memo>>,
192 sequence: Option<u32>,
193 signers: Option<Vec<Signer>>,
194 source_tag: Option<u32>,
195 ticket_sequence: Option<u32>,
196 domain_id: Option<Cow<'a, str>>,
197 accepted_credentials: Vec<Credential>,
198 ) -> Self {
199 Self {
200 common_fields: CommonFields {
201 account,
202 transaction_type: TransactionType::PermissionedDomainSet,
203 account_txn_id,
204 fee,
205 flags: FlagCollection::default(),
206 last_ledger_sequence,
207 memos,
208 sequence,
209 signers,
210 source_tag,
211 ticket_sequence,
212 ..Default::default()
213 },
214 domain_id,
215 accepted_credentials,
216 }
217 }
218
219 pub fn with_domain_id(mut self, domain_id: Cow<'a, str>) -> Self {
221 self.domain_id = Some(domain_id);
222 self
223 }
224
225 pub fn with_accepted_credentials(mut self, credentials: Vec<Credential>) -> Self {
227 self.accepted_credentials = credentials;
228 self
229 }
230
231 pub fn with_credential(mut self, credential: Credential) -> Self {
237 self.accepted_credentials.push(credential);
238 self
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245 use alloc::string::ToString;
246 use alloc::vec;
247
248 const TEST_ACCOUNT: &str = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh";
250
251 #[test]
252 fn test_serde() {
253 let txn = PermissionedDomainSet {
254 common_fields: CommonFields {
255 account: TEST_ACCOUNT.into(),
256 transaction_type: TransactionType::PermissionedDomainSet,
257 fee: Some("10".into()),
258 sequence: Some(1),
259 signing_pub_key: Some("".into()),
260 ..Default::default()
261 },
262 domain_id: None,
263 accepted_credentials: vec![Credential {
264 issuer: TEST_ACCOUNT.to_string(),
265 credential_type: "4B5943".to_string(), }],
267 };
268
269 let serialized = serde_json::to_string(&txn).unwrap();
270 let deserialized: PermissionedDomainSet = serde_json::from_str(&serialized).unwrap();
271 assert_eq!(txn, deserialized);
272 }
273
274 #[test]
275 fn test_serde_with_domain_id() {
276 let txn = PermissionedDomainSet {
277 common_fields: CommonFields {
278 account: TEST_ACCOUNT.into(),
279 transaction_type: TransactionType::PermissionedDomainSet,
280 fee: Some("10".into()),
281 sequence: Some(2),
282 signing_pub_key: Some("".into()),
283 ..Default::default()
284 },
285 domain_id: Some(
286 "A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2".into(),
287 ),
288 accepted_credentials: vec![Credential {
289 issuer: TEST_ACCOUNT.to_string(),
290 credential_type: "414D4C".to_string(), }],
292 };
293
294 let serialized = serde_json::to_string(&txn).unwrap();
295
296 assert!(serialized.contains("DomainID"));
298 assert!(
299 serialized.contains("A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2")
300 );
301
302 let deserialized: PermissionedDomainSet = serde_json::from_str(&serialized).unwrap();
303 assert_eq!(txn, deserialized);
304 }
305
306 #[test]
307 fn test_builder_pattern() {
308 let txn = PermissionedDomainSet {
309 common_fields: CommonFields {
310 account: TEST_ACCOUNT.into(),
311 transaction_type: TransactionType::PermissionedDomainSet,
312 ..Default::default()
313 },
314 ..Default::default()
315 }
316 .with_fee("12".into())
317 .with_sequence(100)
318 .with_last_ledger_sequence(596447)
319 .with_source_tag(42)
320 .with_credential(Credential {
321 issuer: TEST_ACCOUNT.to_string(),
322 credential_type: "4B5943".to_string(), });
324
325 assert_eq!(txn.common_fields.account, TEST_ACCOUNT);
326 assert_eq!(txn.common_fields.fee.as_ref().unwrap().0, "12");
327 assert_eq!(txn.common_fields.sequence, Some(100));
328 assert_eq!(txn.common_fields.last_ledger_sequence, Some(596447));
329 assert_eq!(txn.common_fields.source_tag, Some(42));
330 assert_eq!(txn.accepted_credentials.len(), 1);
331 assert!(txn.domain_id.is_none());
332 }
333
334 #[test]
335 fn test_default() {
336 let txn = PermissionedDomainSet {
337 common_fields: CommonFields {
338 account: TEST_ACCOUNT.into(),
339 transaction_type: TransactionType::PermissionedDomainSet,
340 ..Default::default()
341 },
342 ..Default::default()
343 };
344
345 assert_eq!(txn.common_fields.account, TEST_ACCOUNT);
346 assert_eq!(
347 txn.common_fields.transaction_type,
348 TransactionType::PermissionedDomainSet
349 );
350 assert!(txn.domain_id.is_none());
351 assert!(txn.accepted_credentials.is_empty());
352 assert!(txn.common_fields.fee.is_none());
353 assert!(txn.common_fields.sequence.is_none());
354 assert!(txn.get_errors().is_err());
356 }
357
358 #[test]
359 fn test_with_credentials() {
360 let txn = PermissionedDomainSet {
361 common_fields: CommonFields {
362 account: TEST_ACCOUNT.into(),
363 transaction_type: TransactionType::PermissionedDomainSet,
364 fee: Some("10".into()),
365 sequence: Some(5),
366 ..Default::default()
367 },
368 domain_id: None,
369 accepted_credentials: vec![
370 Credential {
371 issuer: TEST_ACCOUNT.to_string(),
372 credential_type: "4B5943".to_string(), },
374 Credential {
375 issuer: TEST_ACCOUNT.to_string(),
376 credential_type: "414D4C".to_string(), },
378 Credential {
379 issuer: TEST_ACCOUNT.to_string(),
380 credential_type: "41434352454449544544".to_string(), },
382 ],
383 };
384
385 assert_eq!(txn.accepted_credentials.len(), 3);
386 assert_eq!(txn.accepted_credentials[0].issuer, TEST_ACCOUNT.to_string());
387 assert_eq!(
388 txn.accepted_credentials[1].credential_type,
389 "414D4C".to_string()
390 );
391 assert_eq!(
392 txn.accepted_credentials[2].credential_type,
393 "41434352454449544544".to_string()
394 );
395 }
396
397 #[test]
398 fn test_update_domain() {
399 let domain_id =
400 "A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2".to_string();
401 let txn = PermissionedDomainSet {
402 common_fields: CommonFields {
403 account: TEST_ACCOUNT.into(),
404 transaction_type: TransactionType::PermissionedDomainSet,
405 fee: Some("10".into()),
406 sequence: Some(10),
407 ..Default::default()
408 },
409 domain_id: Some(domain_id.clone().into()),
410 accepted_credentials: vec![Credential {
411 issuer: "rNewIssuer".to_string(),
412 credential_type: "5645524946494544".to_string(), }],
414 };
415
416 assert_eq!(txn.domain_id, Some(domain_id.into()));
417 assert_eq!(txn.accepted_credentials.len(), 1);
418 }
419
420 #[test]
421 fn test_create_domain() {
422 let txn = PermissionedDomainSet {
423 common_fields: CommonFields {
424 account: TEST_ACCOUNT.into(),
425 transaction_type: TransactionType::PermissionedDomainSet,
426 fee: Some("10".into()),
427 sequence: Some(1),
428 ..Default::default()
429 },
430 domain_id: None,
431 accepted_credentials: vec![Credential {
432 issuer: TEST_ACCOUNT.to_string(),
433 credential_type: "4B5943".to_string(), }],
435 };
436
437 assert!(txn.domain_id.is_none());
439 assert_eq!(txn.accepted_credentials.len(), 1);
440 }
441
442 #[test]
443 fn test_new_constructor() {
444 let txn = PermissionedDomainSet::new(
445 TEST_ACCOUNT.into(),
446 None,
447 Some("12".into()),
448 Some(596447),
449 None,
450 Some(1),
451 None,
452 None,
453 None,
454 None,
455 vec![Credential {
456 issuer: TEST_ACCOUNT.to_string(),
457 credential_type: "4B5943".to_string(), }],
459 );
460
461 assert_eq!(txn.common_fields.account, TEST_ACCOUNT);
462 assert_eq!(
463 txn.common_fields.transaction_type,
464 TransactionType::PermissionedDomainSet
465 );
466 assert_eq!(txn.common_fields.fee.as_ref().unwrap().0, "12");
467 assert_eq!(txn.common_fields.sequence, Some(1));
468 assert_eq!(txn.common_fields.last_ledger_sequence, Some(596447));
469 assert!(txn.domain_id.is_none());
470 assert_eq!(txn.accepted_credentials.len(), 1);
471 }
472
473 #[test]
474 fn test_with_domain_id_builder() {
475 let txn = PermissionedDomainSet {
476 common_fields: CommonFields {
477 account: TEST_ACCOUNT.into(),
478 transaction_type: TransactionType::PermissionedDomainSet,
479 ..Default::default()
480 },
481 ..Default::default()
482 }
483 .with_domain_id("AABB0011".into())
484 .with_accepted_credentials(vec![Credential {
485 issuer: TEST_ACCOUNT.to_string(),
486 credential_type: "4B5943".to_string(), }]);
488
489 assert_eq!(txn.domain_id, Some("AABB0011".into()));
490 assert_eq!(txn.accepted_credentials.len(), 1);
491 }
492
493 #[test]
494 fn test_with_memo() {
495 let txn = PermissionedDomainSet {
496 common_fields: CommonFields {
497 account: TEST_ACCOUNT.into(),
498 transaction_type: TransactionType::PermissionedDomainSet,
499 ..Default::default()
500 },
501 ..Default::default()
502 }
503 .with_fee("10".into())
504 .with_sequence(1)
505 .with_memo(Memo {
506 memo_data: Some("creating domain".into()),
507 memo_format: None,
508 memo_type: Some("text".into()),
509 })
510 .with_credential(Credential {
511 issuer: TEST_ACCOUNT.to_string(),
512 credential_type: "4B5943".to_string(), });
514
515 assert_eq!(txn.common_fields.memos.as_ref().unwrap().len(), 1);
516 assert_eq!(txn.accepted_credentials.len(), 1);
517 }
518
519 #[test]
520 fn test_empty_credentials_rejected() {
521 let txn = PermissionedDomainSet {
523 common_fields: CommonFields {
524 account: TEST_ACCOUNT.into(),
525 transaction_type: TransactionType::PermissionedDomainSet,
526 fee: Some("10".into()),
527 sequence: Some(1),
528 ..Default::default()
529 },
530 domain_id: Some("AABB0011".into()),
531 accepted_credentials: vec![],
532 };
533
534 let result = txn.get_errors();
535 assert!(result.is_err());
536 assert_eq!(
537 result.unwrap_err(),
538 XRPLModelException::MissingField("AcceptedCredentials".into())
539 );
540 }
541
542 #[test]
543 fn test_too_many_credentials_rejected() {
544 let credentials: Vec<Credential> = (0..11)
546 .map(|_| Credential {
547 issuer: TEST_ACCOUNT.to_string(),
548 credential_type: "4B5943".to_string(),
549 })
550 .collect();
551 let txn = PermissionedDomainSet {
552 common_fields: CommonFields {
553 account: TEST_ACCOUNT.into(),
554 transaction_type: TransactionType::PermissionedDomainSet,
555 ..Default::default()
556 },
557 domain_id: None,
558 accepted_credentials: credentials,
559 };
560
561 let result = txn.get_errors();
562 assert!(result.is_err());
563 assert!(matches!(
564 result.unwrap_err(),
565 XRPLModelException::ValueTooLong {
566 max: 10,
567 found: 11,
568 ..
569 }
570 ));
571 }
572
573 #[test]
574 fn test_non_hex_credential_type_rejected() {
575 let txn = PermissionedDomainSet {
577 common_fields: CommonFields {
578 account: TEST_ACCOUNT.into(),
579 transaction_type: TransactionType::PermissionedDomainSet,
580 ..Default::default()
581 },
582 domain_id: None,
583 accepted_credentials: vec![Credential {
584 issuer: TEST_ACCOUNT.to_string(),
585 credential_type: "KYC".to_string(), }],
587 };
588 let result = txn.get_errors();
589 assert!(result.is_err());
590 assert!(matches!(
591 result.unwrap_err(),
592 XRPLModelException::InvalidValue { .. }
593 ));
594 }
595
596 #[test]
597 fn test_credential_type_64_bytes_accepted() {
598 let credential = Credential {
600 issuer: TEST_ACCOUNT.to_string(),
601 credential_type: "A".repeat(128),
602 };
603
604 assert!(validate_credential(&credential).is_ok());
605 }
606
607 #[test]
608 fn test_credential_type_over_64_bytes_rejected() {
609 let too_long = "A".repeat(130);
610 let txn = PermissionedDomainSet {
611 common_fields: CommonFields {
612 account: TEST_ACCOUNT.into(),
613 transaction_type: TransactionType::PermissionedDomainSet,
614 ..Default::default()
615 },
616 domain_id: None,
617 accepted_credentials: vec![Credential {
618 issuer: TEST_ACCOUNT.to_string(),
619 credential_type: too_long,
620 }],
621 };
622 let result = txn.get_errors();
623 assert!(result.is_err());
624 assert!(matches!(
625 result.unwrap_err(),
626 XRPLModelException::ValueTooLong { max: 128, .. }
627 ));
628 }
629
630 #[test]
631 fn test_duplicate_credentials_rejected() {
632 let duplicate = Credential {
633 issuer: TEST_ACCOUNT.to_string(),
634 credential_type: "4B5943".to_string(),
635 };
636 let txn = PermissionedDomainSet {
637 common_fields: CommonFields {
638 account: TEST_ACCOUNT.into(),
639 transaction_type: TransactionType::PermissionedDomainSet,
640 ..Default::default()
641 },
642 domain_id: None,
643 accepted_credentials: vec![duplicate.clone(), duplicate],
644 };
645
646 assert!(matches!(
647 txn.get_errors(),
648 Err(XRPLModelException::InvalidValue { .. })
649 ));
650 }
651
652 #[test]
653 fn test_set_all_zero_domain_id_rejected() {
654 let txn = PermissionedDomainSet {
655 common_fields: CommonFields {
656 account: TEST_ACCOUNT.into(),
657 transaction_type: TransactionType::PermissionedDomainSet,
658 ..Default::default()
659 },
660 domain_id: Some("0".repeat(64).into()),
661 accepted_credentials: vec![Credential {
662 issuer: TEST_ACCOUNT.to_string(),
663 credential_type: "4B5943".to_string(),
664 }],
665 };
666
667 assert!(matches!(
668 txn.get_errors(),
669 Err(XRPLModelException::InvalidValue { .. })
670 ));
671 }
672
673 #[test]
674 fn test_duplicate_credentials_case_insensitive_rejected() {
675 let txn = PermissionedDomainSet {
678 common_fields: CommonFields {
679 account: TEST_ACCOUNT.into(),
680 transaction_type: TransactionType::PermissionedDomainSet,
681 ..Default::default()
682 },
683 domain_id: None,
684 accepted_credentials: vec![
685 Credential {
686 issuer: TEST_ACCOUNT.to_string(),
687 credential_type: "4b5943".to_string(),
688 },
689 Credential {
690 issuer: TEST_ACCOUNT.to_string(),
691 credential_type: "4B5943".to_string(),
692 },
693 ],
694 };
695
696 assert!(matches!(
697 txn.get_errors(),
698 Err(XRPLModelException::InvalidValue { .. })
699 ));
700 }
701
702 #[test]
703 fn test_set_wrong_length_domain_id_rejected() {
704 let txn = PermissionedDomainSet {
705 common_fields: CommonFields {
706 account: TEST_ACCOUNT.into(),
707 transaction_type: TransactionType::PermissionedDomainSet,
708 ..Default::default()
709 },
710 domain_id: Some("ABCD".into()), accepted_credentials: vec![Credential {
712 issuer: TEST_ACCOUNT.to_string(),
713 credential_type: "4B5943".to_string(),
714 }],
715 };
716
717 assert!(matches!(
718 txn.get_errors(),
719 Err(XRPLModelException::InvalidValue { .. })
720 ));
721 }
722
723 #[test]
724 fn test_set_non_hex_domain_id_rejected() {
725 let txn = PermissionedDomainSet {
726 common_fields: CommonFields {
727 account: TEST_ACCOUNT.into(),
728 transaction_type: TransactionType::PermissionedDomainSet,
729 ..Default::default()
730 },
731 domain_id: Some("G".repeat(64).into()), accepted_credentials: vec![Credential {
733 issuer: TEST_ACCOUNT.to_string(),
734 credential_type: "4B5943".to_string(),
735 }],
736 };
737
738 assert!(matches!(
739 txn.get_errors(),
740 Err(XRPLModelException::InvalidValue { .. })
741 ));
742 }
743
744 #[test]
745 fn test_ticket_sequence() {
746 let txn = PermissionedDomainSet {
747 common_fields: CommonFields {
748 account: TEST_ACCOUNT.into(),
749 transaction_type: TransactionType::PermissionedDomainSet,
750 ..Default::default()
751 },
752 ..Default::default()
753 }
754 .with_ticket_sequence(42)
755 .with_fee("10".into())
756 .with_credential(Credential {
757 issuer: TEST_ACCOUNT.to_string(),
758 credential_type: "4B5943".to_string(), });
760
761 assert_eq!(txn.common_fields.ticket_sequence, Some(42));
762 assert!(txn.common_fields.sequence.is_none());
763 }
764
765 #[test]
766 fn test_credential_empty_issuer_rejected() {
767 let txn = PermissionedDomainSet {
768 common_fields: CommonFields {
769 account: TEST_ACCOUNT.into(),
770 transaction_type: TransactionType::PermissionedDomainSet,
771 ..Default::default()
772 },
773 domain_id: None,
774 accepted_credentials: vec![Credential {
775 issuer: "".to_string(),
776 credential_type: "4B5943".to_string(), }],
778 };
779
780 let result = txn.get_errors();
781 assert!(result.is_err());
782 assert_eq!(
783 result.unwrap_err(),
784 XRPLModelException::MissingField("Credential.Issuer".into())
785 );
786 }
787
788 #[test]
789 fn test_credential_empty_credential_type_rejected() {
790 let txn = PermissionedDomainSet {
791 common_fields: CommonFields {
792 account: TEST_ACCOUNT.into(),
793 transaction_type: TransactionType::PermissionedDomainSet,
794 ..Default::default()
795 },
796 domain_id: None,
797 accepted_credentials: vec![Credential {
798 issuer: TEST_ACCOUNT.to_string(),
799 credential_type: "".to_string(),
800 }],
801 };
802
803 let result = txn.get_errors();
804 assert!(result.is_err());
805 assert_eq!(
806 result.unwrap_err(),
807 XRPLModelException::MissingField("Credential.CredentialType".into())
808 );
809 }
810}