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::{
8 transactions::{Memo, Signer, Transaction, TransactionType},
9 Model, XRPLModelResult,
10};
11use crate::models::{FlagCollection, NoFlags};
12
13use super::{
14 exceptions::{XRPLDIDSetException, XRPLTransactionException},
15 CommonFields, CommonTransactionBuilder,
16};
17
18pub const MAX_DID_FIELD_LENGTH: usize = 512;
23
24#[skip_serializing_none]
30#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq, Clone)]
31#[serde(rename_all = "PascalCase")]
32pub struct DIDSet<'a> {
33 #[serde(flatten)]
38 pub common_fields: CommonFields<'a, NoFlags>,
39 pub data: Option<Cow<'a, str>>,
41 #[serde(rename = "DIDDocument")]
43 pub did_document: Option<Cow<'a, str>>,
44 #[serde(rename = "URI")]
46 pub uri: Option<Cow<'a, str>>,
47}
48
49impl<'a> Model for DIDSet<'a> {
50 fn get_errors(&self) -> XRPLModelResult<()> {
51 self.get_did_field_errors()
52 }
53}
54
55impl<'a> Transaction<'a, NoFlags> for DIDSet<'a> {
56 fn get_transaction_type(&self) -> &TransactionType {
57 self.common_fields.get_transaction_type()
58 }
59
60 fn get_common_fields(&self) -> &CommonFields<'_, NoFlags> {
61 self.common_fields.get_common_fields()
62 }
63
64 fn get_mut_common_fields(&mut self) -> &mut CommonFields<'a, NoFlags> {
65 self.common_fields.get_mut_common_fields()
66 }
67}
68
69impl<'a> CommonTransactionBuilder<'a, NoFlags> for DIDSet<'a> {
70 fn get_mut_common_fields(&mut self) -> &mut CommonFields<'a, NoFlags> {
71 &mut self.common_fields
72 }
73
74 fn into_self(self) -> Self {
75 self
76 }
77}
78
79fn is_hex(s: &str) -> bool {
82 s.chars().all(|c| c.is_ascii_hexdigit())
83}
84
85impl<'a> DIDSet<'a> {
86 pub fn new(
87 account: Cow<'a, str>,
88 account_txn_id: Option<Cow<'a, str>>,
89 fee: Option<XRPAmount<'a>>,
90 last_ledger_sequence: Option<u32>,
91 memos: Option<Vec<Memo>>,
92 sequence: Option<u32>,
93 signers: Option<Vec<Signer>>,
94 source_tag: Option<u32>,
95 ticket_sequence: Option<u32>,
96 data: Option<Cow<'a, str>>,
97 did_document: Option<Cow<'a, str>>,
98 uri: Option<Cow<'a, str>>,
99 ) -> Self {
100 Self {
101 common_fields: CommonFields::new(
102 account,
103 TransactionType::DIDSet,
104 account_txn_id,
105 fee,
106 Some(FlagCollection::default()),
107 last_ledger_sequence,
108 memos,
109 None,
110 sequence,
111 signers,
112 None,
113 source_tag,
114 ticket_sequence,
115 None,
116 ),
117 data,
118 did_document,
119 uri,
120 }
121 }
122
123 fn get_did_field_errors(&self) -> XRPLModelResult<()> {
125 if self.data.is_none() && self.did_document.is_none() && self.uri.is_none() {
127 return Err(XRPLTransactionException::from(
128 XRPLDIDSetException::MustHaveAtLeastOneField,
129 )
130 .into());
131 }
132
133 let all_empty = self.data.as_deref().is_none_or(|s| s.is_empty())
135 && self.did_document.as_deref().is_none_or(|s| s.is_empty())
136 && self.uri.as_deref().is_none_or(|s| s.is_empty());
137
138 if all_empty {
140 return Err(XRPLTransactionException::from(
141 XRPLDIDSetException::AtLeastOneFieldMustBeNonEmpty,
142 )
143 .into());
144 }
145
146 self.validate_did_field("data", self.data.as_deref())?;
148 self.validate_did_field("did_document", self.did_document.as_deref())?;
149 self.validate_did_field("uri", self.uri.as_deref())?;
150
151 Ok(())
152 }
153
154 fn validate_did_field(&self, field_name: &str, value: Option<&str>) -> XRPLModelResult<()> {
156 if let Some(val) = value {
157 if val.is_empty() {
158 return Ok(());
160 }
161
162 let valid_hex = is_hex(val);
163 let valid_length = val.len() <= MAX_DID_FIELD_LENGTH;
164
165 if !valid_hex && !valid_length {
166 return Err(XRPLTransactionException::from(
167 XRPLDIDSetException::InvalidFieldHexAndTooLong {
168 field: field_name.into(),
169 found_length: val.len(),
170 },
171 )
172 .into());
173 }
174
175 if !valid_hex {
176 return Err(
177 XRPLTransactionException::from(XRPLDIDSetException::InvalidFieldHex {
178 field: field_name.into(),
179 })
180 .into(),
181 );
182 }
183
184 if !valid_length {
185 return Err(
186 XRPLTransactionException::from(XRPLDIDSetException::FieldTooLong {
187 field: field_name.into(),
188 max: MAX_DID_FIELD_LENGTH,
189 found: val.len(),
190 })
191 .into(),
192 );
193 }
194 }
195
196 Ok(())
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 const ACCOUNT: &str = "r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ";
205 const VALID_FIELD: &str = "1234567890abcdefABCDEF";
206 const TOO_LONG_FIELD: &str = concat!(
207 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
208 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
209 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
210 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
211 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
212 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
213 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
214 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
215 "A"
216 ); const BAD_HEX_FIELD: &str = "random_non_hex_content";
218
219 #[test]
220 fn test_valid_all_fields() {
221 let tx = DIDSet {
222 common_fields: CommonFields {
223 account: ACCOUNT.into(),
224 transaction_type: TransactionType::DIDSet,
225 ..Default::default()
226 },
227 data: Some(VALID_FIELD.into()),
228 did_document: Some(VALID_FIELD.into()),
229 uri: Some(VALID_FIELD.into()),
230 };
231 assert!(tx.is_valid());
232 }
233
234 #[test]
235 fn test_valid_only_data() {
236 let tx = DIDSet {
237 common_fields: CommonFields {
238 account: ACCOUNT.into(),
239 transaction_type: TransactionType::DIDSet,
240 ..Default::default()
241 },
242 data: Some(VALID_FIELD.into()),
243 did_document: None,
244 uri: None,
245 };
246 assert!(tx.is_valid());
247 }
248
249 #[test]
250 fn test_valid_only_did_document() {
251 let tx = DIDSet {
252 common_fields: CommonFields {
253 account: ACCOUNT.into(),
254 transaction_type: TransactionType::DIDSet,
255 ..Default::default()
256 },
257 data: None,
258 did_document: Some(VALID_FIELD.into()),
259 uri: None,
260 };
261 assert!(tx.is_valid());
262 }
263
264 #[test]
265 fn test_valid_only_uri() {
266 let tx = DIDSet {
267 common_fields: CommonFields {
268 account: ACCOUNT.into(),
269 transaction_type: TransactionType::DIDSet,
270 ..Default::default()
271 },
272 data: None,
273 did_document: None,
274 uri: Some(VALID_FIELD.into()),
275 };
276 assert!(tx.is_valid());
277 }
278
279 #[test]
280 fn test_empty_no_fields() {
281 let tx = DIDSet {
282 common_fields: CommonFields {
283 account: ACCOUNT.into(),
284 transaction_type: TransactionType::DIDSet,
285 ..Default::default()
286 },
287 data: None,
288 did_document: None,
289 uri: None,
290 };
291 assert!(!tx.is_valid());
292 assert!(tx.get_errors().is_err());
293 }
294
295 #[test]
296 fn test_all_empty_strings() {
297 let tx = DIDSet {
298 common_fields: CommonFields {
299 account: ACCOUNT.into(),
300 transaction_type: TransactionType::DIDSet,
301 ..Default::default()
302 },
303 data: Some("".into()),
304 did_document: Some("".into()),
305 uri: Some("".into()),
306 };
307 assert!(!tx.is_valid());
308 assert!(tx.get_errors().is_err());
309 }
310
311 #[test]
312 fn test_single_empty_data_field_is_valid() {
313 let tx = DIDSet {
316 common_fields: CommonFields {
317 account: ACCOUNT.into(),
318 transaction_type: TransactionType::DIDSet,
319 ..Default::default()
320 },
321 data: Some("".into()),
322 did_document: Some(VALID_FIELD.into()),
323 uri: None,
324 };
325 assert!(tx.is_valid());
326 }
327
328 #[test]
329 fn test_too_long() {
330 let tx = DIDSet {
331 common_fields: CommonFields {
332 account: ACCOUNT.into(),
333 transaction_type: TransactionType::DIDSet,
334 ..Default::default()
335 },
336 data: None,
337 did_document: Some(TOO_LONG_FIELD.into()),
338 uri: None,
339 };
340 assert!(!tx.is_valid());
341 assert!(tx.get_errors().is_err());
342 }
343
344 #[test]
345 fn test_not_hex() {
346 let tx = DIDSet {
347 common_fields: CommonFields {
348 account: ACCOUNT.into(),
349 transaction_type: TransactionType::DIDSet,
350 ..Default::default()
351 },
352 data: Some(BAD_HEX_FIELD.into()),
353 did_document: None,
354 uri: None,
355 };
356 assert!(!tx.is_valid());
357 assert!(tx.get_errors().is_err());
358 }
359
360 #[test]
361 fn test_too_long_and_not_hex() {
362 let bad_field: alloc::string::String = "q".repeat(513);
364 let tx = DIDSet {
365 common_fields: CommonFields {
366 account: ACCOUNT.into(),
367 transaction_type: TransactionType::DIDSet,
368 ..Default::default()
369 },
370 data: None,
371 did_document: None,
372 uri: Some(bad_field.into()),
373 };
374 assert!(!tx.is_valid());
375 assert!(tx.get_errors().is_err());
376 }
377
378 #[test]
379 fn test_serialize() {
380 let tx = DIDSet {
381 common_fields: CommonFields {
382 account: "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh".into(),
383 transaction_type: TransactionType::DIDSet,
384 fee: Some("10".into()),
385 sequence: Some(391),
386 signing_pub_key: Some(
387 "0330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD020".into(),
388 ),
389 ..Default::default()
390 },
391 data: Some("617474657374".into()),
392 did_document: Some("646F63".into()),
393 uri: Some("6469645F6578616D706C65".into()),
394 };
395
396 let expected_json = r#"{"Account":"rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh","TransactionType":"DIDSet","Fee":"10","Flags":0,"Sequence":391,"SigningPubKey":"0330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD020","Data":"617474657374","DIDDocument":"646F63","URI":"6469645F6578616D706C65"}"#;
397
398 let serialized = serde_json::to_string(&tx).unwrap();
399 let expected_value = serde_json::to_value(expected_json).unwrap();
400 let serialized_value = serde_json::to_value(&serialized).unwrap();
401 assert_eq!(serialized_value, expected_value);
402
403 let deserialized: DIDSet = serde_json::from_str(expected_json).unwrap();
404 assert_eq!(tx, deserialized);
405 }
406
407 #[test]
408 fn test_builder_pattern() {
409 let tx = DIDSet {
410 common_fields: CommonFields {
411 account: "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh".into(),
412 transaction_type: TransactionType::DIDSet,
413 ..Default::default()
414 },
415 data: Some("617474657374".into()),
416 did_document: Some("646F63".into()),
417 uri: Some("6469645F6578616D706C65".into()),
418 }
419 .with_fee("10".into())
420 .with_sequence(391)
421 .with_last_ledger_sequence(7108682);
422
423 assert_eq!(tx.data.as_deref(), Some("617474657374"));
424 assert_eq!(tx.did_document.as_deref(), Some("646F63"));
425 assert_eq!(tx.uri.as_deref(), Some("6469645F6578616D706C65"));
426 assert_eq!(tx.common_fields.fee.as_ref().unwrap().0, "10");
427 assert_eq!(tx.common_fields.sequence, Some(391));
428 assert_eq!(tx.common_fields.last_ledger_sequence, Some(7108682));
429 }
430
431 #[test]
432 fn test_default() {
433 let tx = DIDSet {
434 common_fields: CommonFields {
435 account: ACCOUNT.into(),
436 transaction_type: TransactionType::DIDSet,
437 ..Default::default()
438 },
439 data: Some(VALID_FIELD.into()),
440 ..Default::default()
441 };
442
443 assert_eq!(tx.common_fields.account, ACCOUNT);
444 assert_eq!(tx.common_fields.transaction_type, TransactionType::DIDSet);
445 assert_eq!(tx.data.as_deref(), Some(VALID_FIELD));
446 assert!(tx.did_document.is_none());
447 assert!(tx.uri.is_none());
448 }
449
450 #[test]
451 fn test_max_length_exactly_512() {
452 let max_field: alloc::string::String = "A".repeat(512);
453 let tx = DIDSet {
454 common_fields: CommonFields {
455 account: ACCOUNT.into(),
456 transaction_type: TransactionType::DIDSet,
457 ..Default::default()
458 },
459 data: Some(max_field.into()),
460 did_document: None,
461 uri: None,
462 };
463 assert!(tx.is_valid());
464 }
465}