Skip to main content

xrpl/models/transactions/
vault_set.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::{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/// Update the settings of an existing vault on the XRP Ledger (XLS-65).
17///
18/// Only the vault owner can submit this transaction. It allows updating
19/// optional metadata fields such as data, assets maximum, and domain ID.
20///
21/// See VaultSet transaction:
22/// `<https://github.com/XRPLF/XRPL-Standards/tree/master/XLS-0065d-single-asset-vault>`
23#[skip_serializing_none]
24#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq, Clone)]
25#[serde(rename_all = "PascalCase")]
26pub struct VaultSet<'a> {
27    /// The base fields for all transaction models.
28    ///
29    /// See Transaction Common Fields:
30    /// `<https://xrpl.org/transaction-common-fields.html>`
31    #[serde(flatten)]
32    pub common_fields: CommonFields<'a, NoFlags>,
33    /// The ID of the vault to update (256-bit hex string).
34    #[serde(rename = "VaultID")]
35    pub vault_id: Cow<'a, str>,
36    /// Arbitrary hex-encoded data associated with the vault.
37    pub data: Option<Cow<'a, str>>,
38    /// The maximum amount of assets the vault can hold, as a string-encoded integer.
39    pub assets_maximum: Option<Cow<'a, str>>,
40    /// The domain ID associated with the vault.
41    #[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    /// Set the data field.
131    pub fn with_data(mut self, data: Cow<'a, str>) -> Self {
132        self.data = Some(data);
133        self
134    }
135
136    /// Set the assets maximum field.
137    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    /// Set the domain ID field.
143    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        // Serialize
173        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        // Deserialize
180        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        // Wrong length.
364        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        // Non-hex character.
376        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}