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, ValidateCurrencies, XRPLModelException,
10};
11use crate::models::{FlagCollection, NoFlags};
12
13use super::confidential_mpt_constants::{
14 address_is_issuer, validate_hex_length, validate_mpt_amount, BLINDING_FACTOR_LENGTH,
15 CIPHERTEXT_LENGTH, ENCRYPTION_KEY_LENGTH, SCHNORR_PROOF_LENGTH,
16};
17use super::mptoken_issuance_set::validate_mptoken_issuance_id;
18use super::{CommonFields, CommonTransactionBuilder};
19
20#[skip_serializing_none]
31#[derive(
32 Debug,
33 Default,
34 Serialize,
35 Deserialize,
36 PartialEq,
37 Eq,
38 Clone,
39 xrpl_rust_macros::ValidateCurrencies,
40)]
41#[serde(rename_all = "PascalCase")]
42pub struct ConfidentialMPTConvert<'a> {
43 #[serde(flatten)]
44 pub common_fields: CommonFields<'a, NoFlags>,
45
46 #[serde(rename = "MPTokenIssuanceID")]
48 pub mptoken_issuance_id: Cow<'a, str>,
49
50 #[serde(rename = "MPTAmount")]
53 pub mpt_amount: Cow<'a, str>,
54
55 pub holder_encrypted_amount: Cow<'a, str>,
57
58 pub issuer_encrypted_amount: Cow<'a, str>,
60
61 pub blinding_factor: Cow<'a, str>,
64
65 pub holder_encryption_key: Option<Cow<'a, str>>,
68
69 pub auditor_encrypted_amount: Option<Cow<'a, str>>,
72
73 #[serde(rename = "ZKProof")]
77 pub zk_proof: Option<Cow<'a, str>>,
78}
79
80impl<'a> Model for ConfidentialMPTConvert<'a> {
81 fn get_errors(&self) -> crate::models::XRPLModelResult<()> {
82 self._get_registration_error()?;
83 self._get_field_length_errors()?;
84 self._get_issuer_role_error()?;
85 self.validate_currencies()
86 }
87}
88
89impl<'a> ConfidentialMPTConvert<'a> {
90 fn _get_registration_error(&self) -> crate::models::XRPLModelResult<()> {
94 match (
95 self.holder_encryption_key.is_some(),
96 self.zk_proof.is_some(),
97 ) {
98 (true, false) => Err(XRPLModelException::FieldRequiresField {
99 field1: "holder_encryption_key".into(),
100 field2: "zk_proof".into(),
101 }),
102 (false, true) => Err(XRPLModelException::FieldRequiresField {
103 field1: "zk_proof".into(),
104 field2: "holder_encryption_key".into(),
105 }),
106 _ => Ok(()),
107 }
108 }
109
110 fn _get_issuer_role_error(&self) -> crate::models::XRPLModelResult<()> {
114 if address_is_issuer(
115 self.mptoken_issuance_id.as_ref(),
116 self.common_fields.account.as_ref(),
117 ) {
118 return Err(XRPLModelException::ValueEqualsValue {
119 field1: "account".into(),
120 field2: "issuer".into(),
121 });
122 }
123 Ok(())
124 }
125
126 fn _get_field_length_errors(&self) -> crate::models::XRPLModelResult<()> {
127 validate_mptoken_issuance_id(self.mptoken_issuance_id.as_ref())?;
128 validate_mpt_amount("mpt_amount", self.mpt_amount.as_ref(), false)?;
133 validate_hex_length(
134 "holder_encrypted_amount",
135 self.holder_encrypted_amount.as_ref(),
136 CIPHERTEXT_LENGTH,
137 )?;
138 validate_hex_length(
139 "issuer_encrypted_amount",
140 self.issuer_encrypted_amount.as_ref(),
141 CIPHERTEXT_LENGTH,
142 )?;
143 if let Some(auditor) = self.auditor_encrypted_amount.as_deref() {
144 validate_hex_length("auditor_encrypted_amount", auditor, CIPHERTEXT_LENGTH)?;
145 }
146 validate_hex_length(
147 "blinding_factor",
148 self.blinding_factor.as_ref(),
149 BLINDING_FACTOR_LENGTH,
150 )?;
151 if let Some(key) = self.holder_encryption_key.as_deref() {
152 validate_hex_length("holder_encryption_key", key, ENCRYPTION_KEY_LENGTH)?;
153 }
154 if let Some(proof) = self.zk_proof.as_deref() {
155 validate_hex_length("zk_proof", proof, SCHNORR_PROOF_LENGTH)?;
156 }
157 Ok(())
158 }
159}
160
161impl<'a> Transaction<'a, NoFlags> for ConfidentialMPTConvert<'a> {
162 fn get_transaction_type(&self) -> &TransactionType {
163 self.common_fields.get_transaction_type()
164 }
165
166 fn get_common_fields(&self) -> &CommonFields<'_, NoFlags> {
167 self.common_fields.get_common_fields()
168 }
169
170 fn get_mut_common_fields(&mut self) -> &mut CommonFields<'a, NoFlags> {
171 self.common_fields.get_mut_common_fields()
172 }
173}
174
175impl<'a> CommonTransactionBuilder<'a, NoFlags> for ConfidentialMPTConvert<'a> {
176 fn get_mut_common_fields(&mut self) -> &mut CommonFields<'a, NoFlags> {
177 &mut self.common_fields
178 }
179
180 fn into_self(self) -> Self {
181 self
182 }
183}
184
185impl<'a> ConfidentialMPTConvert<'a> {
186 #[allow(clippy::too_many_arguments)]
187 pub fn new(
188 account: Cow<'a, str>,
189 account_txn_id: Option<Cow<'a, str>>,
190 fee: Option<XRPAmount<'a>>,
191 last_ledger_sequence: Option<u32>,
192 memos: Option<Vec<Memo>>,
193 sequence: Option<u32>,
194 signers: Option<Vec<Signer>>,
195 source_tag: Option<u32>,
196 ticket_sequence: Option<u32>,
197 mptoken_issuance_id: Cow<'a, str>,
198 mpt_amount: Cow<'a, str>,
199 holder_encrypted_amount: Cow<'a, str>,
200 issuer_encrypted_amount: Cow<'a, str>,
201 blinding_factor: Cow<'a, str>,
202 holder_encryption_key: Option<Cow<'a, str>>,
203 auditor_encrypted_amount: Option<Cow<'a, str>>,
204 zk_proof: Option<Cow<'a, str>>,
205 ) -> Self {
206 Self {
207 common_fields: CommonFields::new(
208 account,
209 TransactionType::ConfidentialMPTConvert,
210 account_txn_id,
211 fee,
212 Some(FlagCollection::default()),
213 last_ledger_sequence,
214 memos,
215 None,
216 sequence,
217 signers,
218 None,
219 source_tag,
220 ticket_sequence,
221 None,
222 ),
223 mptoken_issuance_id,
224 mpt_amount,
225 holder_encrypted_amount,
226 issuer_encrypted_amount,
227 blinding_factor,
228 holder_encryption_key,
229 auditor_encrypted_amount,
230 zk_proof,
231 }
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use alloc::string::ToString;
238
239 use super::*;
240
241 #[test]
242 fn test_serialize_first_convert_with_registration() {
243 let tx = ConfidentialMPTConvert {
244 common_fields: CommonFields {
245 account: "rUserAccount11111111111111111111".into(),
246 transaction_type: TransactionType::ConfidentialMPTConvert,
247 ..Default::default()
248 },
249 mptoken_issuance_id: "610F33B8EBF7EC795F822A454FB852156AEFE50BE0CB8326338A81CD74801864"
250 .into(),
251 mpt_amount: "1000".into(),
252 holder_encrypted_amount: "AD3F".repeat(33).into(),
253 issuer_encrypted_amount: "BC2E".repeat(33).into(),
254 blinding_factor: "EE".repeat(32).into(),
255 holder_encryption_key: Some("03".to_string() + &"8d".repeat(32)).map(Into::into),
256 auditor_encrypted_amount: None,
257 zk_proof: Some("AB".repeat(64).into()),
258 };
259
260 let json = serde_json::to_string(&tx).unwrap();
261 assert!(json.contains("\"TransactionType\":\"ConfidentialMPTConvert\""));
262 assert!(json.contains("\"HolderEncryptionKey\""));
263 assert!(json.contains("\"ZKProof\""));
264
265 let round_tripped: ConfidentialMPTConvert = serde_json::from_str(&json).unwrap();
266 assert_eq!(round_tripped, tx);
267 }
268
269 #[test]
270 fn test_serialize_subsequent_convert_no_key() {
271 let tx = ConfidentialMPTConvert {
272 common_fields: CommonFields {
273 account: "rUserAccount11111111111111111111".into(),
274 transaction_type: TransactionType::ConfidentialMPTConvert,
275 ..Default::default()
276 },
277 mptoken_issuance_id: "610F33".repeat(4).into(),
278 mpt_amount: "500".into(),
279 holder_encrypted_amount: "AD3F".repeat(33).into(),
280 issuer_encrypted_amount: "BC2E".repeat(33).into(),
281 blinding_factor: "EE".repeat(32).into(),
282 holder_encryption_key: None,
283 auditor_encrypted_amount: None,
284 zk_proof: None,
285 };
286
287 let json = serde_json::to_string(&tx).unwrap();
288 assert!(!json.contains("\"HolderEncryptionKey\""));
290 assert!(!json.contains("\"ZKProof\""));
291 }
292
293 #[test]
294 fn test_new_builder_and_accessors() {
295 let mut tx = ConfidentialMPTConvert::new(
296 "rUserAccount11111111111111111111".into(),
297 None,
298 None,
299 None,
300 None,
301 None,
302 None,
303 None,
304 None,
305 "610F33".repeat(8).into(),
306 "1000".into(),
307 "AD3F".repeat(33).into(),
308 "BC2E".repeat(33).into(),
309 "EE".repeat(32).into(),
310 None,
311 None,
312 None,
313 )
314 .with_fee(XRPAmount::from("20000"))
315 .with_sequence(7);
316
317 assert_eq!(tx.get_common_fields().sequence, Some(7));
320 assert_eq!(tx.get_common_fields().fee, Some(XRPAmount::from("20000")));
321 assert_eq!(
322 tx.get_transaction_type(),
323 &TransactionType::ConfidentialMPTConvert
324 );
325 assert!(tx.get_errors().is_ok());
327
328 let common =
331 <ConfidentialMPTConvert as Transaction<'_, NoFlags>>::get_mut_common_fields(&mut tx);
332 assert_eq!(common.sequence, Some(7));
333 }
334
335 #[test]
336 fn test_serialize_with_auditor_mirror() {
337 let tx = ConfidentialMPTConvert {
338 common_fields: CommonFields {
339 account: "rUserAccount11111111111111111111".into(),
340 transaction_type: TransactionType::ConfidentialMPTConvert,
341 ..Default::default()
342 },
343 mptoken_issuance_id: "610F33".repeat(4).into(),
344 mpt_amount: "750".into(),
345 holder_encrypted_amount: "AD3F".repeat(33).into(),
346 issuer_encrypted_amount: "BC2E".repeat(33).into(),
347 blinding_factor: "EE".repeat(32).into(),
348 holder_encryption_key: None,
349 auditor_encrypted_amount: Some("CD".repeat(66).into()),
351 zk_proof: None,
352 };
353
354 let json = serde_json::to_string(&tx).unwrap();
355 assert!(json.contains("\"AuditorEncryptedAmount\""));
356
357 let round_tripped: ConfidentialMPTConvert = serde_json::from_str(&json).unwrap();
358 assert_eq!(round_tripped, tx);
359 }
360
361 const ACCT: &str = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh";
363 const ISS_OF_ACCT: &str = "00000001B5F762798A53D543A014CAF8B297CFF8F2F937E8";
365
366 fn valid_convert() -> ConfidentialMPTConvert<'static> {
367 ConfidentialMPTConvert {
368 common_fields: CommonFields {
369 account: ACCT.into(),
370 transaction_type: TransactionType::ConfidentialMPTConvert,
371 ..Default::default()
372 },
373 mptoken_issuance_id: "610F33".repeat(8).into(),
375 mpt_amount: "1000".into(),
376 holder_encrypted_amount: "AD3F".repeat(33).into(),
377 issuer_encrypted_amount: "BC2E".repeat(33).into(),
378 blinding_factor: "EE".repeat(32).into(),
379 holder_encryption_key: None,
380 auditor_encrypted_amount: None,
381 zk_proof: None,
382 }
383 }
384
385 #[test]
386 fn test_valid_convert_passes() {
387 assert!(valid_convert().get_errors().is_ok());
388 }
389
390 #[test]
391 fn test_zero_amount_convert_allowed() {
392 let mut tx = valid_convert();
394 tx.mpt_amount = "0".into();
395 assert!(tx.get_errors().is_ok());
396 }
397
398 #[test]
399 fn test_account_is_issuer_rejected() {
400 let mut tx = valid_convert();
401 tx.mptoken_issuance_id = ISS_OF_ACCT.into();
402 assert!(tx.get_errors().is_err());
403 }
404
405 #[test]
406 fn test_amount_above_mpt_max_rejected() {
407 let mut tx = valid_convert();
409 tx.mpt_amount = "9223372036854775808".into();
410 assert!(tx.get_errors().is_err());
411 }
412
413 #[test]
414 fn test_key_without_proof_rejected() {
415 let mut tx = valid_convert();
417 tx.holder_encryption_key = Some(("03".to_string() + &"8d".repeat(32)).into());
418 assert!(tx.get_errors().is_err());
419 }
420}