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 crate::core::addresscodec::decode_classic_address;
14
15use super::confidential_mpt_constants::{
16 address_is_issuer, validate_hex_length, CIPHERTEXT_LENGTH, COMMITMENT_LENGTH, SEND_PROOF_LENGTH,
17};
18use super::mptoken_issuance_set::validate_mptoken_issuance_id;
19use super::{validate_credential_ids, CommonFields, CommonTransactionBuilder};
20
21#[skip_serializing_none]
34#[derive(
35 Debug,
36 Default,
37 Serialize,
38 Deserialize,
39 PartialEq,
40 Eq,
41 Clone,
42 xrpl_rust_macros::ValidateCurrencies,
43)]
44#[serde(rename_all = "PascalCase")]
45pub struct ConfidentialMPTSend<'a> {
46 #[serde(flatten)]
47 pub common_fields: CommonFields<'a, NoFlags>,
48
49 pub destination: Cow<'a, str>,
51
52 pub destination_tag: Option<u32>,
55
56 #[serde(rename = "MPTokenIssuanceID")]
57 pub mptoken_issuance_id: Cow<'a, str>,
58
59 pub sender_encrypted_amount: Cow<'a, str>,
61
62 pub destination_encrypted_amount: Cow<'a, str>,
64
65 pub issuer_encrypted_amount: Cow<'a, str>,
68
69 pub amount_commitment: Cow<'a, str>,
71
72 pub balance_commitment: Cow<'a, str>,
74
75 #[serde(rename = "ZKProof")]
78 pub zk_proof: Cow<'a, str>,
79
80 pub auditor_encrypted_amount: Option<Cow<'a, str>>,
83
84 #[serde(rename = "CredentialIDs")]
87 pub credential_ids: Option<Vec<Cow<'a, str>>>,
88}
89
90impl<'a> Model for ConfidentialMPTSend<'a> {
91 fn get_errors(&self) -> crate::models::XRPLModelResult<()> {
92 self._get_destination_error()?;
93 self._get_field_length_errors()?;
94 self._get_issuer_role_error()?;
95 validate_credential_ids(&self.credential_ids)?;
96 self.validate_currencies()
97 }
98}
99
100impl<'a> ConfidentialMPTSend<'a> {
101 fn _get_destination_error(&self) -> crate::models::XRPLModelResult<()> {
103 if decode_classic_address(self.destination.as_ref()).is_err() {
104 return Err(XRPLModelException::InvalidValueFormat {
105 field: "destination".into(),
106 format: "classic XRPL address".into(),
107 found: self.destination.as_ref().into(),
108 });
109 }
110 if self.destination == self.common_fields.account {
111 return Err(XRPLModelException::ValueEqualsValue {
112 field1: "destination".into(),
113 field2: "account".into(),
114 });
115 }
116 Ok(())
117 }
118
119 fn _get_issuer_role_error(&self) -> crate::models::XRPLModelResult<()> {
124 let issuance_id = self.mptoken_issuance_id.as_ref();
125 if address_is_issuer(issuance_id, self.common_fields.account.as_ref()) {
126 return Err(XRPLModelException::ValueEqualsValue {
127 field1: "account".into(),
128 field2: "issuer".into(),
129 });
130 }
131 if address_is_issuer(issuance_id, self.destination.as_ref()) {
132 return Err(XRPLModelException::ValueEqualsValue {
133 field1: "destination".into(),
134 field2: "issuer".into(),
135 });
136 }
137 Ok(())
138 }
139
140 fn _get_field_length_errors(&self) -> crate::models::XRPLModelResult<()> {
143 validate_mptoken_issuance_id(self.mptoken_issuance_id.as_ref())?;
144 validate_hex_length(
145 "sender_encrypted_amount",
146 self.sender_encrypted_amount.as_ref(),
147 CIPHERTEXT_LENGTH,
148 )?;
149 validate_hex_length(
150 "destination_encrypted_amount",
151 self.destination_encrypted_amount.as_ref(),
152 CIPHERTEXT_LENGTH,
153 )?;
154 validate_hex_length(
155 "issuer_encrypted_amount",
156 self.issuer_encrypted_amount.as_ref(),
157 CIPHERTEXT_LENGTH,
158 )?;
159 if let Some(auditor) = self.auditor_encrypted_amount.as_deref() {
160 validate_hex_length("auditor_encrypted_amount", auditor, CIPHERTEXT_LENGTH)?;
161 }
162 validate_hex_length(
163 "amount_commitment",
164 self.amount_commitment.as_ref(),
165 COMMITMENT_LENGTH,
166 )?;
167 validate_hex_length(
168 "balance_commitment",
169 self.balance_commitment.as_ref(),
170 COMMITMENT_LENGTH,
171 )?;
172 validate_hex_length("zk_proof", self.zk_proof.as_ref(), SEND_PROOF_LENGTH)
173 }
174}
175
176impl<'a> Transaction<'a, NoFlags> for ConfidentialMPTSend<'a> {
177 fn get_transaction_type(&self) -> &TransactionType {
178 self.common_fields.get_transaction_type()
179 }
180
181 fn get_common_fields(&self) -> &CommonFields<'_, NoFlags> {
182 self.common_fields.get_common_fields()
183 }
184
185 fn get_mut_common_fields(&mut self) -> &mut CommonFields<'a, NoFlags> {
186 self.common_fields.get_mut_common_fields()
187 }
188}
189
190impl<'a> CommonTransactionBuilder<'a, NoFlags> for ConfidentialMPTSend<'a> {
191 fn get_mut_common_fields(&mut self) -> &mut CommonFields<'a, NoFlags> {
192 &mut self.common_fields
193 }
194
195 fn into_self(self) -> Self {
196 self
197 }
198}
199
200impl<'a> ConfidentialMPTSend<'a> {
201 #[allow(clippy::too_many_arguments)]
202 pub fn new(
203 account: Cow<'a, str>,
204 account_txn_id: Option<Cow<'a, str>>,
205 fee: Option<XRPAmount<'a>>,
206 last_ledger_sequence: Option<u32>,
207 memos: Option<Vec<Memo>>,
208 sequence: Option<u32>,
209 signers: Option<Vec<Signer>>,
210 source_tag: Option<u32>,
211 ticket_sequence: Option<u32>,
212 destination: Cow<'a, str>,
213 destination_tag: Option<u32>,
214 mptoken_issuance_id: Cow<'a, str>,
215 sender_encrypted_amount: Cow<'a, str>,
216 destination_encrypted_amount: Cow<'a, str>,
217 issuer_encrypted_amount: Cow<'a, str>,
218 amount_commitment: Cow<'a, str>,
219 balance_commitment: Cow<'a, str>,
220 zk_proof: Cow<'a, str>,
221 auditor_encrypted_amount: Option<Cow<'a, str>>,
222 credential_ids: Option<Vec<Cow<'a, str>>>,
223 ) -> Self {
224 Self {
225 common_fields: CommonFields::new(
226 account,
227 TransactionType::ConfidentialMPTSend,
228 account_txn_id,
229 fee,
230 Some(FlagCollection::default()),
231 last_ledger_sequence,
232 memos,
233 None,
234 sequence,
235 signers,
236 None,
237 source_tag,
238 ticket_sequence,
239 None,
240 ),
241 destination,
242 destination_tag,
243 mptoken_issuance_id,
244 sender_encrypted_amount,
245 destination_encrypted_amount,
246 issuer_encrypted_amount,
247 amount_commitment,
248 balance_commitment,
249 zk_proof,
250 auditor_encrypted_amount,
251 credential_ids,
252 }
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 #[test]
261 fn test_serialize() {
262 let tx = ConfidentialMPTSend {
263 common_fields: CommonFields {
264 account: "rSenderAccount11111111111111111".into(),
265 transaction_type: TransactionType::ConfidentialMPTSend,
266 ..Default::default()
267 },
268 destination: "rRecipientAccount111111111111".into(),
269 destination_tag: None,
270 mptoken_issuance_id: "610F33".repeat(8).into(),
271 sender_encrypted_amount: "AD".repeat(66).into(),
272 destination_encrypted_amount: "DF".repeat(66).into(),
273 issuer_encrypted_amount: "BC".repeat(66).into(),
274 amount_commitment: "04".repeat(33).into(),
275 balance_commitment: "03".repeat(33).into(),
276 zk_proof: "84".repeat(946).into(),
277 auditor_encrypted_amount: None,
278 credential_ids: None,
279 };
280
281 let json = serde_json::to_string(&tx).unwrap();
282 assert!(json.contains("\"TransactionType\":\"ConfidentialMPTSend\""));
283 assert!(json.contains("\"Destination\":\"rRecipientAccount"));
284 assert!(json.contains("\"AmountCommitment\""));
285 assert!(json.contains("\"BalanceCommitment\""));
286 assert!(json.contains("\"ZKProof\""));
287
288 let round_tripped: ConfidentialMPTSend = serde_json::from_str(&json).unwrap();
289 assert_eq!(round_tripped, tx);
290 }
291
292 #[test]
293 fn test_new_builder_and_accessors() {
294 let mut tx = ConfidentialMPTSend::new(
295 "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh".into(),
296 None,
297 None,
298 None,
299 None,
300 None,
301 None,
302 None,
303 None,
304 "rLSn6Z3T8uCxbcd1oxwfGQN1Fdn5CyGujK".into(),
305 None,
306 "610F33".repeat(8).into(),
307 "AD".repeat(66).into(),
308 "DF".repeat(66).into(),
309 "BC".repeat(66).into(),
310 "04".repeat(33).into(),
311 "03".repeat(33).into(),
312 "84".repeat(946).into(),
313 None,
314 None,
315 )
316 .with_fee(XRPAmount::from("15000"))
317 .with_sequence(9);
318
319 assert_eq!(tx.get_common_fields().sequence, Some(9));
320 assert_eq!(tx.get_common_fields().fee, Some(XRPAmount::from("15000")));
321 assert_eq!(
322 tx.get_transaction_type(),
323 &TransactionType::ConfidentialMPTSend
324 );
325 assert!(tx.get_errors().is_ok());
326
327 let common =
328 <ConfidentialMPTSend as Transaction<'_, NoFlags>>::get_mut_common_fields(&mut tx);
329 assert_eq!(common.sequence, Some(9));
330 }
331
332 const ACCT: &str = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh";
335 const DEST: &str = "rLSn6Z3T8uCxbcd1oxwfGQN1Fdn5CyGujK";
336 const ISS_OF_ACCT: &str = "00000001B5F762798A53D543A014CAF8B297CFF8F2F937E8";
338 const ISS_OF_DEST: &str = "00000001D528B62DC7AF16417C9F44AAD8C04D920A3A705F";
339
340 fn valid_send() -> ConfidentialMPTSend<'static> {
341 ConfidentialMPTSend {
342 common_fields: CommonFields {
343 account: ACCT.into(),
344 transaction_type: TransactionType::ConfidentialMPTSend,
345 ..Default::default()
346 },
347 destination: DEST.into(),
348 destination_tag: None,
349 mptoken_issuance_id: "610F33".repeat(8).into(),
351 sender_encrypted_amount: "AD".repeat(66).into(),
352 destination_encrypted_amount: "DF".repeat(66).into(),
353 issuer_encrypted_amount: "BC".repeat(66).into(),
354 amount_commitment: "04".repeat(33).into(),
355 balance_commitment: "03".repeat(33).into(),
356 zk_proof: "84".repeat(946).into(),
357 auditor_encrypted_amount: None,
358 credential_ids: None,
359 }
360 }
361
362 #[test]
363 fn test_valid_send_passes() {
364 assert!(valid_send().get_errors().is_ok());
365 }
366
367 #[test]
368 fn test_self_send_rejected() {
369 let mut tx = valid_send();
370 tx.destination = ACCT.into();
371 assert!(tx.get_errors().is_err());
372 }
373
374 #[test]
375 fn test_malformed_destination_rejected() {
376 let mut tx = valid_send();
377 tx.destination = "not_a_classic_address".into();
378 assert!(tx.get_errors().is_err());
379 }
380
381 #[test]
382 fn test_account_is_issuer_rejected() {
383 let mut tx = valid_send();
384 tx.mptoken_issuance_id = ISS_OF_ACCT.into();
385 assert!(tx.get_errors().is_err());
386 }
387
388 #[test]
389 fn test_destination_is_issuer_rejected() {
390 let mut tx = valid_send();
391 tx.mptoken_issuance_id = ISS_OF_DEST.into();
392 assert!(tx.get_errors().is_err());
393 }
394
395 #[test]
396 fn test_wrong_length_ciphertext_rejected() {
397 let mut tx = valid_send();
398 tx.sender_encrypted_amount = "AD".repeat(10).into();
399 assert!(tx.get_errors().is_err());
400 }
401}