Skip to main content

xrpl/models/transactions/
credential_accept.rs

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, XRPLModelResult,
11};
12use crate::models::{FlagCollection, NoFlags};
13
14use super::CommonTransactionBuilder;
15
16/// A CredentialAccept transaction accepts a credential issued to the sender.
17///
18/// See CredentialAccept:
19/// `<https://github.com/XRPLF/XRPL-Standards/tree/master/XLS-0070-credentials>`
20#[skip_serializing_none]
21#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq, Clone)]
22#[serde(rename_all = "PascalCase")]
23pub struct CredentialAccept<'a> {
24    /// The base fields for all transaction models.
25    #[serde(flatten)]
26    pub common_fields: CommonFields<'a, NoFlags>,
27    /// The issuer of the credential.
28    pub issuer: Cow<'a, str>,
29    /// A hex-encoded value identifying the credential type from this issuer.
30    pub credential_type: Cow<'a, str>,
31}
32
33impl<'a> Model for CredentialAccept<'a> {
34    fn get_errors(&self) -> XRPLModelResult<()> {
35        self._get_credential_type_error()
36    }
37}
38
39impl<'a> Transaction<'a, NoFlags> for CredentialAccept<'a> {
40    fn get_transaction_type(&self) -> &TransactionType {
41        self.common_fields.get_transaction_type()
42    }
43
44    fn get_common_fields(&self) -> &CommonFields<'_, NoFlags> {
45        self.common_fields.get_common_fields()
46    }
47
48    fn get_mut_common_fields(&mut self) -> &mut CommonFields<'a, NoFlags> {
49        self.common_fields.get_mut_common_fields()
50    }
51}
52
53impl<'a> CommonTransactionBuilder<'a, NoFlags> for CredentialAccept<'a> {
54    fn get_mut_common_fields(&mut self) -> &mut CommonFields<'a, NoFlags> {
55        &mut self.common_fields
56    }
57
58    fn into_self(self) -> Self {
59        self
60    }
61}
62
63impl<'a> CredentialAccept<'a> {
64    #[allow(clippy::too_many_arguments)]
65    pub fn new(
66        account: Cow<'a, str>,
67        account_txn_id: Option<Cow<'a, str>>,
68        fee: Option<XRPAmount<'a>>,
69        last_ledger_sequence: Option<u32>,
70        memos: Option<Vec<Memo>>,
71        sequence: Option<u32>,
72        signers: Option<Vec<Signer>>,
73        source_tag: Option<u32>,
74        ticket_sequence: Option<u32>,
75        issuer: Cow<'a, str>,
76        credential_type: Cow<'a, str>,
77    ) -> Self {
78        Self {
79            common_fields: CommonFields::new(
80                account,
81                TransactionType::CredentialAccept,
82                account_txn_id,
83                fee,
84                Some(FlagCollection::default()),
85                last_ledger_sequence,
86                memos,
87                None,
88                sequence,
89                signers,
90                None,
91                source_tag,
92                ticket_sequence,
93                None,
94            ),
95            issuer,
96            credential_type,
97        }
98    }
99}
100
101impl<'a> CredentialAcceptError for CredentialAccept<'a> {
102    fn _get_credential_type_error(&self) -> XRPLModelResult<()> {
103        super::validate_credential_type(&self.credential_type)
104    }
105}
106
107pub trait CredentialAcceptError {
108    fn _get_credential_type_error(&self) -> XRPLModelResult<()>;
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    use crate::models::{Model, XRPLModelException};
115    use alloc::borrow::Cow;
116    use alloc::format;
117    use proptest::prelude::*;
118
119    #[test]
120    fn test_serde() {
121        let default_txn = CredentialAccept {
122            common_fields: CommonFields {
123                account: "rSubject11111111111111111111111111".into(),
124                transaction_type: TransactionType::CredentialAccept,
125                fee: Some("10".into()),
126                sequence: Some(8),
127                signing_pub_key: Some("".into()),
128                ..Default::default()
129            },
130            issuer: "rIssuer111111111111111111111111111".into(),
131            credential_type: "4B5943".into(),
132        };
133
134        let default_json_str = r#"{"Account":"rSubject11111111111111111111111111","TransactionType":"CredentialAccept","Fee":"10","Flags":0,"Sequence":8,"SigningPubKey":"","Issuer":"rIssuer111111111111111111111111111","CredentialType":"4B5943"}"#;
135
136        let default_json_value = serde_json::to_value(default_json_str).unwrap();
137        let serialized_string = serde_json::to_string(&default_txn).unwrap();
138        let serialized_value = serde_json::to_value(&serialized_string).unwrap();
139        assert_eq!(serialized_value, default_json_value);
140
141        let deserialized: CredentialAccept = serde_json::from_str(default_json_str).unwrap();
142        assert_eq!(default_txn, deserialized);
143    }
144
145    #[test]
146    fn test_credential_type_empty_error() {
147        let tx = CredentialAccept {
148            common_fields: CommonFields {
149                account: "rSubject11111111111111111111111111".into(),
150                transaction_type: TransactionType::CredentialAccept,
151                ..Default::default()
152            },
153            issuer: "rIssuer111111111111111111111111111".into(),
154            credential_type: Cow::from(""),
155        };
156        assert_eq!(
157            tx.get_errors().unwrap_err(),
158            XRPLModelException::ValueTooShort {
159                field: "credential_type".into(),
160                min: 1,
161                found: 0,
162            }
163        );
164    }
165
166    #[test]
167    fn test_credential_type_too_long_error() {
168        // 129 hex chars exceeds the 128 limit
169        let too_long: Cow<'_, str> = Cow::from("A".repeat(129));
170        let tx = CredentialAccept {
171            common_fields: CommonFields {
172                account: "rSubject11111111111111111111111111".into(),
173                transaction_type: TransactionType::CredentialAccept,
174                ..Default::default()
175            },
176            issuer: "rIssuer111111111111111111111111111".into(),
177            credential_type: too_long,
178        };
179        assert_eq!(
180            tx.get_errors().unwrap_err(),
181            XRPLModelException::ValueTooLong {
182                field: "credential_type".into(),
183                max: 128,
184                found: 129,
185            }
186        );
187    }
188
189    #[test]
190    fn test_credential_type_non_hex_error() {
191        let tx = CredentialAccept {
192            common_fields: CommonFields {
193                account: "rSubject11111111111111111111111111".into(),
194                transaction_type: TransactionType::CredentialAccept,
195                ..Default::default()
196            },
197            issuer: "rIssuer111111111111111111111111111".into(),
198            credential_type: "NOTHEX".into(),
199        };
200        assert_eq!(
201            tx.get_errors().unwrap_err(),
202            XRPLModelException::InvalidValueFormat {
203                field: "credential_type".into(),
204                format: "hexadecimal".into(),
205                found: "NOTHEX".into(),
206            }
207        );
208    }
209
210    /// Guards the upper length boundary: 128 hex chars (= 64 bytes) must still pass.
211    #[test]
212    fn test_credential_type_at_max_128_ok() {
213        let max_hex: Cow<'_, str> = Cow::from("A".repeat(128));
214        let tx = CredentialAccept {
215            common_fields: CommonFields {
216                account: "rSubject11111111111111111111111111".into(),
217                transaction_type: TransactionType::CredentialAccept,
218                ..Default::default()
219            },
220            issuer: "rIssuer111111111111111111111111111".into(),
221            credential_type: max_hex,
222        };
223        assert!(tx.get_errors().is_ok());
224    }
225
226    /// Guards the minimal-valid path: a short, well-formed hex credential_type passes.
227    #[test]
228    fn test_valid_minimal_accept() {
229        let tx = CredentialAccept {
230            common_fields: CommonFields {
231                account: "rSubject11111111111111111111111111".into(),
232                transaction_type: TransactionType::CredentialAccept,
233                ..Default::default()
234            },
235            issuer: "rIssuer111111111111111111111111111".into(),
236            credential_type: "4B5943".into(),
237        };
238        assert!(tx.get_errors().is_ok());
239    }
240
241    proptest! {
242        #![proptest_config(ProptestConfig::with_cases(200))]
243
244        #[test]
245        fn prop_credential_type_valid_length(len in 1_usize..=64) {
246            let ct = "AB".repeat(len); // even-length hex pairs — valid encodable bytes
247            let tx = CredentialAccept {
248                common_fields: CommonFields {
249                    account: "rSubject11111111111111111111111111".into(),
250                    transaction_type: TransactionType::CredentialAccept,
251                    ..Default::default()
252                },
253                issuer: "rIssuer111111111111111111111111111".into(),
254                credential_type: Cow::Owned(ct),
255            };
256            prop_assert!(tx.get_errors().is_ok(), "len {} should be valid", len);
257        }
258
259        #[test]
260        fn prop_credential_type_too_long(extra in 1_usize..=100) {
261            let len = 64 + extra; // "AB".repeat(64) = 128 chars (max); exceed that
262            let ct = "AB".repeat(len);
263            let tx = CredentialAccept {
264                common_fields: CommonFields {
265                    account: "rSubject11111111111111111111111111".into(),
266                    transaction_type: TransactionType::CredentialAccept,
267                    ..Default::default()
268                },
269                issuer: "rIssuer111111111111111111111111111".into(),
270                credential_type: Cow::Owned(ct),
271            };
272            prop_assert!(tx.get_errors().is_err(), "len {} should be rejected", len);
273        }
274
275        #[test]
276        fn prop_serde_roundtrip(ct in "[0-9A-F]{2,128}") {
277            let tx = CredentialAccept {
278                common_fields: CommonFields {
279                    account: "rSubject11111111111111111111111111".into(),
280                    transaction_type: TransactionType::CredentialAccept,
281                    fee: Some("10".into()),
282                    sequence: Some(1),
283                    signing_pub_key: Some(Cow::Borrowed("")),
284                    ..Default::default()
285                },
286                issuer: "rIssuer111111111111111111111111111".into(),
287                credential_type: Cow::Owned(ct),
288            };
289            let json = serde_json::to_string(&tx)
290                .map_err(|e| TestCaseError::fail(format!("serialize: {e}")))?;
291            let rt: CredentialAccept = serde_json::from_str(&json)
292                .map_err(|e| TestCaseError::fail(format!("deserialize: {e}")))?;
293            prop_assert_eq!(&tx, &rt);
294        }
295    }
296}