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, COMMITMENT_LENGTH, CONVERT_BACK_PROOF_LENGTH,
16};
17use super::mptoken_issuance_set::validate_mptoken_issuance_id;
18use super::{CommonFields, CommonTransactionBuilder};
19
20#[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 ConfidentialMPTConvertBack<'a> {
41 #[serde(flatten)]
42 pub common_fields: CommonFields<'a, NoFlags>,
43
44 #[serde(rename = "MPTokenIssuanceID")]
45 pub mptoken_issuance_id: Cow<'a, str>,
46
47 #[serde(rename = "MPTAmount")]
49 pub mpt_amount: Cow<'a, str>,
50
51 pub holder_encrypted_amount: Cow<'a, str>,
53
54 pub issuer_encrypted_amount: Cow<'a, str>,
56
57 pub blinding_factor: Cow<'a, str>,
60
61 pub balance_commitment: Cow<'a, str>,
63
64 #[serde(rename = "ZKProof")]
66 pub zk_proof: Cow<'a, str>,
67
68 pub auditor_encrypted_amount: Option<Cow<'a, str>>,
71}
72
73impl<'a> Model for ConfidentialMPTConvertBack<'a> {
74 fn get_errors(&self) -> crate::models::XRPLModelResult<()> {
75 self._get_field_length_errors()?;
76 self._get_issuer_role_error()?;
77 self.validate_currencies()
78 }
79}
80
81impl<'a> ConfidentialMPTConvertBack<'a> {
82 fn _get_issuer_role_error(&self) -> crate::models::XRPLModelResult<()> {
86 if address_is_issuer(
87 self.mptoken_issuance_id.as_ref(),
88 self.common_fields.account.as_ref(),
89 ) {
90 return Err(XRPLModelException::ValueEqualsValue {
91 field1: "account".into(),
92 field2: "issuer".into(),
93 });
94 }
95 Ok(())
96 }
97
98 fn _get_field_length_errors(&self) -> crate::models::XRPLModelResult<()> {
99 validate_mptoken_issuance_id(self.mptoken_issuance_id.as_ref())?;
100 validate_mpt_amount("mpt_amount", self.mpt_amount.as_ref(), true)?;
102 validate_hex_length(
103 "holder_encrypted_amount",
104 self.holder_encrypted_amount.as_ref(),
105 CIPHERTEXT_LENGTH,
106 )?;
107 validate_hex_length(
108 "issuer_encrypted_amount",
109 self.issuer_encrypted_amount.as_ref(),
110 CIPHERTEXT_LENGTH,
111 )?;
112 if let Some(auditor) = self.auditor_encrypted_amount.as_deref() {
113 validate_hex_length("auditor_encrypted_amount", auditor, CIPHERTEXT_LENGTH)?;
114 }
115 validate_hex_length(
116 "blinding_factor",
117 self.blinding_factor.as_ref(),
118 BLINDING_FACTOR_LENGTH,
119 )?;
120 validate_hex_length(
121 "balance_commitment",
122 self.balance_commitment.as_ref(),
123 COMMITMENT_LENGTH,
124 )?;
125 validate_hex_length(
126 "zk_proof",
127 self.zk_proof.as_ref(),
128 CONVERT_BACK_PROOF_LENGTH,
129 )
130 }
131}
132
133impl<'a> Transaction<'a, NoFlags> for ConfidentialMPTConvertBack<'a> {
134 fn get_transaction_type(&self) -> &TransactionType {
135 self.common_fields.get_transaction_type()
136 }
137
138 fn get_common_fields(&self) -> &CommonFields<'_, NoFlags> {
139 self.common_fields.get_common_fields()
140 }
141
142 fn get_mut_common_fields(&mut self) -> &mut CommonFields<'a, NoFlags> {
143 self.common_fields.get_mut_common_fields()
144 }
145}
146
147impl<'a> CommonTransactionBuilder<'a, NoFlags> for ConfidentialMPTConvertBack<'a> {
148 fn get_mut_common_fields(&mut self) -> &mut CommonFields<'a, NoFlags> {
149 &mut self.common_fields
150 }
151
152 fn into_self(self) -> Self {
153 self
154 }
155}
156
157impl<'a> ConfidentialMPTConvertBack<'a> {
158 #[allow(clippy::too_many_arguments)]
159 pub fn new(
160 account: Cow<'a, str>,
161 account_txn_id: Option<Cow<'a, str>>,
162 fee: Option<XRPAmount<'a>>,
163 last_ledger_sequence: Option<u32>,
164 memos: Option<Vec<Memo>>,
165 sequence: Option<u32>,
166 signers: Option<Vec<Signer>>,
167 source_tag: Option<u32>,
168 ticket_sequence: Option<u32>,
169 mptoken_issuance_id: Cow<'a, str>,
170 mpt_amount: Cow<'a, str>,
171 holder_encrypted_amount: Cow<'a, str>,
172 issuer_encrypted_amount: Cow<'a, str>,
173 blinding_factor: Cow<'a, str>,
174 balance_commitment: Cow<'a, str>,
175 zk_proof: Cow<'a, str>,
176 auditor_encrypted_amount: Option<Cow<'a, str>>,
177 ) -> Self {
178 Self {
179 common_fields: CommonFields::new(
180 account,
181 TransactionType::ConfidentialMPTConvertBack,
182 account_txn_id,
183 fee,
184 Some(FlagCollection::default()),
185 last_ledger_sequence,
186 memos,
187 None,
188 sequence,
189 signers,
190 None,
191 source_tag,
192 ticket_sequence,
193 None,
194 ),
195 mptoken_issuance_id,
196 mpt_amount,
197 holder_encrypted_amount,
198 issuer_encrypted_amount,
199 blinding_factor,
200 balance_commitment,
201 zk_proof,
202 auditor_encrypted_amount,
203 }
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 #[test]
212 fn test_serialize() {
213 let tx = ConfidentialMPTConvertBack {
214 common_fields: CommonFields {
215 account: "rUserAccount11111111111111111111".into(),
216 transaction_type: TransactionType::ConfidentialMPTConvertBack,
217 ..Default::default()
218 },
219 mptoken_issuance_id: "610F33".repeat(8).into(),
220 mpt_amount: "500".into(),
221 holder_encrypted_amount: "AD".repeat(66).into(),
222 issuer_encrypted_amount: "BC".repeat(66).into(),
223 blinding_factor: "12".repeat(32).into(),
224 balance_commitment: "03".repeat(33).into(),
225 zk_proof: "AB".repeat(816).into(),
226 auditor_encrypted_amount: None,
227 };
228
229 let json = serde_json::to_string(&tx).unwrap();
230 assert!(json.contains("\"TransactionType\":\"ConfidentialMPTConvertBack\""));
231 assert!(json.contains("\"BalanceCommitment\""));
232
233 let round_tripped: ConfidentialMPTConvertBack = serde_json::from_str(&json).unwrap();
234 assert_eq!(round_tripped, tx);
235 }
236
237 #[test]
238 fn test_new_builder_and_accessors() {
239 let mut tx = ConfidentialMPTConvertBack::new(
240 "rUserAccount11111111111111111111".into(),
241 None,
242 None,
243 None,
244 None,
245 None,
246 None,
247 None,
248 None,
249 "610F33".repeat(8).into(),
250 "500".into(),
251 "AD".repeat(66).into(),
252 "BC".repeat(66).into(),
253 "12".repeat(32).into(),
254 "03".repeat(33).into(),
255 "AB".repeat(816).into(),
256 None,
257 )
258 .with_fee(XRPAmount::from("15000"))
259 .with_sequence(9);
260
261 assert_eq!(tx.get_common_fields().sequence, Some(9));
262 assert_eq!(tx.get_common_fields().fee, Some(XRPAmount::from("15000")));
263 assert_eq!(
264 tx.get_transaction_type(),
265 &TransactionType::ConfidentialMPTConvertBack
266 );
267 assert!(tx.get_errors().is_ok());
268
269 let common =
270 <ConfidentialMPTConvertBack as Transaction<'_, NoFlags>>::get_mut_common_fields(
271 &mut tx,
272 );
273 assert_eq!(common.sequence, Some(9));
274 }
275
276 const ACCT: &str = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh";
278 const ISS_OF_ACCT: &str = "00000001B5F762798A53D543A014CAF8B297CFF8F2F937E8";
280
281 fn valid_convert_back() -> ConfidentialMPTConvertBack<'static> {
282 ConfidentialMPTConvertBack {
283 common_fields: CommonFields {
284 account: ACCT.into(),
285 transaction_type: TransactionType::ConfidentialMPTConvertBack,
286 ..Default::default()
287 },
288 mptoken_issuance_id: "610F33".repeat(8).into(),
290 mpt_amount: "500".into(),
291 holder_encrypted_amount: "AD".repeat(66).into(),
292 issuer_encrypted_amount: "BC".repeat(66).into(),
293 blinding_factor: "12".repeat(32).into(),
294 balance_commitment: "03".repeat(33).into(),
295 zk_proof: "AB".repeat(816).into(),
296 auditor_encrypted_amount: None,
297 }
298 }
299
300 #[test]
301 fn test_valid_convert_back_passes() {
302 assert!(valid_convert_back().get_errors().is_ok());
303 }
304
305 #[test]
306 fn test_zero_amount_convert_back_rejected() {
307 let mut tx = valid_convert_back();
309 tx.mpt_amount = "0".into();
310 assert!(tx.get_errors().is_err());
311 }
312
313 #[test]
314 fn test_account_is_issuer_rejected() {
315 let mut tx = valid_convert_back();
316 tx.mptoken_issuance_id = ISS_OF_ACCT.into();
317 assert!(tx.get_errors().is_err());
318 }
319
320 #[test]
321 fn test_amount_above_mpt_max_rejected() {
322 let mut tx = valid_convert_back();
323 tx.mpt_amount = "9223372036854775808".into();
324 assert!(tx.get_errors().is_err());
325 }
326}