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::{FlagCollection, Model, NoFlags, XRPLModelException, XRPLModelResult};
8
9use super::vault_common::{
10 validate_hash256, validate_hex_blob, validate_nonnegative_number, validate_vault_id,
11};
12use super::{CommonFields, CommonTransactionBuilder, Memo, Signer, Transaction, TransactionType};
13
14const MAX_VAULT_DATA_HEX_LEN: usize = 512;
15
16#[skip_serializing_none]
24#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq, Clone)]
25#[serde(rename_all = "PascalCase")]
26pub struct VaultSet<'a> {
27 #[serde(flatten)]
32 pub common_fields: CommonFields<'a, NoFlags>,
33 #[serde(rename = "VaultID")]
35 pub vault_id: Cow<'a, str>,
36 pub data: Option<Cow<'a, str>>,
38 pub assets_maximum: Option<Cow<'a, str>>,
40 #[serde(rename = "DomainID")]
42 pub domain_id: Option<Cow<'a, str>>,
43}
44
45impl Model for VaultSet<'_> {
46 fn get_errors(&self) -> XRPLModelResult<()> {
47 validate_vault_id(&self.vault_id)?;
48 if self.data.is_none() && self.assets_maximum.is_none() && self.domain_id.is_none() {
49 return Err(XRPLModelException::MissingField(
50 "data, assets_maximum, or domain_id".into(),
51 ));
52 }
53 if let Some(data) = self.data.as_deref() {
54 validate_hex_blob("data", data, MAX_VAULT_DATA_HEX_LEN)?;
55 }
56 if let Some(maximum) = self.assets_maximum.as_deref() {
57 validate_nonnegative_number("assets_maximum", maximum)?;
58 }
59 if let Some(domain_id) = self.domain_id.as_deref() {
60 validate_hash256("domain_id", domain_id)?;
61 }
62 Ok(())
63 }
64}
65
66impl<'a> Transaction<'a, NoFlags> for VaultSet<'a> {
67 fn get_common_fields(&self) -> &CommonFields<'_, NoFlags> {
68 &self.common_fields
69 }
70
71 fn get_mut_common_fields(&mut self) -> &mut CommonFields<'a, NoFlags> {
72 &mut self.common_fields
73 }
74
75 fn get_transaction_type(&self) -> &TransactionType {
76 self.common_fields.get_transaction_type()
77 }
78}
79
80impl<'a> CommonTransactionBuilder<'a, NoFlags> for VaultSet<'a> {
81 fn get_mut_common_fields(&mut self) -> &mut CommonFields<'a, NoFlags> {
82 &mut self.common_fields
83 }
84
85 fn into_self(self) -> Self {
86 self
87 }
88}
89
90impl<'a> VaultSet<'a> {
91 pub fn new(
92 account: Cow<'a, str>,
93 account_txn_id: Option<Cow<'a, str>>,
94 fee: Option<XRPAmount<'a>>,
95 last_ledger_sequence: Option<u32>,
96 memos: Option<Vec<Memo>>,
97 sequence: Option<u32>,
98 signers: Option<Vec<Signer>>,
99 source_tag: Option<u32>,
100 ticket_sequence: Option<u32>,
101 vault_id: Cow<'a, str>,
102 data: Option<Cow<'a, str>>,
103 assets_maximum: Option<Cow<'a, str>>,
104 domain_id: Option<Cow<'a, str>>,
105 ) -> VaultSet<'a> {
106 VaultSet {
107 common_fields: CommonFields::new(
108 account,
109 TransactionType::VaultSet,
110 account_txn_id,
111 fee,
112 Some(FlagCollection::default()),
113 last_ledger_sequence,
114 memos,
115 None,
116 sequence,
117 signers,
118 None,
119 source_tag,
120 ticket_sequence,
121 None,
122 ),
123 vault_id,
124 data,
125 assets_maximum,
126 domain_id,
127 }
128 }
129
130 pub fn with_data(mut self, data: Cow<'a, str>) -> Self {
132 self.data = Some(data);
133 self
134 }
135
136 pub fn with_assets_maximum(mut self, assets_maximum: Cow<'a, str>) -> Self {
138 self.assets_maximum = Some(assets_maximum);
139 self
140 }
141
142 pub fn with_domain_id(mut self, domain_id: Cow<'a, str>) -> Self {
144 self.domain_id = Some(domain_id);
145 self
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 const VAULT_ID: &str = "A0000000000000000000000000000000000000000000000000000000DEADBEEF";
154
155 #[test]
156 fn test_serde() {
157 let vault_set = VaultSet {
158 common_fields: CommonFields {
159 account: "rVaultOwner123".into(),
160 transaction_type: TransactionType::VaultSet,
161 signing_pub_key: Some("".into()),
162 ..Default::default()
163 },
164 vault_id: VAULT_ID.into(),
165 data: Some("48656C6C6F".into()),
166 assets_maximum: None,
167 domain_id: None,
168 };
169
170 let json_str = r#"{"Account":"rVaultOwner123","TransactionType":"VaultSet","Flags":0,"SigningPubKey":"","VaultID":"A0000000000000000000000000000000000000000000000000000000DEADBEEF","Data":"48656C6C6F"}"#;
171
172 let serialized = serde_json::to_string(&vault_set).unwrap();
174 assert_eq!(
175 serde_json::to_value(&serialized).unwrap(),
176 serde_json::to_value(json_str).unwrap()
177 );
178
179 let deserialized: VaultSet = serde_json::from_str(json_str).unwrap();
181 assert_eq!(vault_set, deserialized);
182 }
183
184 #[test]
185 fn test_serde_all_optional_fields() {
186 let vault_set = VaultSet {
187 common_fields: CommonFields {
188 account: "rVaultOwnerAll456".into(),
189 transaction_type: TransactionType::VaultSet,
190 signing_pub_key: Some("".into()),
191 ..Default::default()
192 },
193 vault_id: VAULT_ID.into(),
194 data: Some("48656C6C6F".into()),
195 assets_maximum: Some("2000000000".into()),
196 domain_id: Some(
197 "D0000000000000000000000000000000000000000000000000000000DEADBEEF".into(),
198 ),
199 };
200
201 let serialized = serde_json::to_string(&vault_set).unwrap();
202 let deserialized: VaultSet = serde_json::from_str(&serialized).unwrap();
203 assert_eq!(vault_set, deserialized);
204 }
205
206 #[test]
207 fn test_builder_pattern() {
208 let vault_set = VaultSet {
209 common_fields: CommonFields {
210 account: "rVaultOwner123".into(),
211 transaction_type: TransactionType::VaultSet,
212 ..Default::default()
213 },
214 vault_id: VAULT_ID.into(),
215 ..Default::default()
216 }
217 .with_fee("12".into())
218 .with_sequence(100)
219 .with_last_ledger_sequence(7108682)
220 .with_source_tag(12345)
221 .with_data("48656C6C6F".into())
222 .with_assets_maximum("2000000000".into())
223 .with_domain_id("D0000000000000000000000000000000000000000000000000000000DEADBEEF".into())
224 .with_memo(Memo {
225 memo_data: Some("updating vault settings".into()),
226 memo_format: None,
227 memo_type: Some("text".into()),
228 });
229
230 assert_eq!(vault_set.vault_id, VAULT_ID);
231 assert_eq!(vault_set.common_fields.fee.as_ref().unwrap().0, "12");
232 assert_eq!(vault_set.common_fields.sequence, Some(100));
233 assert_eq!(vault_set.common_fields.last_ledger_sequence, Some(7108682));
234 assert_eq!(vault_set.common_fields.source_tag, Some(12345));
235 assert_eq!(vault_set.data, Some("48656C6C6F".into()));
236 assert_eq!(vault_set.assets_maximum, Some("2000000000".into()));
237 assert_eq!(vault_set.common_fields.memos.as_ref().unwrap().len(), 1);
238 }
239
240 #[test]
241 fn test_default() {
242 let vault_set = VaultSet {
243 common_fields: CommonFields {
244 account: "rVaultOwner789".into(),
245 transaction_type: TransactionType::VaultSet,
246 ..Default::default()
247 },
248 vault_id: VAULT_ID.into(),
249 ..Default::default()
250 };
251
252 assert_eq!(vault_set.common_fields.account, "rVaultOwner789");
253 assert_eq!(
254 vault_set.common_fields.transaction_type,
255 TransactionType::VaultSet
256 );
257 assert_eq!(vault_set.vault_id, VAULT_ID);
258 assert!(vault_set.data.is_none());
259 assert!(vault_set.assets_maximum.is_none());
260 assert!(vault_set.domain_id.is_none());
261 assert!(vault_set.common_fields.fee.is_none());
262 assert!(vault_set.common_fields.sequence.is_none());
263 }
264
265 #[test]
266 fn test_ticket_sequence() {
267 let ticket_set = VaultSet {
268 common_fields: CommonFields {
269 account: "rTicketVaultSet111".into(),
270 transaction_type: TransactionType::VaultSet,
271 ..Default::default()
272 },
273 vault_id: VAULT_ID.into(),
274 ..Default::default()
275 }
276 .with_ticket_sequence(54321)
277 .with_fee("12".into());
278
279 assert_eq!(ticket_set.common_fields.ticket_sequence, Some(54321));
280 assert!(ticket_set.common_fields.sequence.is_none());
281 }
282
283 #[test]
284 fn test_multiple_memos() {
285 let multi_memo_set = VaultSet {
286 common_fields: CommonFields {
287 account: "rMultiMemoVaultSet222".into(),
288 transaction_type: TransactionType::VaultSet,
289 ..Default::default()
290 },
291 vault_id: VAULT_ID.into(),
292 ..Default::default()
293 }
294 .with_memo(Memo {
295 memo_data: Some("first update".into()),
296 memo_format: None,
297 memo_type: Some("text".into()),
298 })
299 .with_memo(Memo {
300 memo_data: Some("second update".into()),
301 memo_format: None,
302 memo_type: Some("text".into()),
303 })
304 .with_fee("18".into())
305 .with_sequence(400);
306
307 assert_eq!(
308 multi_memo_set.common_fields.memos.as_ref().unwrap().len(),
309 2
310 );
311 assert_eq!(multi_memo_set.common_fields.sequence, Some(400));
312 }
313
314 #[test]
315 fn test_new_constructor() {
316 let vault_set = VaultSet::new(
317 "rNewVaultSetter333".into(),
318 None,
319 Some("12".into()),
320 Some(7108682),
321 None,
322 Some(100),
323 None,
324 None,
325 None,
326 VAULT_ID.into(),
327 Some("48656C6C6F".into()),
328 Some("2000000000".into()),
329 Some("D0000000000000000000000000000000000000000000000000000000DEADBEEF".into()),
330 );
331
332 assert_eq!(vault_set.common_fields.account, "rNewVaultSetter333");
333 assert_eq!(
334 vault_set.common_fields.transaction_type,
335 TransactionType::VaultSet
336 );
337 assert_eq!(vault_set.common_fields.fee.as_ref().unwrap().0, "12");
338 assert_eq!(vault_set.vault_id, VAULT_ID);
339 assert_eq!(vault_set.data, Some("48656C6C6F".into()));
340 assert_eq!(vault_set.assets_maximum, Some("2000000000".into()));
341 }
342
343 #[test]
344 fn test_validate() {
345 let vault_set = VaultSet {
346 common_fields: CommonFields {
347 account: "rValidateVaultSet444".into(),
348 transaction_type: TransactionType::VaultSet,
349 ..Default::default()
350 },
351 vault_id: VAULT_ID.into(),
352 ..Default::default()
353 }
354 .with_fee("12".into())
355 .with_sequence(300)
356 .with_data("48656C6C6F".into());
357
358 assert!(vault_set.validate().is_ok());
359 }
360
361 #[test]
362 fn test_invalid_vault_id_rejected() {
363 let short_id = VaultSet {
365 common_fields: CommonFields {
366 account: "rShortID".into(),
367 transaction_type: TransactionType::VaultSet,
368 ..Default::default()
369 },
370 vault_id: "DEADBEEF".into(),
371 ..Default::default()
372 };
373 assert!(short_id.validate().is_err());
374
375 let bad_hex = VaultSet {
377 common_fields: CommonFields {
378 account: "rBadHex".into(),
379 transaction_type: TransactionType::VaultSet,
380 ..Default::default()
381 },
382 vault_id: "Z0000000000000000000000000000000000000000000000000000000DEADBEEF".into(),
383 ..Default::default()
384 };
385 assert!(bad_hex.validate().is_err());
386 }
387
388 #[test]
389 fn test_update_data_only() {
390 let vault_set = VaultSet {
391 common_fields: CommonFields {
392 account: "rDataOnlyUpdate555".into(),
393 transaction_type: TransactionType::VaultSet,
394 ..Default::default()
395 },
396 vault_id: VAULT_ID.into(),
397 ..Default::default()
398 }
399 .with_data("4E6577446174614F6E6C79".into())
400 .with_fee("12".into())
401 .with_sequence(500);
402
403 assert_eq!(vault_set.data, Some("4E6577446174614F6E6C79".into()));
404 assert!(vault_set.assets_maximum.is_none());
405 assert!(vault_set.domain_id.is_none());
406 assert!(vault_set.validate().is_ok());
407 }
408
409 #[test]
410 fn test_assets_maximum_xrpl_number_accepted() {
411 for assets_maximum in ["100.5", "1e6", "9999999999999999e80"] {
412 let vault_set = VaultSet {
413 common_fields: CommonFields {
414 account: "rVaultNumber".into(),
415 transaction_type: TransactionType::VaultSet,
416 ..Default::default()
417 },
418 vault_id: VAULT_ID.into(),
419 assets_maximum: Some(assets_maximum.into()),
420 ..Default::default()
421 };
422 assert!(
423 vault_set.validate().is_ok(),
424 "{assets_maximum} should validate"
425 );
426 }
427 }
428
429 #[test]
430 fn test_assets_maximum_integer_accepted() {
431 let vault_set = VaultSet {
432 common_fields: CommonFields {
433 account: "rVaultMax".into(),
434 transaction_type: TransactionType::VaultSet,
435 fee: Some("12".into()),
436 sequence: Some(1),
437 ..Default::default()
438 },
439 vault_id: VAULT_ID.into(),
440 assets_maximum: Some("2000000000".into()),
441 ..Default::default()
442 };
443 assert!(vault_set.validate().is_ok());
444 }
445}