use alloc::borrow::Cow;
use alloc::vec::Vec;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use crate::models::amount::XRPAmount;
use crate::models::{
transactions::{Memo, Signer, Transaction, TransactionType},
Model, ValidateCurrencies, XRPLModelException,
};
use crate::models::{FlagCollection, NoFlags};
use crate::core::addresscodec::decode_classic_address;
use super::confidential_mpt_constants::{
address_is_issuer, validate_hex_length, CIPHERTEXT_LENGTH, COMMITMENT_LENGTH, SEND_PROOF_LENGTH,
};
use super::mptoken_issuance_set::validate_mptoken_issuance_id;
use super::{validate_credential_ids, CommonFields, CommonTransactionBuilder};
#[skip_serializing_none]
#[derive(
Debug,
Default,
Serialize,
Deserialize,
PartialEq,
Eq,
Clone,
xrpl_rust_macros::ValidateCurrencies,
)]
#[serde(rename_all = "PascalCase")]
pub struct ConfidentialMPTSend<'a> {
#[serde(flatten)]
pub common_fields: CommonFields<'a, NoFlags>,
pub destination: Cow<'a, str>,
pub destination_tag: Option<u32>,
#[serde(rename = "MPTokenIssuanceID")]
pub mptoken_issuance_id: Cow<'a, str>,
pub sender_encrypted_amount: Cow<'a, str>,
pub destination_encrypted_amount: Cow<'a, str>,
pub issuer_encrypted_amount: Cow<'a, str>,
pub amount_commitment: Cow<'a, str>,
pub balance_commitment: Cow<'a, str>,
#[serde(rename = "ZKProof")]
pub zk_proof: Cow<'a, str>,
pub auditor_encrypted_amount: Option<Cow<'a, str>>,
#[serde(rename = "CredentialIDs")]
pub credential_ids: Option<Vec<Cow<'a, str>>>,
}
impl<'a> Model for ConfidentialMPTSend<'a> {
fn get_errors(&self) -> crate::models::XRPLModelResult<()> {
self._get_destination_error()?;
self._get_field_length_errors()?;
self._get_issuer_role_error()?;
validate_credential_ids(&self.credential_ids)?;
self.validate_currencies()
}
}
impl<'a> ConfidentialMPTSend<'a> {
fn _get_destination_error(&self) -> crate::models::XRPLModelResult<()> {
if decode_classic_address(self.destination.as_ref()).is_err() {
return Err(XRPLModelException::InvalidValueFormat {
field: "destination".into(),
format: "classic XRPL address".into(),
found: self.destination.as_ref().into(),
});
}
if self.destination == self.common_fields.account {
return Err(XRPLModelException::ValueEqualsValue {
field1: "destination".into(),
field2: "account".into(),
});
}
Ok(())
}
fn _get_issuer_role_error(&self) -> crate::models::XRPLModelResult<()> {
let issuance_id = self.mptoken_issuance_id.as_ref();
if address_is_issuer(issuance_id, self.common_fields.account.as_ref()) {
return Err(XRPLModelException::ValueEqualsValue {
field1: "account".into(),
field2: "issuer".into(),
});
}
if address_is_issuer(issuance_id, self.destination.as_ref()) {
return Err(XRPLModelException::ValueEqualsValue {
field1: "destination".into(),
field2: "issuer".into(),
});
}
Ok(())
}
fn _get_field_length_errors(&self) -> crate::models::XRPLModelResult<()> {
validate_mptoken_issuance_id(self.mptoken_issuance_id.as_ref())?;
validate_hex_length(
"sender_encrypted_amount",
self.sender_encrypted_amount.as_ref(),
CIPHERTEXT_LENGTH,
)?;
validate_hex_length(
"destination_encrypted_amount",
self.destination_encrypted_amount.as_ref(),
CIPHERTEXT_LENGTH,
)?;
validate_hex_length(
"issuer_encrypted_amount",
self.issuer_encrypted_amount.as_ref(),
CIPHERTEXT_LENGTH,
)?;
if let Some(auditor) = self.auditor_encrypted_amount.as_deref() {
validate_hex_length("auditor_encrypted_amount", auditor, CIPHERTEXT_LENGTH)?;
}
validate_hex_length(
"amount_commitment",
self.amount_commitment.as_ref(),
COMMITMENT_LENGTH,
)?;
validate_hex_length(
"balance_commitment",
self.balance_commitment.as_ref(),
COMMITMENT_LENGTH,
)?;
validate_hex_length("zk_proof", self.zk_proof.as_ref(), SEND_PROOF_LENGTH)
}
}
impl<'a> Transaction<'a, NoFlags> for ConfidentialMPTSend<'a> {
fn get_transaction_type(&self) -> &TransactionType {
self.common_fields.get_transaction_type()
}
fn get_common_fields(&self) -> &CommonFields<'_, NoFlags> {
self.common_fields.get_common_fields()
}
fn get_mut_common_fields(&mut self) -> &mut CommonFields<'a, NoFlags> {
self.common_fields.get_mut_common_fields()
}
}
impl<'a> CommonTransactionBuilder<'a, NoFlags> for ConfidentialMPTSend<'a> {
fn get_mut_common_fields(&mut self) -> &mut CommonFields<'a, NoFlags> {
&mut self.common_fields
}
fn into_self(self) -> Self {
self
}
}
impl<'a> ConfidentialMPTSend<'a> {
#[allow(clippy::too_many_arguments)]
pub fn new(
account: Cow<'a, str>,
account_txn_id: Option<Cow<'a, str>>,
fee: Option<XRPAmount<'a>>,
last_ledger_sequence: Option<u32>,
memos: Option<Vec<Memo>>,
sequence: Option<u32>,
signers: Option<Vec<Signer>>,
source_tag: Option<u32>,
ticket_sequence: Option<u32>,
destination: Cow<'a, str>,
destination_tag: Option<u32>,
mptoken_issuance_id: Cow<'a, str>,
sender_encrypted_amount: Cow<'a, str>,
destination_encrypted_amount: Cow<'a, str>,
issuer_encrypted_amount: Cow<'a, str>,
amount_commitment: Cow<'a, str>,
balance_commitment: Cow<'a, str>,
zk_proof: Cow<'a, str>,
auditor_encrypted_amount: Option<Cow<'a, str>>,
credential_ids: Option<Vec<Cow<'a, str>>>,
) -> Self {
Self {
common_fields: CommonFields::new(
account,
TransactionType::ConfidentialMPTSend,
account_txn_id,
fee,
Some(FlagCollection::default()),
last_ledger_sequence,
memos,
None,
sequence,
signers,
None,
source_tag,
ticket_sequence,
None,
),
destination,
destination_tag,
mptoken_issuance_id,
sender_encrypted_amount,
destination_encrypted_amount,
issuer_encrypted_amount,
amount_commitment,
balance_commitment,
zk_proof,
auditor_encrypted_amount,
credential_ids,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_serialize() {
let tx = ConfidentialMPTSend {
common_fields: CommonFields {
account: "rSenderAccount11111111111111111".into(),
transaction_type: TransactionType::ConfidentialMPTSend,
..Default::default()
},
destination: "rRecipientAccount111111111111".into(),
destination_tag: None,
mptoken_issuance_id: "610F33".repeat(8).into(),
sender_encrypted_amount: "AD".repeat(66).into(),
destination_encrypted_amount: "DF".repeat(66).into(),
issuer_encrypted_amount: "BC".repeat(66).into(),
amount_commitment: "04".repeat(33).into(),
balance_commitment: "03".repeat(33).into(),
zk_proof: "84".repeat(946).into(),
auditor_encrypted_amount: None,
credential_ids: None,
};
let json = serde_json::to_string(&tx).unwrap();
assert!(json.contains("\"TransactionType\":\"ConfidentialMPTSend\""));
assert!(json.contains("\"Destination\":\"rRecipientAccount"));
assert!(json.contains("\"AmountCommitment\""));
assert!(json.contains("\"BalanceCommitment\""));
assert!(json.contains("\"ZKProof\""));
let round_tripped: ConfidentialMPTSend = serde_json::from_str(&json).unwrap();
assert_eq!(round_tripped, tx);
}
#[test]
fn test_new_builder_and_accessors() {
let mut tx = ConfidentialMPTSend::new(
"rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh".into(),
None,
None,
None,
None,
None,
None,
None,
None,
"rLSn6Z3T8uCxbcd1oxwfGQN1Fdn5CyGujK".into(),
None,
"610F33".repeat(8).into(),
"AD".repeat(66).into(),
"DF".repeat(66).into(),
"BC".repeat(66).into(),
"04".repeat(33).into(),
"03".repeat(33).into(),
"84".repeat(946).into(),
None,
None,
)
.with_fee(XRPAmount::from("15000"))
.with_sequence(9);
assert_eq!(tx.get_common_fields().sequence, Some(9));
assert_eq!(tx.get_common_fields().fee, Some(XRPAmount::from("15000")));
assert_eq!(
tx.get_transaction_type(),
&TransactionType::ConfidentialMPTSend
);
assert!(tx.get_errors().is_ok());
let common =
<ConfidentialMPTSend as Transaction<'_, NoFlags>>::get_mut_common_fields(&mut tx);
assert_eq!(common.sequence, Some(9));
}
const ACCT: &str = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh";
const DEST: &str = "rLSn6Z3T8uCxbcd1oxwfGQN1Fdn5CyGujK";
const ISS_OF_ACCT: &str = "00000001B5F762798A53D543A014CAF8B297CFF8F2F937E8";
const ISS_OF_DEST: &str = "00000001D528B62DC7AF16417C9F44AAD8C04D920A3A705F";
fn valid_send() -> ConfidentialMPTSend<'static> {
ConfidentialMPTSend {
common_fields: CommonFields {
account: ACCT.into(),
transaction_type: TransactionType::ConfidentialMPTSend,
..Default::default()
},
destination: DEST.into(),
destination_tag: None,
mptoken_issuance_id: "610F33".repeat(8).into(),
sender_encrypted_amount: "AD".repeat(66).into(),
destination_encrypted_amount: "DF".repeat(66).into(),
issuer_encrypted_amount: "BC".repeat(66).into(),
amount_commitment: "04".repeat(33).into(),
balance_commitment: "03".repeat(33).into(),
zk_proof: "84".repeat(946).into(),
auditor_encrypted_amount: None,
credential_ids: None,
}
}
#[test]
fn test_valid_send_passes() {
assert!(valid_send().get_errors().is_ok());
}
#[test]
fn test_self_send_rejected() {
let mut tx = valid_send();
tx.destination = ACCT.into();
assert!(tx.get_errors().is_err());
}
#[test]
fn test_malformed_destination_rejected() {
let mut tx = valid_send();
tx.destination = "not_a_classic_address".into();
assert!(tx.get_errors().is_err());
}
#[test]
fn test_account_is_issuer_rejected() {
let mut tx = valid_send();
tx.mptoken_issuance_id = ISS_OF_ACCT.into();
assert!(tx.get_errors().is_err());
}
#[test]
fn test_destination_is_issuer_rejected() {
let mut tx = valid_send();
tx.mptoken_issuance_id = ISS_OF_DEST.into();
assert!(tx.get_errors().is_err());
}
#[test]
fn test_wrong_length_ciphertext_rejected() {
let mut tx = valid_send();
tx.sender_encrypted_amount = "AD".repeat(10).into();
assert!(tx.get_errors().is_err());
}
}