xrpl/models/transactions/
confidential_mpt_clawback.rs1use 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, ValidateCurrencies, XRPLModelException,
10};
11use crate::models::{FlagCollection, NoFlags};
12
13use super::confidential_mpt_constants::{
14 address_is_issuer, validate_hex_length, validate_mpt_amount, CLAWBACK_PROOF_LENGTH,
15};
16use super::mptoken_issuance_set::{validate_holder_address, validate_mptoken_issuance_id};
17use super::{CommonFields, CommonTransactionBuilder};
18
19#[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 ConfidentialMPTClawback<'a> {
41 #[serde(flatten)]
43 pub common_fields: CommonFields<'a, NoFlags>,
44
45 pub holder: Cow<'a, str>,
47
48 #[serde(rename = "MPTokenIssuanceID")]
49 pub mptoken_issuance_id: Cow<'a, str>,
50
51 #[serde(rename = "MPTAmount")]
54 pub mpt_amount: Cow<'a, str>,
55
56 #[serde(rename = "ZKProof")]
58 pub zk_proof: Cow<'a, str>,
59}
60
61impl<'a> Model for ConfidentialMPTClawback<'a> {
62 fn get_errors(&self) -> crate::models::XRPLModelResult<()> {
63 self._get_holder_error()?;
64 self._get_field_length_errors()?;
65 self._get_issuer_role_error()?;
66 self.validate_currencies()
67 }
68}
69
70impl<'a> ConfidentialMPTClawback<'a> {
71 fn _get_holder_error(&self) -> crate::models::XRPLModelResult<()> {
73 validate_holder_address(self.holder.as_ref())?;
74 if self.holder == self.common_fields.account {
75 return Err(XRPLModelException::ValueEqualsValue {
76 field1: "holder".into(),
77 field2: "account".into(),
78 });
79 }
80 Ok(())
81 }
82
83 fn _get_issuer_role_error(&self) -> crate::models::XRPLModelResult<()> {
88 if !address_is_issuer(
89 self.mptoken_issuance_id.as_ref(),
90 self.common_fields.account.as_ref(),
91 ) {
92 return Err(XRPLModelException::InvalidValue {
93 field: "account".into(),
94 expected: "the issuance's issuer (ConfidentialMPTClawback is issuer-only)".into(),
95 found: self.common_fields.account.as_ref().into(),
96 });
97 }
98 Ok(())
99 }
100
101 fn _get_field_length_errors(&self) -> crate::models::XRPLModelResult<()> {
102 validate_mptoken_issuance_id(self.mptoken_issuance_id.as_ref())?;
103 validate_mpt_amount("mpt_amount", self.mpt_amount.as_ref(), true)?;
104 validate_hex_length("zk_proof", self.zk_proof.as_ref(), CLAWBACK_PROOF_LENGTH)
105 }
106}
107
108impl<'a> Transaction<'a, NoFlags> for ConfidentialMPTClawback<'a> {
109 fn get_transaction_type(&self) -> &TransactionType {
110 self.common_fields.get_transaction_type()
111 }
112
113 fn get_common_fields(&self) -> &CommonFields<'_, NoFlags> {
114 self.common_fields.get_common_fields()
115 }
116
117 fn get_mut_common_fields(&mut self) -> &mut CommonFields<'a, NoFlags> {
118 self.common_fields.get_mut_common_fields()
119 }
120}
121
122impl<'a> CommonTransactionBuilder<'a, NoFlags> for ConfidentialMPTClawback<'a> {
123 fn get_mut_common_fields(&mut self) -> &mut CommonFields<'a, NoFlags> {
124 &mut self.common_fields
125 }
126
127 fn into_self(self) -> Self {
128 self
129 }
130}
131
132impl<'a> ConfidentialMPTClawback<'a> {
133 #[allow(clippy::too_many_arguments)]
134 pub fn new(
135 account: Cow<'a, str>,
136 account_txn_id: Option<Cow<'a, str>>,
137 fee: Option<XRPAmount<'a>>,
138 last_ledger_sequence: Option<u32>,
139 memos: Option<Vec<Memo>>,
140 sequence: Option<u32>,
141 signers: Option<Vec<Signer>>,
142 source_tag: Option<u32>,
143 ticket_sequence: Option<u32>,
144 holder: Cow<'a, str>,
145 mptoken_issuance_id: Cow<'a, str>,
146 mpt_amount: Cow<'a, str>,
147 zk_proof: Cow<'a, str>,
148 ) -> Self {
149 Self {
150 common_fields: CommonFields::new(
151 account,
152 TransactionType::ConfidentialMPTClawback,
153 account_txn_id,
154 fee,
155 Some(FlagCollection::default()),
156 last_ledger_sequence,
157 memos,
158 None,
159 sequence,
160 signers,
161 None,
162 source_tag,
163 ticket_sequence,
164 None,
165 ),
166 holder,
167 mptoken_issuance_id,
168 mpt_amount,
169 zk_proof,
170 }
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
179 fn test_serialize() {
180 let tx = ConfidentialMPTClawback {
181 common_fields: CommonFields {
182 account: "rIssuerAccount11111111111111".into(),
183 transaction_type: TransactionType::ConfidentialMPTClawback,
184 ..Default::default()
185 },
186 holder: "rHolderAccount11111111111111".into(),
187 mptoken_issuance_id: "610F33".repeat(8).into(),
188 mpt_amount: "1000".into(),
189 zk_proof: "a1".repeat(64).into(),
190 };
191
192 let json = serde_json::to_string(&tx).unwrap();
193 assert!(json.contains("\"TransactionType\":\"ConfidentialMPTClawback\""));
194 assert!(json.contains("\"Holder\":\"rHolderAccount"));
195
196 let round_tripped: ConfidentialMPTClawback = serde_json::from_str(&json).unwrap();
197 assert_eq!(round_tripped, tx);
198 }
199
200 #[test]
201 fn test_new_builder_and_accessors() {
202 let mut tx = ConfidentialMPTClawback::new(
203 "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh".into(),
204 None,
205 None,
206 None,
207 None,
208 None,
209 None,
210 None,
211 None,
212 "rLSn6Z3T8uCxbcd1oxwfGQN1Fdn5CyGujK".into(),
213 "00000001B5F762798A53D543A014CAF8B297CFF8F2F937E8".into(),
216 "1000".into(),
217 "a1".repeat(64).into(),
218 )
219 .with_fee(XRPAmount::from("15000"))
220 .with_sequence(9);
221
222 assert_eq!(tx.get_common_fields().sequence, Some(9));
223 assert_eq!(tx.get_common_fields().fee, Some(XRPAmount::from("15000")));
224 assert_eq!(
225 tx.get_transaction_type(),
226 &TransactionType::ConfidentialMPTClawback
227 );
228 assert!(tx.get_errors().is_ok());
229
230 let common =
231 <ConfidentialMPTClawback as Transaction<'_, NoFlags>>::get_mut_common_fields(&mut tx);
232 assert_eq!(common.sequence, Some(9));
233 }
234
235 const ISSUER: &str = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh";
237 const HOLDER: &str = "rLSn6Z3T8uCxbcd1oxwfGQN1Fdn5CyGujK";
238 const ISS_OF_ISSUER: &str = "00000001B5F762798A53D543A014CAF8B297CFF8F2F937E8";
240
241 fn valid_clawback() -> ConfidentialMPTClawback<'static> {
242 ConfidentialMPTClawback {
243 common_fields: CommonFields {
244 account: ISSUER.into(),
245 transaction_type: TransactionType::ConfidentialMPTClawback,
246 ..Default::default()
247 },
248 holder: HOLDER.into(),
249 mptoken_issuance_id: ISS_OF_ISSUER.into(),
250 mpt_amount: "1000".into(),
251 zk_proof: "a1".repeat(64).into(),
252 }
253 }
254
255 #[test]
256 fn test_valid_clawback_passes() {
257 assert!(valid_clawback().get_errors().is_ok());
258 }
259
260 #[test]
261 fn test_non_issuer_account_rejected() {
262 let mut tx = valid_clawback();
264 tx.mptoken_issuance_id = "610F33".repeat(8).into();
265 assert!(tx.get_errors().is_err());
266 }
267
268 #[test]
269 fn test_self_clawback_rejected() {
270 let mut tx = valid_clawback();
272 tx.holder = ISSUER.into();
273 assert!(tx.get_errors().is_err());
274 }
275
276 #[test]
277 fn test_zero_amount_rejected() {
278 let mut tx = valid_clawback();
279 tx.mpt_amount = "0".into();
280 assert!(tx.get_errors().is_err());
281 }
282
283 #[test]
284 fn test_amount_above_mpt_max_rejected() {
285 let mut tx = valid_clawback();
286 tx.mpt_amount = "9223372036854775808".into();
287 assert!(tx.get_errors().is_err());
288 }
289
290 #[test]
291 fn test_malformed_holder_rejected() {
292 let mut tx = valid_clawback();
293 tx.holder = "not_a_classic_address".into();
294 assert!(tx.get_errors().is_err());
295 }
296}