use alloc::borrow::Cow;
use alloc::vec::Vec;
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use serde_with::skip_serializing_none;
use strum_macros::{AsRefStr, Display, EnumIter};
use crate::models::{
amount::Amount,
transactions::{Memo, Signer, Transaction, TransactionType},
Model, PathStep, ValidateCurrencies, XRPLModelResult,
};
use crate::models::amount::XRPAmount;
use crate::models::transactions::exceptions::XRPLPaymentException;
use super::{CommonFields, CommonTransactionBuilder, FlagCollection};
#[derive(
Default,
Debug,
Eq,
PartialEq,
Clone,
Copy,
Serialize_repr,
Deserialize_repr,
Display,
AsRefStr,
EnumIter,
)]
#[repr(u32)]
pub enum PaymentFlag {
TfNoDirectRipple = 0x00010000,
TfPartialPayment = 0x00020000,
#[default]
TfLimitQuality = 0x00040000,
}
#[skip_serializing_none]
#[derive(
Debug,
Default,
Serialize,
Deserialize,
PartialEq,
Eq,
Clone,
xrpl_rust_macros::ValidateCurrencies,
)]
#[serde(rename_all = "PascalCase")]
pub struct Payment<'a> {
#[serde(flatten)]
pub common_fields: CommonFields<'a, PaymentFlag>,
pub amount: Amount<'a>,
pub destination: Cow<'a, str>,
pub destination_tag: Option<u32>,
pub invoice_id: Option<u32>,
pub paths: Option<Vec<Vec<PathStep<'a>>>>,
pub send_max: Option<Amount<'a>>,
pub deliver_min: Option<Amount<'a>>,
}
impl<'a: 'static> Model for Payment<'a> {
fn get_errors(&self) -> XRPLModelResult<()> {
self._get_xrp_transaction_error()?;
self._get_partial_payment_error()?;
self._get_exchange_error()?;
self.validate_currencies()
}
}
impl<'a> Transaction<'a, PaymentFlag> for Payment<'a> {
fn has_flag(&self, flag: &PaymentFlag) -> bool {
self.common_fields.has_flag(flag)
}
fn get_transaction_type(&self) -> &TransactionType {
self.common_fields.get_transaction_type()
}
fn get_common_fields(&self) -> &CommonFields<'_, PaymentFlag> {
self.common_fields.get_common_fields()
}
fn get_mut_common_fields(&mut self) -> &mut CommonFields<'a, PaymentFlag> {
self.common_fields.get_mut_common_fields()
}
}
impl<'a> CommonTransactionBuilder<'a, PaymentFlag> for Payment<'a> {
fn get_mut_common_fields(&mut self) -> &mut CommonFields<'a, PaymentFlag> {
&mut self.common_fields
}
fn into_self(self) -> Self {
self
}
}
impl<'a> PaymentError for Payment<'a> {
fn _get_xrp_transaction_error(&self) -> XRPLModelResult<()> {
if self.amount.is_xrp() && self.send_max.is_none() {
if self.paths.is_some() {
Err(XRPLPaymentException::IllegalOption {
field: "paths".into(),
context: "XRP to XRP payments".into(),
}
.into())
} else if self.common_fields.account == self.destination {
Err(XRPLPaymentException::ValueEqualsValueInContext {
field1: "account".into(),
field2: "destination".into(),
context: "XRP to XRP Payments".into(),
}
.into())
} else {
Ok(())
}
} else {
Ok(())
}
}
fn _get_partial_payment_error(&self) -> XRPLModelResult<()> {
if let Some(send_max) = &self.send_max {
if !self.has_flag(&PaymentFlag::TfPartialPayment)
&& send_max.is_xrp()
&& self.amount.is_xrp()
{
Err(XRPLPaymentException::IllegalOption {
field: "send_max".into(),
context: "XRP to XRP non-partial payments".into(),
}
.into())
} else {
Ok(())
}
} else if self.has_flag(&PaymentFlag::TfPartialPayment) {
Err(XRPLPaymentException::FlagRequiresField {
flag: PaymentFlag::TfPartialPayment,
field: "send_max".into(),
}
.into())
} else if !self.has_flag(&PaymentFlag::TfPartialPayment) {
if let Some(_deliver_min) = &self.deliver_min {
Err(XRPLPaymentException::IllegalOption {
field: "deliver_min".into(),
context: "XRP to XRP non-partial payments".into(),
}
.into())
} else {
Ok(())
}
} else {
Ok(())
}
}
fn _get_exchange_error(&self) -> XRPLModelResult<()> {
if self.common_fields.account == self.destination && self.send_max.is_none() {
return Err(XRPLPaymentException::OptionRequired {
field: "send_max".into(),
context: "exchanges".into(),
}
.into());
}
Ok(())
}
}
impl<'a> Payment<'a> {
pub fn new(
account: Cow<'a, str>,
account_txn_id: Option<Cow<'a, str>>,
fee: Option<XRPAmount<'a>>,
flags: Option<FlagCollection<PaymentFlag>>,
last_ledger_sequence: Option<u32>,
memos: Option<Vec<Memo>>,
sequence: Option<u32>,
signers: Option<Vec<Signer>>,
source_tag: Option<u32>,
ticket_sequence: Option<u32>,
amount: Amount<'a>,
destination: Cow<'a, str>,
deliver_min: Option<Amount<'a>>,
destination_tag: Option<u32>,
invoice_id: Option<u32>,
paths: Option<Vec<Vec<PathStep<'a>>>>,
send_max: Option<Amount<'a>>,
) -> Self {
Self {
common_fields: CommonFields::new(
account,
TransactionType::Payment,
account_txn_id,
fee,
Some(flags.unwrap_or_default()),
last_ledger_sequence,
memos,
None,
sequence,
signers,
None,
source_tag,
ticket_sequence,
None,
),
amount,
destination,
destination_tag,
invoice_id,
paths,
send_max,
deliver_min,
}
}
pub fn with_destination_tag(mut self, tag: u32) -> Self {
self.destination_tag = Some(tag);
self
}
pub fn with_invoice_id(mut self, invoice_id: u32) -> Self {
self.invoice_id = Some(invoice_id);
self
}
pub fn with_send_max(mut self, send_max: Amount<'a>) -> Self {
self.send_max = Some(send_max);
self
}
pub fn with_deliver_min(mut self, deliver_min: Amount<'a>) -> Self {
self.deliver_min = Some(deliver_min);
self
}
pub fn with_paths(mut self, paths: Vec<Vec<PathStep<'a>>>) -> Self {
self.paths = Some(paths);
self
}
pub fn add_path(mut self, path: Vec<PathStep<'a>>) -> Self {
match &mut self.paths {
Some(paths) => paths.push(path),
None => self.paths = Some(alloc::vec![path]),
}
self
}
pub fn with_flag(mut self, flag: PaymentFlag) -> Self {
self.common_fields.flags.0.push(flag);
self
}
pub fn with_flags(mut self, flags: Vec<PaymentFlag>) -> Self {
self.common_fields.flags = flags.into();
self
}
}
pub trait PaymentError {
fn _get_xrp_transaction_error(&self) -> XRPLModelResult<()>;
fn _get_partial_payment_error(&self) -> XRPLModelResult<()>;
fn _get_exchange_error(&self) -> XRPLModelResult<()>;
}
#[cfg(test)]
mod tests {
use alloc::string::ToString;
use alloc::vec;
use crate::models::amount::{Amount, IssuedCurrencyAmount, XRPAmount};
use crate::models::{Model, PathStep};
use crate::{
asynch::{exceptions::XRPLHelperResult, transaction::sign},
models::transactions::Transaction,
wallet::Wallet,
};
use super::*;
#[cfg(all(feature = "helpers", feature = "wallet"))]
#[test]
fn test_payment_sign_with_memo() -> XRPLHelperResult<()> {
let mut payment = Payment {
common_fields: CommonFields {
account: "rU4EE1FskCPJw5QkLx1iGgdWiJa6HeqYyb".into(),
transaction_type: TransactionType::Payment,
memos: Some(vec![Memo {
memo_data: Some("68656c6c6f".into()),
memo_format: None,
memo_type: Some("74657874".into()),
}]),
..Default::default()
},
amount: Amount::XRPAmount("1000000".into()),
destination: "rLSn6Z3T8uCxbcd1oxwfGQN1Fdn5CyGujK".into(),
..Default::default()
};
let wallet = Wallet::create(None)?;
sign(&mut payment, &wallet, false)?;
assert!(payment.get_common_fields().is_signed());
Ok(())
}
#[test]
fn test_xrp_to_xrp_error() {
let mut payment = Payment {
common_fields: CommonFields {
account: "rU4EE1FskCPJw5QkLx1iGgdWiJa6HeqYyb".into(),
transaction_type: TransactionType::Payment,
..Default::default()
},
amount: Amount::XRPAmount(XRPAmount::from("1000000")),
destination: "rLSn6Z3T8uCxbcd1oxwfGQN1Fdn5CyGujK".into(),
paths: Some(vec![vec![
PathStep::default().with_account("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B".into())
]]),
..Default::default()
};
assert_eq!(
payment.validate().unwrap_err().to_string().as_str(),
"The optional field `\"paths\"` is not allowed to be defined for \"XRP to XRP payments\""
);
payment.paths = None;
payment.send_max = Some(Amount::XRPAmount(XRPAmount::from("99999")));
assert_eq!(
payment.validate().unwrap_err().to_string().as_str(),
"The optional field `\"send_max\"` is not allowed to be defined for \"XRP to XRP non-partial payments\""
);
payment.send_max = None;
payment.destination = "rU4EE1FskCPJw5QkLx1iGgdWiJa6HeqYyb".into();
assert_eq!(
payment.validate().unwrap_err().to_string().as_str(),
"The value of the field `\"account\"` is not allowed to be the same as the value of the field `\"destination\"`, for \"XRP to XRP Payments\""
);
}
#[test]
fn test_partial_payments_error() {
let payment = Payment {
common_fields: CommonFields {
account: "rU4EE1FskCPJw5QkLx1iGgdWiJa6HeqYyb".into(),
transaction_type: TransactionType::Payment,
flags: vec![PaymentFlag::TfPartialPayment].into(),
..Default::default()
},
amount: Amount::XRPAmount("1000000".into()),
destination: "rLSn6Z3T8uCxbcd1oxwfGQN1Fdn5CyGujK".into(),
..Default::default()
};
assert_eq!(
payment.validate().unwrap_err().to_string().as_str(),
"For the flag `TfPartialPayment` to be set it is required to define the field `\"send_max\"`"
);
let payment = Payment {
common_fields: CommonFields {
account: "rU4EE1FskCPJw5QkLx1iGgdWiJa6HeqYyb".into(),
transaction_type: TransactionType::Payment,
..Default::default()
},
amount: Amount::XRPAmount("1000000".into()),
destination: "rLSn6Z3T8uCxbcd1oxwfGQN1Fdn5CyGujK".into(),
deliver_min: Some(Amount::XRPAmount("99999".into())),
..Default::default()
};
assert_eq!(
payment.validate().unwrap_err().to_string().as_str(),
"The optional field `\"deliver_min\"` is not allowed to be defined for \"XRP to XRP non-partial payments\""
);
}
#[test]
fn test_exchange_error() {
let payment = Payment {
common_fields: CommonFields {
account: "rU4EE1FskCPJw5QkLx1iGgdWiJa6HeqYyb".into(),
transaction_type: TransactionType::Payment,
..Default::default()
},
amount: Amount::IssuedCurrencyAmount(IssuedCurrencyAmount::new(
"USD".into(),
"rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B".into(),
"10".into(),
)),
destination: "rU4EE1FskCPJw5QkLx1iGgdWiJa6HeqYyb".into(),
..Default::default()
};
assert_eq!(
payment.validate().unwrap_err().to_string().as_str(),
"The optional field `\"send_max\"` is required to be defined for \"exchanges\""
);
}
#[test]
fn test_serde() {
let default_txn = Payment {
common_fields: CommonFields {
account: "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn".into(),
transaction_type: TransactionType::Payment,
fee: Some("12".into()),
flags: vec![PaymentFlag::TfPartialPayment].into(),
sequence: Some(2),
signing_pub_key: Some("".into()),
..Default::default()
},
amount: Amount::IssuedCurrencyAmount(IssuedCurrencyAmount::new(
"USD".into(),
"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn".into(),
"1".into(),
)),
destination: "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX".into(),
..Default::default()
};
let default_json_str = r#"{"Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn","TransactionType":"Payment","Fee":"12","Flags":131072,"Sequence":2,"SigningPubKey":"","Amount":{"currency":"USD","issuer":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn","value":"1"},"Destination":"ra5nK24KXen9AHvsdFTKHSANinZseWnPcX"}"#;
let default_json_value = serde_json::to_value(default_json_str).unwrap();
let serialized_string = serde_json::to_string(&default_txn).unwrap();
let serialized_value = serde_json::to_value(&serialized_string).unwrap();
assert_eq!(serialized_value, default_json_value);
let deserialized: Payment = serde_json::from_str(default_json_str).unwrap();
assert_eq!(default_txn, deserialized);
}
#[test]
fn test_builder_pattern() {
let payment = Payment {
common_fields: CommonFields {
account: "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn".into(),
transaction_type: TransactionType::Payment,
..Default::default()
},
amount: Amount::XRPAmount("1000000".into()),
destination: "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX".into(),
..Default::default()
}
.with_destination_tag(12345)
.with_send_max(Amount::XRPAmount("1100000".into()))
.with_flag(PaymentFlag::TfPartialPayment)
.with_fee("12".into())
.with_sequence(2)
.with_last_ledger_sequence(7108682)
.with_source_tag(54321);
assert_eq!(payment.destination_tag, Some(12345));
assert!(payment.send_max.is_some());
assert!(payment.has_flag(&PaymentFlag::TfPartialPayment));
assert_eq!(payment.common_fields.fee.as_ref().unwrap().0, "12");
assert_eq!(payment.common_fields.sequence, Some(2));
assert_eq!(payment.common_fields.last_ledger_sequence, Some(7108682));
assert_eq!(payment.common_fields.source_tag, Some(54321));
}
#[test]
fn test_cross_currency_payment() {
let payment = Payment {
common_fields: CommonFields {
account: "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn".into(),
transaction_type: TransactionType::Payment,
..Default::default()
},
amount: Amount::IssuedCurrencyAmount(IssuedCurrencyAmount::new(
"USD".into(),
"rhub8VRN55s94qWKDv6jmDy1pUykJzF3wq".into(),
"100".into(),
)),
destination: "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX".into(),
..Default::default()
}
.with_send_max(Amount::XRPAmount("110000000".into())) .with_destination_tag(987654)
.with_fee("12".into());
assert!(payment.send_max.is_some());
assert_eq!(payment.destination_tag, Some(987654));
assert!(payment.validate().is_ok());
}
#[test]
fn test_partial_payment() {
let payment = Payment {
common_fields: CommonFields {
account: "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn".into(),
transaction_type: TransactionType::Payment,
..Default::default()
},
amount: Amount::XRPAmount("1000000".into()),
destination: "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX".into(),
..Default::default()
}
.with_send_max(Amount::XRPAmount("1100000".into()))
.with_deliver_min(Amount::XRPAmount("900000".into()))
.with_flag(PaymentFlag::TfPartialPayment)
.with_fee("12".into());
assert!(payment.has_flag(&PaymentFlag::TfPartialPayment));
assert!(payment.send_max.is_some());
assert!(payment.deliver_min.is_some());
assert!(payment.validate().is_ok());
}
#[test]
fn test_path_building() {
let path1 = vec![
PathStep::default().with_account("rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh".into()),
PathStep::default().with_currency("USD".into()),
];
let path2 = vec![
PathStep::default().with_currency("EUR".into()),
PathStep::default().with_issuer("rhub8VRN55s94qWKDv6jmDy1pUykJzF3wq".into()),
];
let payment = Payment {
common_fields: CommonFields {
account: "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn".into(),
transaction_type: TransactionType::Payment,
..Default::default()
},
amount: Amount::IssuedCurrencyAmount(IssuedCurrencyAmount::new(
"USD".into(),
"rhub8VRN55s94qWKDv6jmDy1pUykJzF3wq".into(),
"100".into(),
)),
destination: "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX".into(),
..Default::default()
}
.add_path(path1)
.add_path(path2)
.with_send_max(Amount::XRPAmount("110000000".into()))
.with_fee("12".into());
assert_eq!(payment.paths.as_ref().unwrap().len(), 2);
assert!(payment.validate().is_ok());
}
}