diff --git a/CHANGELOG.md b/CHANGELOG.md index c5c17562..69f56051 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [[Unreleased]] +- missing network_id member added to server info response +- server_state_duration_us in server info type changed to str + ## [[v0.5.0]] - add missing NFT request models diff --git a/Cargo.toml b/Cargo.toml index a1c03a8b..a201376f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,8 @@ name = "xrpl" crate-type = ["lib"] [dependencies] +xrpl-rust-macros = { path = "xrpl-rust-macros" } + lazy_static = "1.4.0" sha2 = { version = "0.10.2", default-features = false } rand_hc = "0.3.1" diff --git a/src/asynch/transaction/mod.rs b/src/asynch/transaction/mod.rs index 827c458c..ab5a1e50 100644 --- a/src/asynch/transaction/mod.rs +++ b/src/asynch/transaction/mod.rs @@ -539,6 +539,7 @@ mod test_autofill { #[cfg(test)] mod test_sign { use alloc::borrow::Cow; + use core::time::Duration; use crate::{ asynch::{ @@ -546,34 +547,26 @@ mod test_sign { transaction::{autofill_and_sign, sign}, wallet::generate_faucet_wallet, }, - models::transactions::{account_set::AccountSet, Transaction}, + models::transactions::{ + account_set::AccountSet, CommonFields, Transaction, TransactionType, + }, wallet::Wallet, }; #[tokio::test] async fn test_sign() { let wallet = Wallet::new("sEdT7wHTCLzDG7ueaw4hroSTBvH7Mk5", 0).unwrap(); - let mut tx = AccountSet::new( - Cow::from(wallet.classic_address.clone()), - None, - Some("10".into()), - None, - None, - None, - Some(227234), - None, - None, - None, - None, - Some("6578616d706c652e636f6d".into()), // "example.com" - None, - None, - None, - None, - None, - None, - ); + let mut tx = AccountSet { + common_fields: CommonFields::from_account(&wallet.classic_address) + .with_transaction_type(TransactionType::AccountSet) + .with_fee("10".into()) + .with_sequence(227234), + domain: Some("6578616d706c652e636f6d".into()), // "example.com" + ..Default::default() + }; + sign(&mut tx, &wallet, false).unwrap(); + let expected_signature: Cow = "C3F435CFBFAE996FE297F3A71BEAB68FF5322CBF039E41A9615BC48A59FB4EC\ 5A55F8D4EC0225D47056E02ECCCDF7E8FF5F8B7FAA1EBBCBF7D0491FCB2D98807" @@ -585,32 +578,31 @@ mod test_sign { #[tokio::test] async fn test_autofill_and_sign() { let client = AsyncJsonRpcClient::connect("https://testnet.xrpl-labs.com/".parse().unwrap()); - let wallet = generate_faucet_wallet(&client, None, None, None, None) - .await - .unwrap(); - let mut tx = AccountSet::new( - Cow::from(wallet.classic_address.clone()), - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - Some("6578616d706c652e636f6d".into()), // "example.com" - None, - None, - None, - None, - None, - None, - ); - autofill_and_sign(&mut tx, &client, &wallet, true) - .await - .unwrap(); + // Add timeout and better error handling for wallet generation + let wallet = tokio::time::timeout( + Duration::from_secs(30), + generate_faucet_wallet(&client, None, None, None, None), + ) + .await + .expect("Wallet generation timed out") + .expect("Failed to generate faucet wallet"); + + let mut tx = AccountSet { + common_fields: CommonFields::from_account(&wallet.classic_address) + .with_transaction_type(TransactionType::AccountSet), + domain: Some("6578616d706c652e636f6d".into()), + ..Default::default() + }; + + // Add timeout for autofill_and_sign + tokio::time::timeout( + Duration::from_secs(30), + autofill_and_sign(&mut tx, &client, &wallet, true), + ) + .await + .expect("Autofill and sign timed out") + .expect("Failed to autofill and sign transaction"); + assert!(tx.get_common_fields().sequence.is_some()); assert!(tx.get_common_fields().txn_signature.is_some()); } diff --git a/src/asynch/transaction/submit_and_wait.rs b/src/asynch/transaction/submit_and_wait.rs index eb9054e7..265e315d 100644 --- a/src/asynch/transaction/submit_and_wait.rs +++ b/src/asynch/transaction/submit_and_wait.rs @@ -185,41 +185,42 @@ where feature = "tokio-rt" ))] #[cfg(test)] -mod test_submit_and_wait { +mod tests { + use core::time::Duration; + use super::*; use crate::{ asynch::{clients::AsyncJsonRpcClient, wallet::generate_faucet_wallet}, - models::transactions::account_set::AccountSet, + models::transactions::{account_set::AccountSet, CommonFields, TransactionType}, }; #[tokio::test] async fn test_submit_and_wait() { let client = AsyncJsonRpcClient::connect("https://testnet.xrpl-labs.com/".parse().unwrap()); - let wallet = generate_faucet_wallet(&client, None, None, None, None) - .await - .unwrap(); - let mut tx = AccountSet::new( - Cow::from(wallet.classic_address.clone()), - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - Some("6578616d706c652e636f6d".into()), // "example.com" - None, - None, - None, - None, - None, - None, - ); - submit_and_wait(&mut tx, &client, Some(&wallet), Some(true), Some(true)) - .await - .unwrap(); + + // Add timeout and better error handling for wallet generation + let wallet = tokio::time::timeout( + Duration::from_secs(30), + generate_faucet_wallet(&client, None, None, None, None), + ) + .await + .expect("Wallet generation timed out") + .expect("Failed to generate faucet wallet"); + + let mut tx = AccountSet { + common_fields: CommonFields::from_account(&wallet.classic_address) + .with_transaction_type(TransactionType::AccountSet), + domain: Some("6578616d706c652e636f6d".into()), + ..Default::default() + }; + + // Add timeout for submit_and_wait + tokio::time::timeout( + Duration::from_secs(60), + submit_and_wait(&mut tx, &client, Some(&wallet), Some(true), Some(true)), + ) + .await + .expect("Submit and wait timed out") + .expect("Failed to submit and wait for transaction"); } } diff --git a/src/constants.rs b/src/constants.rs index a651965e..7cae0fca 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -26,10 +26,11 @@ pub const MAX_URI_LENGTH: usize = 512; pub const MAX_DOMAIN_LENGTH: usize = 256; /// Represents the supported cryptography algorithms. -#[derive(Debug, PartialEq, Eq, Clone, EnumIter, Display, Deserialize, Serialize)] +#[derive(Debug, PartialEq, Eq, Clone, EnumIter, Display, Deserialize, Serialize, Default)] #[serde(rename_all = "lowercase")] #[strum(serialize_all = "lowercase")] pub enum CryptoAlgorithm { + #[default] ED25519, SECP256K1, } diff --git a/src/models/amount/issued_currency_amount.rs b/src/models/amount/issued_currency_amount.rs index 12671f0d..32d31c80 100644 --- a/src/models/amount/issued_currency_amount.rs +++ b/src/models/amount/issued_currency_amount.rs @@ -12,7 +12,13 @@ pub struct IssuedCurrencyAmount<'a> { pub value: Cow<'a, str>, } -impl<'a> Model for IssuedCurrencyAmount<'a> {} +impl<'a> Model for IssuedCurrencyAmount<'a> { + fn get_errors(&self) -> XRPLModelResult<()> { + self.value.parse::()?; + + Ok(()) + } +} impl<'a> IssuedCurrencyAmount<'a> { pub fn new(currency: Cow<'a, str>, issuer: Cow<'a, str>, value: Cow<'a, str>) -> Self { diff --git a/src/models/amount/mod.rs b/src/models/amount/mod.rs index c890b590..40fab310 100644 --- a/src/models/amount/mod.rs +++ b/src/models/amount/mod.rs @@ -33,7 +33,14 @@ impl<'a> TryInto for Amount<'a> { } } -impl<'a> Model for Amount<'a> {} +impl<'a> Model for Amount<'a> { + fn get_errors(&self) -> XRPLModelResult<()> { + match self { + Amount::IssuedCurrencyAmount(amount) => amount.get_errors(), + Amount::XRPAmount(amount) => amount.get_errors(), + } + } +} impl<'a> Default for Amount<'a> { fn default() -> Self { diff --git a/src/models/amount/xrp_amount.rs b/src/models/amount/xrp_amount.rs index a42dbeb2..e717b471 100644 --- a/src/models/amount/xrp_amount.rs +++ b/src/models/amount/xrp_amount.rs @@ -16,7 +16,13 @@ use serde_json::Value; #[derive(Debug, PartialEq, Eq, Clone, Serialize)] pub struct XRPAmount<'a>(pub Cow<'a, str>); -impl<'a> Model for XRPAmount<'a> {} +impl<'a> Model for XRPAmount<'a> { + fn get_errors(&self) -> XRPLModelResult<()> { + self.0.parse::()?; + + Ok(()) + } +} impl Default for XRPAmount<'_> { fn default() -> Self { diff --git a/src/models/currency/mod.rs b/src/models/currency/mod.rs index 3b0aeb54..03f60d99 100644 --- a/src/models/currency/mod.rs +++ b/src/models/currency/mod.rs @@ -21,7 +21,14 @@ pub enum Currency<'a> { XRP(XRP<'a>), } -impl<'a> Model for Currency<'a> {} +impl<'a> Model for Currency<'a> { + fn get_errors(&self) -> crate::models::XRPLModelResult<()> { + match self { + Currency::IssuedCurrency(issued_currency) => issued_currency.get_errors(), + Currency::XRP(xrp) => xrp.get_errors(), + } + } +} impl<'a> Default for Currency<'a> { fn default() -> Self { diff --git a/src/models/currency/xrp.rs b/src/models/currency/xrp.rs index dcaf5103..6923b885 100644 --- a/src/models/currency/xrp.rs +++ b/src/models/currency/xrp.rs @@ -1,6 +1,6 @@ -use crate::models::amount::XRPAmount; use crate::models::currency::ToAmount; -use crate::models::Model; +use crate::models::{amount::XRPAmount, XRPLModelException}; +use crate::models::{Model, XRPLModelResult}; use alloc::borrow::Cow; use serde::{Deserialize, Serialize}; @@ -9,7 +9,19 @@ pub struct XRP<'a> { pub currency: Cow<'a, str>, } -impl<'a> Model for XRP<'a> {} +impl<'a> Model for XRP<'a> { + fn get_errors(&self) -> XRPLModelResult<()> { + if self.currency != "XRP" { + Err(XRPLModelException::InvalidValue { + field: "currency".into(), + expected: "XRP".into(), + found: self.currency.clone().into(), + }) + } else { + Ok(()) + } + } +} impl<'a> ToAmount<'a, XRPAmount<'a>> for XRP<'a> { fn to_amount(&self, value: Cow<'a, str>) -> XRPAmount<'a> { diff --git a/src/models/exceptions.rs b/src/models/exceptions.rs index 624a152d..ccbc80e5 100644 --- a/src/models/exceptions.rs +++ b/src/models/exceptions.rs @@ -64,6 +64,12 @@ pub enum XRPLModelException { ValueZero(String), #[error("If the field `{field1:?}` is defined, the field `{field2:?}` must also be defined")] FieldRequiresField { field1: String, field2: String }, + #[error("The value of the field `{field:?}` is not a valid value (expected: {expected:?}, found: {found:?})")] + InvalidValue { + field: String, + expected: String, + found: String, + }, #[error("Expected field `{0}` is missing")] MissingField(String), diff --git a/src/models/mod.rs b/src/models/mod.rs index 26804343..63cb3d2c 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -60,3 +60,10 @@ pub struct XChainBridge<'a> { fn default_false() -> Option { Some(false) } + +/// Trait for validating currencies in models. This is needed to use xrpl-rust-macros for deriving validation methods. +/// This trait is implemented by models that contain fields of type `Amount`, `XRPAmount`, `IssuedCurrencyAmount`, `Currency`, `XRP`, or `IssuedCurrency`. +/// It provides a method `validate_currencies` that checks if the provided values are valid according to the XRPL specifications. +pub trait ValidateCurrencies { + fn validate_currencies(&self) -> crate::models::XRPLModelResult<()>; +} diff --git a/src/models/results/server_info.rs b/src/models/results/server_info.rs index a7b94507..2c734951 100644 --- a/src/models/results/server_info.rs +++ b/src/models/results/server_info.rs @@ -47,6 +47,8 @@ pub struct Info<'a> { pub load_factor_fee_queue: Option, /// Transaction cost multiplier excluding open ledger pub load_factor_server: Option, + /// Network id for ledger + pub network_id: Option, /// Number of connected peer servers pub peers: u32, /// List of ports listening for API commands @@ -60,7 +62,7 @@ pub struct Info<'a> { /// Current server state pub server_state: Cow<'a, str>, /// Microseconds in current state - pub server_state_duration_us: Option, + pub server_state_duration_us: Option>, /// Server state accounting information pub state_accounting: Option, /// Current UTC time according to server @@ -155,6 +157,7 @@ mod tests { "proposers": 35 }, "load_factor": 1, + "network_id": 10, "peers": 22, "ports": [ { @@ -176,7 +179,7 @@ mod tests { ], "pubkey_node": "n9KQK8yvTDcZdGyhu2EGdDnFPEBSsY5wEGpU5GgpygTgLFsjQyPt", "server_state": "full", - "server_state_duration_us": 91758491912, + "server_state_duration_us": "91758491912", "time": "2023-Sep-13 22:12:31.377492 UTC", "uptime": 91948, "validated_ledger": { @@ -202,13 +205,17 @@ mod tests { assert_eq!(result.info.last_close.converge_time_s, 3); assert_eq!(result.info.last_close.proposers, 35); assert_eq!(result.info.load_factor, 1); + assert_eq!(result.info.network_id, Some(10)); assert_eq!(result.info.peers, 22); assert_eq!( result.info.pubkey_node, "n9KQK8yvTDcZdGyhu2EGdDnFPEBSsY5wEGpU5GgpygTgLFsjQyPt" ); assert_eq!(result.info.server_state, "full"); - assert_eq!(result.info.server_state_duration_us, Some(91758491912)); + assert_eq!( + result.info.server_state_duration_us, + Some("91758491912".into()) + ); assert_eq!( result.info.time, Some("2023-Sep-13 22:12:31.377492 UTC".into()) diff --git a/src/models/transactions/account_delete.rs b/src/models/transactions/account_delete.rs index afc98177..ff3846cb 100644 --- a/src/models/transactions/account_delete.rs +++ b/src/models/transactions/account_delete.rs @@ -7,7 +7,7 @@ use crate::models::amount::XRPAmount; use crate::models::transactions::CommonFields; use crate::models::{ transactions::{Transaction, TransactionType}, - Model, + Model, ValidateCurrencies, }; use crate::models::{FlagCollection, NoFlags}; @@ -21,7 +21,9 @@ use super::{Memo, Signer}; /// See AccountDelete: /// `` #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[derive( + Debug, Serialize, Deserialize, PartialEq, Eq, Clone, xrpl_rust_macros::ValidateCurrencies, +)] #[serde(rename_all = "PascalCase")] pub struct AccountDelete<'a> { /// The base fields for all transaction models. @@ -44,7 +46,11 @@ pub struct AccountDelete<'a> { pub destination_tag: Option, } -impl<'a> Model for AccountDelete<'a> {} +impl<'a> Model for AccountDelete<'a> { + fn get_errors(&self) -> crate::models::XRPLModelResult<()> { + self.validate_currencies() + } +} impl<'a> Transaction<'a, NoFlags> for AccountDelete<'a> { fn get_transaction_type(&self) -> &TransactionType { diff --git a/src/models/transactions/account_set.rs b/src/models/transactions/account_set.rs index 1b95b057..be944c34 100644 --- a/src/models/transactions/account_set.rs +++ b/src/models/transactions/account_set.rs @@ -8,7 +8,7 @@ use strum_macros::{AsRefStr, Display, EnumIter}; use crate::models::amount::XRPAmount; use crate::models::transactions::{exceptions::XRPLAccountSetException, CommonFields}; -use crate::models::{XRPLModelException, XRPLModelResult}; +use crate::models::{ValidateCurrencies, XRPLModelException, XRPLModelResult}; use crate::{ constants::{ DISABLE_TICK_SIZE, MAX_DOMAIN_LENGTH, MAX_TICK_SIZE, MAX_TRANSFER_RATE, MIN_TICK_SIZE, @@ -72,7 +72,16 @@ pub enum AccountSetFlag { /// See AccountSet: /// `` #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[derive( + Debug, + Default, + Serialize, + Deserialize, + PartialEq, + Eq, + Clone, + xrpl_rust_macros::ValidateCurrencies, +)] #[serde(rename_all = "PascalCase")] pub struct AccountSet<'a> { /// The base fields for all transaction models. @@ -122,6 +131,7 @@ pub struct AccountSet<'a> { impl<'a> Model for AccountSet<'a> { fn get_errors(&self) -> XRPLModelResult<()> { + self.validate_currencies()?; self._get_tick_size_error()?; self._get_transfer_rate_error()?; self._get_domain_error()?; diff --git a/src/models/transactions/amm_bid.rs b/src/models/transactions/amm_bid.rs index 520f561f..e742df9a 100644 --- a/src/models/transactions/amm_bid.rs +++ b/src/models/transactions/amm_bid.rs @@ -4,7 +4,7 @@ use serde_with::skip_serializing_none; use crate::models::{ transactions::TransactionType, Currency, FlagCollection, IssuedCurrencyAmount, Model, NoFlags, - XRPAmount, + ValidateCurrencies, XRPAmount, }; use super::{AuthAccount, CommonFields, Memo, Signer, Transaction}; @@ -18,7 +18,9 @@ use super::{AuthAccount, CommonFields, Memo, Signer, Transaction}; /// You bid using the AMM's LP Tokens; the amount of a winning bid is returned /// to the AMM, decreasing the outstanding balance of LP Tokens. #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[derive( + Debug, Serialize, Deserialize, PartialEq, Eq, Clone, xrpl_rust_macros::ValidateCurrencies, +)] #[serde(rename_all = "PascalCase")] pub struct AMMBid<'a> { #[serde(flatten)] @@ -41,7 +43,11 @@ pub struct AMMBid<'a> { pub auth_accounts: Option>, } -impl Model for AMMBid<'_> {} +impl Model for AMMBid<'_> { + fn get_errors(&self) -> crate::models::XRPLModelResult<()> { + self.validate_currencies() + } +} impl<'a> Transaction<'a, NoFlags> for AMMBid<'a> { fn get_common_fields(&self) -> &CommonFields<'_, NoFlags> { diff --git a/src/models/transactions/amm_create.rs b/src/models/transactions/amm_create.rs index b0fe8d79..7b1750df 100644 --- a/src/models/transactions/amm_create.rs +++ b/src/models/transactions/amm_create.rs @@ -2,7 +2,9 @@ use alloc::{borrow::Cow, vec::Vec}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; -use crate::models::{Amount, FlagCollection, Model, NoFlags, XRPAmount, XRPLModelResult}; +use crate::models::{ + Amount, FlagCollection, Model, NoFlags, ValidateCurrencies, XRPAmount, XRPLModelResult, +}; use super::{ exceptions::{XRPLAMMCreateException, XRPLTransactionException}, @@ -28,7 +30,9 @@ pub const AMM_CREATE_MAX_FEE: u16 = 1000; /// The higher the trading fee, the more it offsets this risk, /// so it's best to set the trading fee based on the volatility of the asset pair. #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[derive( + Debug, Serialize, Deserialize, PartialEq, Eq, Clone, xrpl_rust_macros::ValidateCurrencies, +)] #[serde(rename_all = "PascalCase")] pub struct AMMCreate<'a> { #[serde(flatten)] @@ -48,8 +52,7 @@ pub struct AMMCreate<'a> { impl Model for AMMCreate<'_> { fn get_errors(&self) -> XRPLModelResult<()> { self.get_tranding_fee_error()?; - - Ok(()) + self.validate_currencies() } } diff --git a/src/models/transactions/amm_delete.rs b/src/models/transactions/amm_delete.rs index 4afd2fc0..657b082a 100644 --- a/src/models/transactions/amm_delete.rs +++ b/src/models/transactions/amm_delete.rs @@ -2,7 +2,7 @@ use alloc::{borrow::Cow, vec::Vec}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; -use crate::models::{Currency, FlagCollection, Model, NoFlags, XRPAmount}; +use crate::models::{Currency, FlagCollection, Model, NoFlags, ValidateCurrencies, XRPAmount}; use super::{CommonFields, Memo, Signer, Transaction, TransactionType}; @@ -18,7 +18,9 @@ use super::{CommonFields, Memo, Signer, Transaction, TransactionType}; /// and the associated AMM. In all cases, the AMM ledger entry and AMM account are /// deleted by the last such transaction. #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[derive( + Debug, Serialize, Deserialize, PartialEq, Eq, Clone, xrpl_rust_macros::ValidateCurrencies, +)] #[serde(rename_all = "PascalCase")] pub struct AMMDelete<'a> { #[serde(flatten)] @@ -30,7 +32,11 @@ pub struct AMMDelete<'a> { pub asset2: Currency<'a>, } -impl Model for AMMDelete<'_> {} +impl Model for AMMDelete<'_> { + fn get_errors(&self) -> crate::models::XRPLModelResult<()> { + self.validate_currencies() + } +} impl<'a> Transaction<'a, NoFlags> for AMMDelete<'a> { fn get_common_fields(&self) -> &CommonFields<'_, NoFlags> { diff --git a/src/models/transactions/amm_deposit.rs b/src/models/transactions/amm_deposit.rs index 911379c9..d7204b63 100644 --- a/src/models/transactions/amm_deposit.rs +++ b/src/models/transactions/amm_deposit.rs @@ -6,7 +6,7 @@ use strum_macros::{AsRefStr, Display, EnumIter}; use crate::models::{ transactions::TransactionType, Amount, Currency, FlagCollection, IssuedCurrencyAmount, Model, - XRPAmount, XRPLModelException, XRPLModelResult, + ValidateCurrencies, XRPAmount, XRPLModelException, XRPLModelResult, }; use super::{CommonFields, Memo, Signer, Transaction}; @@ -33,7 +33,9 @@ pub enum AMMDepositFlag { /// If successful, this transaction creates a trust line to the AMM Account (limit 0) /// to hold the LP Tokens. #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[derive( + Debug, Serialize, Deserialize, PartialEq, Eq, Clone, xrpl_rust_macros::ValidateCurrencies, +)] #[serde(rename_all = "PascalCase")] pub struct AMMDeposit<'a> { #[serde(flatten)] @@ -61,6 +63,7 @@ pub struct AMMDeposit<'a> { impl Model for AMMDeposit<'_> { fn get_errors(&self) -> XRPLModelResult<()> { + self.validate_currencies()?; if self.amount2.is_some() && self.amount.is_none() { Err(XRPLModelException::FieldRequiresField { field1: "amount2".into(), diff --git a/src/models/transactions/amm_vote.rs b/src/models/transactions/amm_vote.rs index f37cb749..f4cb831a 100644 --- a/src/models/transactions/amm_vote.rs +++ b/src/models/transactions/amm_vote.rs @@ -3,7 +3,8 @@ use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; use crate::models::{ - Currency, FlagCollection, Model, NoFlags, XRPAmount, XRPLModelException, XRPLModelResult, + Currency, FlagCollection, Model, NoFlags, ValidateCurrencies, XRPAmount, XRPLModelException, + XRPLModelResult, }; use super::{CommonFields, Memo, Signer, Transaction, TransactionType}; @@ -17,7 +18,9 @@ pub const AMM_VOTE_MAX_TRADING_FEE: u16 = 1000; /// Each new vote re-calculates the AMM's trading fee based on a weighted average /// of the votes. #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[derive( + Debug, Serialize, Deserialize, PartialEq, Eq, Clone, xrpl_rust_macros::ValidateCurrencies, +)] #[serde(rename_all = "PascalCase")] pub struct AMMVote<'a> { pub common_fields: CommonFields<'a, NoFlags>, @@ -34,6 +37,7 @@ pub struct AMMVote<'a> { impl Model for AMMVote<'_> { fn get_errors(&self) -> XRPLModelResult<()> { + self.validate_currencies()?; if let Some(trading_fee) = self.trading_fee { if trading_fee > AMM_VOTE_MAX_TRADING_FEE { return Err(XRPLModelException::ValueTooHigh { diff --git a/src/models/transactions/amm_withdraw.rs b/src/models/transactions/amm_withdraw.rs index c6deaaa6..55719afc 100644 --- a/src/models/transactions/amm_withdraw.rs +++ b/src/models/transactions/amm_withdraw.rs @@ -4,7 +4,9 @@ use serde_repr::{Deserialize_repr, Serialize_repr}; use serde_with::skip_serializing_none; use strum_macros::{AsRefStr, Display, EnumIter}; -use crate::models::{Amount, Currency, FlagCollection, IssuedCurrencyAmount, Model, XRPAmount}; +use crate::models::{ + Amount, Currency, FlagCollection, IssuedCurrencyAmount, Model, ValidateCurrencies, XRPAmount, +}; use super::{CommonFields, Memo, Signer, Transaction, TransactionType}; @@ -27,7 +29,9 @@ pub enum AMMWithdrawFlag { /// Withdraw assets from an Automated Market Maker (AMM) instance by returning the /// AMM's liquidity provider tokens (LP Tokens). #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[derive( + Debug, Serialize, Deserialize, PartialEq, Eq, Clone, xrpl_rust_macros::ValidateCurrencies, +)] #[serde(rename_all = "PascalCase")] pub struct AMMWithdraw<'a> { pub common_fields: CommonFields<'a, AMMWithdrawFlag>, @@ -53,6 +57,7 @@ pub struct AMMWithdraw<'a> { impl Model for AMMWithdraw<'_> { fn get_errors(&self) -> crate::models::XRPLModelResult<()> { + self.validate_currencies()?; if self.amount2.is_some() && self.amount.is_none() { Err(crate::models::XRPLModelException::FieldRequiresField { field1: "amount2".into(), diff --git a/src/models/transactions/check_cancel.rs b/src/models/transactions/check_cancel.rs index 3f814261..0faf5476 100644 --- a/src/models/transactions/check_cancel.rs +++ b/src/models/transactions/check_cancel.rs @@ -9,7 +9,7 @@ use crate::models::{ transactions::{Transaction, TransactionType}, Model, }; -use crate::models::{FlagCollection, NoFlags}; +use crate::models::{FlagCollection, NoFlags, ValidateCurrencies}; use super::{Memo, Signer}; @@ -21,7 +21,9 @@ use super::{Memo, Signer}; /// See CheckCancel: /// `` #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[derive( + Debug, Serialize, Deserialize, PartialEq, Eq, Clone, xrpl_rust_macros::ValidateCurrencies, +)] #[serde(rename_all = "PascalCase")] pub struct CheckCancel<'a> { /// The base fields for all transaction models. @@ -39,7 +41,11 @@ pub struct CheckCancel<'a> { pub check_id: Cow<'a, str>, } -impl<'a> Model for CheckCancel<'a> {} +impl<'a> Model for CheckCancel<'a> { + fn get_errors(&self) -> crate::models::XRPLModelResult<()> { + self.validate_currencies() + } +} impl<'a> Transaction<'a, NoFlags> for CheckCancel<'a> { fn get_transaction_type(&self) -> &TransactionType { diff --git a/src/models/transactions/check_cash.rs b/src/models/transactions/check_cash.rs index 9b832261..98b1cc55 100644 --- a/src/models/transactions/check_cash.rs +++ b/src/models/transactions/check_cash.rs @@ -10,7 +10,9 @@ use crate::models::{ transactions::{Memo, Signer, Transaction, TransactionType}, Model, }; -use crate::models::{FlagCollection, NoFlags, XRPLModelException, XRPLModelResult}; +use crate::models::{ + FlagCollection, NoFlags, ValidateCurrencies, XRPLModelException, XRPLModelResult, +}; /// Cancels an unredeemed Check, removing it from the ledger without /// sending any money. The source or the destination of the check can @@ -20,7 +22,9 @@ use crate::models::{FlagCollection, NoFlags, XRPLModelException, XRPLModelResult /// See CheckCash: /// `` #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[derive( + Debug, Serialize, Deserialize, PartialEq, Eq, Clone, xrpl_rust_macros::ValidateCurrencies, +)] #[serde(rename_all = "PascalCase")] pub struct CheckCash<'a> { /// The base fields for all transaction models. @@ -48,8 +52,7 @@ pub struct CheckCash<'a> { impl<'a: 'static> Model for CheckCash<'a> { fn get_errors(&self) -> XRPLModelResult<()> { self._get_amount_and_deliver_min_error()?; - - Ok(()) + self.validate_currencies() } } diff --git a/src/models/transactions/check_create.rs b/src/models/transactions/check_create.rs index 61e7484c..47967185 100644 --- a/src/models/transactions/check_create.rs +++ b/src/models/transactions/check_create.rs @@ -11,7 +11,7 @@ use crate::models::{ transactions::{Transaction, TransactionType}, Model, }; -use crate::models::{FlagCollection, NoFlags}; +use crate::models::{FlagCollection, NoFlags, ValidateCurrencies}; use super::{Memo, Signer}; @@ -21,7 +21,9 @@ use super::{Memo, Signer}; /// See CheckCreate: /// `` #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[derive( + Debug, Serialize, Deserialize, PartialEq, Eq, Clone, xrpl_rust_macros::ValidateCurrencies, +)] #[serde(rename_all = "PascalCase")] pub struct CheckCreate<'a> { /// The base fields for all transaction models. @@ -50,7 +52,11 @@ pub struct CheckCreate<'a> { pub invoice_id: Option>, } -impl<'a> Model for CheckCreate<'a> {} +impl<'a> Model for CheckCreate<'a> { + fn get_errors(&self) -> crate::models::XRPLModelResult<()> { + self.validate_currencies() + } +} impl<'a> Transaction<'a, NoFlags> for CheckCreate<'a> { fn get_transaction_type(&self) -> &TransactionType { diff --git a/src/models/transactions/deposit_preauth.rs b/src/models/transactions/deposit_preauth.rs index e8bd5993..33d7d65b 100644 --- a/src/models/transactions/deposit_preauth.rs +++ b/src/models/transactions/deposit_preauth.rs @@ -9,7 +9,9 @@ use crate::models::{ transactions::{Memo, Signer, Transaction, TransactionType}, Model, }; -use crate::models::{FlagCollection, NoFlags, XRPLModelException, XRPLModelResult}; +use crate::models::{ + FlagCollection, NoFlags, ValidateCurrencies, XRPLModelException, XRPLModelResult, +}; /// A DepositPreauth transaction gives another account pre-approval /// to deliver payments to the sender of this transaction. @@ -17,7 +19,9 @@ use crate::models::{FlagCollection, NoFlags, XRPLModelException, XRPLModelResult /// See DepositPreauth: /// `` #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[derive( + Debug, Serialize, Deserialize, PartialEq, Eq, Clone, xrpl_rust_macros::ValidateCurrencies, +)] #[serde(rename_all = "PascalCase")] pub struct DepositPreauth<'a> { /// The base fields for all transaction models. @@ -39,8 +43,7 @@ pub struct DepositPreauth<'a> { impl<'a: 'static> Model for DepositPreauth<'a> { fn get_errors(&self) -> XRPLModelResult<()> { self._get_authorize_and_unauthorize_error()?; - - Ok(()) + self.validate_currencies() } } diff --git a/src/models/transactions/escrow_cancel.rs b/src/models/transactions/escrow_cancel.rs index e307ed58..b4b0f3b4 100644 --- a/src/models/transactions/escrow_cancel.rs +++ b/src/models/transactions/escrow_cancel.rs @@ -10,7 +10,7 @@ use crate::models::{ transactions::{Transaction, TransactionType}, Model, }; -use crate::models::{FlagCollection, NoFlags}; +use crate::models::{FlagCollection, NoFlags, ValidateCurrencies}; use super::{Memo, Signer}; @@ -19,7 +19,9 @@ use super::{Memo, Signer}; /// See EscrowCancel: /// `` #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[derive( + Debug, Serialize, Deserialize, PartialEq, Eq, Clone, xrpl_rust_macros::ValidateCurrencies, +)] #[serde(rename_all = "PascalCase")] pub struct EscrowCancel<'a> { /// The base fields for all transaction models. @@ -38,7 +40,11 @@ pub struct EscrowCancel<'a> { pub offer_sequence: u32, } -impl<'a> Model for EscrowCancel<'a> {} +impl<'a> Model for EscrowCancel<'a> { + fn get_errors(&self) -> crate::models::XRPLModelResult<()> { + self.validate_currencies() + } +} impl<'a> Transaction<'a, NoFlags> for EscrowCancel<'a> { fn get_transaction_type(&self) -> &TransactionType { diff --git a/src/models/transactions/escrow_create.rs b/src/models/transactions/escrow_create.rs index 8ee31493..3b54e358 100644 --- a/src/models/transactions/escrow_create.rs +++ b/src/models/transactions/escrow_create.rs @@ -7,7 +7,7 @@ use crate::models::amount::XRPAmount; use crate::models::transactions::CommonFields; use crate::models::{ transactions::{Memo, Signer, Transaction, TransactionType}, - Model, + Model, ValidateCurrencies, }; use crate::models::{FlagCollection, NoFlags, XRPLModelException, XRPLModelResult}; @@ -16,7 +16,9 @@ use crate::models::{FlagCollection, NoFlags, XRPLModelException, XRPLModelResult /// See EscrowCreate: /// `` #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[derive( + Debug, Serialize, Deserialize, PartialEq, Eq, Clone, xrpl_rust_macros::ValidateCurrencies, +)] #[serde(rename_all = "PascalCase")] pub struct EscrowCreate<'a> { /// The base fields for all transaction models. @@ -57,8 +59,7 @@ pub struct EscrowCreate<'a> { impl<'a: 'static> Model for EscrowCreate<'a> { fn get_errors(&self) -> XRPLModelResult<()> { self._get_finish_after_error()?; - - Ok(()) + self.validate_currencies() } } diff --git a/src/models/transactions/escrow_finish.rs b/src/models/transactions/escrow_finish.rs index 1cb74129..5bdf5189 100644 --- a/src/models/transactions/escrow_finish.rs +++ b/src/models/transactions/escrow_finish.rs @@ -6,7 +6,7 @@ use serde_with::skip_serializing_none; use crate::models::{ amount::XRPAmount, transactions::{Memo, Signer, Transaction, TransactionType}, - Model, XRPLModelException, XRPLModelResult, + Model, ValidateCurrencies, XRPLModelException, XRPLModelResult, }; use crate::models::{FlagCollection, NoFlags}; @@ -17,7 +17,9 @@ use super::CommonFields; /// See EscrowFinish: /// `` #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[derive( + Debug, Serialize, Deserialize, PartialEq, Eq, Clone, xrpl_rust_macros::ValidateCurrencies, +)] #[serde(rename_all = "PascalCase")] pub struct EscrowFinish<'a> { // The base fields for all transaction models. @@ -47,8 +49,7 @@ pub struct EscrowFinish<'a> { impl<'a: 'static> Model for EscrowFinish<'a> { fn get_errors(&self) -> XRPLModelResult<()> { self._get_condition_and_fulfillment_error()?; - - Ok(()) + self.validate_currencies() } } diff --git a/src/models/transactions/metadata.rs b/src/models/transactions/metadata.rs index 1e45c85d..cb235f44 100644 --- a/src/models/transactions/metadata.rs +++ b/src/models/transactions/metadata.rs @@ -4,18 +4,29 @@ use serde_with::skip_serializing_none; use crate::models::ledger::objects::LedgerEntryType; use crate::models::requests::LedgerIndex; -use crate::models::{Amount, IssuedCurrencyAmount}; +use crate::models::{Amount, IssuedCurrencyAmount, Model, ValidateCurrencies}; #[skip_serializing_none] -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive( + Debug, Clone, Serialize, Deserialize, PartialEq, Eq, xrpl_rust_macros::ValidateCurrencies, +)] #[serde(rename_all = "PascalCase")] pub struct NFTokenMetadata<'a> { #[serde(rename = "NFToken")] pub nftoken: NFTokenMetadataFields<'a>, } +impl Model for NFTokenMetadata<'_> { + fn get_errors(&self) -> crate::models::XRPLModelResult<()> { + self.validate_currencies()?; + Ok(()) + } +} + #[skip_serializing_none] -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive( + Debug, Clone, Serialize, Deserialize, PartialEq, Eq, xrpl_rust_macros::ValidateCurrencies, +)] #[serde(rename_all = "PascalCase")] pub struct NFTokenMetadataFields<'a> { #[serde(rename = "NFTokenID")] @@ -23,8 +34,18 @@ pub struct NFTokenMetadataFields<'a> { #[serde(rename = "URI")] pub uri: Cow<'a, str>, } + +impl Model for NFTokenMetadataFields<'_> { + fn get_errors(&self) -> crate::models::XRPLModelResult<()> { + self.validate_currencies()?; + Ok(()) + } +} + #[skip_serializing_none] -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive( + Debug, Clone, Serialize, Deserialize, PartialEq, Eq, xrpl_rust_macros::ValidateCurrencies, +)] #[serde(rename_all = "PascalCase")] pub struct Fields<'a> { pub account: Option>, @@ -46,6 +67,13 @@ pub struct Fields<'a> { pub xchain_claim_id: Option>, } +impl Model for Fields<'_> { + fn get_errors(&self) -> crate::models::XRPLModelResult<()> { + self.validate_currencies()?; + Ok(()) + } +} + #[skip_serializing_none] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "PascalCase")] @@ -82,7 +110,9 @@ pub enum NodeType { } #[skip_serializing_none] -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive( + Debug, Clone, Serialize, Deserialize, PartialEq, Eq, xrpl_rust_macros::ValidateCurrencies, +)] #[serde(rename_all = "PascalCase")] pub struct TransactionMetadata<'a> { pub affected_nodes: Vec>, @@ -92,6 +122,13 @@ pub struct TransactionMetadata<'a> { pub delivered_amount: Option>, } +impl Model for TransactionMetadata<'_> { + fn get_errors(&self) -> crate::models::XRPLModelResult<()> { + self.validate_currencies()?; + Ok(()) + } +} + #[cfg(test)] mod test_serde { #[test] diff --git a/src/models/transactions/mod.rs b/src/models/transactions/mod.rs index 22043760..d2d2e047 100644 --- a/src/models/transactions/mod.rs +++ b/src/models/transactions/mod.rs @@ -49,6 +49,7 @@ use alloc::format; use alloc::string::{String, ToString}; use alloc::vec::Vec; use core::fmt::Debug; +use core::str::FromStr; use derive_new::new; use exceptions::XRPLTransactionException; use serde::de::DeserializeOwned; @@ -275,6 +276,149 @@ where } } +impl<'a, T> Default for CommonFields<'a, T> +where + T: IntoEnumIterator + Serialize + core::fmt::Debug, +{ + fn default() -> Self { + Self { + account: "".into(), + transaction_type: TransactionType::Payment, // Temporary default, will be overridden + account_txn_id: None, + fee: None, + flags: FlagCollection::default(), + last_ledger_sequence: None, + memos: None, + network_id: None, + sequence: None, + signers: None, + signing_pub_key: None, + source_tag: None, + ticket_sequence: None, + txn_signature: None, + } + } +} + +impl<'a, T> FromStr for CommonFields<'a, T> +where + T: IntoEnumIterator + Serialize + core::fmt::Debug, +{ + type Err = core::convert::Infallible; + + fn from_str(account: &str) -> Result { + Ok(Self { + account: Cow::Owned(account.to_string()), + ..Default::default() + }) + } +} + +impl<'a, T> From for CommonFields<'a, T> +where + T: IntoEnumIterator + Serialize + core::fmt::Debug, +{ + fn from(account: String) -> Self { + Self { + account: account.into(), + ..Default::default() + } + } +} + +impl<'a, T> From> for CommonFields<'a, T> +where + T: IntoEnumIterator + Serialize + core::fmt::Debug, +{ + fn from(account: Cow<'a, str>) -> Self { + Self { + account, + ..Default::default() + } + } +} + +impl<'a, T> CommonFields<'a, T> +where + T: IntoEnumIterator + Serialize + core::fmt::Debug, +{ + pub fn with_transaction_type(mut self, transaction_type: TransactionType) -> Self { + self.transaction_type = transaction_type; + self + } + + pub fn with_fee(mut self, fee: XRPAmount<'a>) -> Self { + self.fee = Some(fee); + self + } + + pub fn with_sequence(mut self, sequence: u32) -> Self { + self.sequence = Some(sequence); + self + } + + pub fn with_last_ledger_sequence(mut self, last_ledger_sequence: u32) -> Self { + self.last_ledger_sequence = Some(last_ledger_sequence); + self + } + + pub fn with_source_tag(mut self, source_tag: u32) -> Self { + self.source_tag = Some(source_tag); + self + } + + pub fn with_memo(mut self, memo: Memo) -> Self { + match self.memos { + Some(ref mut memos) => memos.push(memo), + None => self.memos = Some(alloc::vec![memo]), + } + self + } + + pub fn with_memos(mut self, memos: Vec) -> Self { + self.memos = Some(memos); + self + } + + pub fn with_network_id(mut self, network_id: u32) -> Self { + self.network_id = Some(network_id); + self + } + + pub fn with_ticket_sequence(mut self, ticket_sequence: u32) -> Self { + self.ticket_sequence = Some(ticket_sequence); + self + } + + pub fn with_account_txn_id(mut self, account_txn_id: Cow<'a, str>) -> Self { + self.account_txn_id = Some(account_txn_id); + self + } + + pub fn with_signers(mut self, signers: Vec) -> Self { + self.signers = Some(signers); + self + } + + pub fn with_signing_pub_key(mut self, signing_pub_key: Cow<'a, str>) -> Self { + self.signing_pub_key = Some(signing_pub_key); + self + } + + pub fn with_txn_signature(mut self, txn_signature: Cow<'a, str>) -> Self { + self.txn_signature = Some(txn_signature); + self + } + + /// Create CommonFields from an account string (takes ownership) + pub fn from_account(account: impl Into>) -> Self { + Self { + account: account.into(), + ..Default::default() + } + } +} + fn flag_collection_default() -> FlagCollection where T: IntoEnumIterator + Serialize + core::fmt::Debug, @@ -397,10 +541,13 @@ pub enum Flag { feature = "wallet" ))] #[cfg(test)] -mod test_tx_common_fields { - use super::*; - use account_set::AccountSet; +mod tests { + use alloc::borrow::Cow; + use super::*; + use crate::models::transactions::payment::PaymentFlag; + use crate::models::transactions::{account_set::AccountSet, payment::Payment}; + use crate::models::{Amount, XRPAmount}; use offer_create::OfferCreate; #[tokio::test] @@ -444,4 +591,341 @@ mod test_tx_common_fields { let tx: AccountSet = serde_json::from_str(tx_json_str).unwrap(); assert_eq!(tx.get_hash().unwrap(), expected_hash); } + + #[test] + fn test_common_fields_default() { + let common_fields: CommonFields = Default::default(); + + assert_eq!(common_fields.account, ""); + assert_eq!(common_fields.transaction_type, TransactionType::Payment); + assert!(common_fields.fee.is_none()); + assert!(common_fields.sequence.is_none()); + assert!(common_fields.memos.is_none()); + } + + #[test] + fn test_common_fields_from_str() { + let account = "rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH"; + let common_fields: CommonFields = account.parse().unwrap(); + + assert_eq!(common_fields.account, account); + assert_eq!(common_fields.transaction_type, TransactionType::Payment); + assert!(common_fields.fee.is_none()); + } + + #[test] + fn test_common_fields_from_string() { + let account = String::from("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH"); + let common_fields: CommonFields = CommonFields::from(account.clone()); + + assert_eq!(common_fields.account, account); + assert_eq!(common_fields.transaction_type, TransactionType::Payment); + } + + #[test] + fn test_common_fields_from_cow() { + let account: Cow = "rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH".into(); + let common_fields: CommonFields = CommonFields::from(account.clone()); + + assert_eq!(common_fields.account, account); + } + + #[test] + fn test_common_fields_builder_methods() { + let common_fields = "rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH" + .parse::>() + .unwrap() + .with_transaction_type(TransactionType::Payment) + .with_fee("12".into()) + .with_sequence(100) + .with_last_ledger_sequence(596447) + .with_source_tag(42) + .with_network_id(1025); + + assert_eq!(common_fields.account, "rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH"); + assert_eq!(common_fields.transaction_type, TransactionType::Payment); + assert_eq!(common_fields.fee, Some("12".into())); + assert_eq!(common_fields.sequence, Some(100)); + assert_eq!(common_fields.last_ledger_sequence, Some(596447)); + assert_eq!(common_fields.source_tag, Some(42)); + assert_eq!(common_fields.network_id, Some(1025)); + } + + #[test] + fn test_memo_builder() { + let memo = Memo { + memo_data: Some("Test memo".to_string()), + memo_format: Some("text/plain".to_string()), + memo_type: Some("test".to_string()), + }; + + let common_fields = "rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH" + .parse::>() + .unwrap() + .with_memo(memo.clone()); + + assert_eq!(common_fields.memos, Some(alloc::vec![memo])); + } + + #[test] + fn test_multiple_memos_builder() { + let memo1 = Memo { + memo_data: Some("First memo".to_string()), + memo_format: Some("text/plain".to_string()), + memo_type: Some("info".to_string()), + }; + + let memo2 = Memo { + memo_data: Some("Second memo".to_string()), + memo_format: Some("text/plain".to_string()), + memo_type: Some("note".to_string()), + }; + + let common_fields = "rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH" + .parse::>() + .unwrap() + .with_memo(memo1.clone()) + .with_memo(memo2.clone()); + + assert_eq!(common_fields.memos, Some(alloc::vec![memo1, memo2])); + } + + #[test] + fn test_memos_builder_replace() { + let memo1 = Memo { + memo_data: Some("First memo".to_string()), + memo_format: None, + memo_type: None, + }; + + let memo2 = Memo { + memo_data: Some("Second memo".to_string()), + memo_format: None, + memo_type: None, + }; + + let common_fields = "rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH" + .parse::>() + .unwrap() + .with_memo(memo1) + .with_memos(alloc::vec![memo2.clone()]); + + assert_eq!(common_fields.memos, Some(alloc::vec![memo2])); + } + + #[test] + fn test_from_account_helper() { + let common_fields: CommonFields = + CommonFields::from_account("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH"); + + assert_eq!(common_fields.account, "rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH"); + assert_eq!(common_fields.transaction_type, TransactionType::Payment); + } + + #[test] + fn test_signers_builder() { + let signer = Signer { + account: "rSignerAccount123".to_string(), + txn_signature: "signature123".to_string(), + signing_pub_key: "pubkey123".to_string(), + }; + + let common_fields = "rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH" + .parse::>() + .unwrap() + .with_signers(alloc::vec![signer.clone()]); + + assert_eq!(common_fields.signers, Some(alloc::vec![signer])); + } + + #[test] + fn test_signature_fields_builder() { + let common_fields = "rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH" + .parse::>() + .unwrap() + .with_signing_pub_key("ED12345...".into()) + .with_txn_signature("A1B2C3...".into()); + + assert_eq!(common_fields.signing_pub_key, Some("ED12345...".into())); + assert_eq!(common_fields.txn_signature, Some("A1B2C3...".into())); + } + + #[test] + fn test_account_txn_id_builder() { + let txn_id = "F1E2D3C4B5A69788"; + let common_fields = "rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH" + .parse::>() + .unwrap() + .with_account_txn_id(txn_id.into()); + + assert_eq!(common_fields.account_txn_id, Some(txn_id.into())); + } + + #[test] + fn test_ticket_sequence_builder() { + let common_fields = "rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH" + .parse::>() + .unwrap() + .with_ticket_sequence(50); + + assert_eq!(common_fields.ticket_sequence, Some(50)); + } + + #[test] + fn test_payment_with_builder_pattern() { + let payment = Payment { + common_fields: "rSender123" + .parse::>() + .unwrap() + .with_transaction_type(TransactionType::Payment) + .with_fee("12".into()) + .with_sequence(100), + amount: Amount::XRPAmount(XRPAmount::from("1000000")), + destination: "rReceiver456".into(), + ..Default::default() + }; + + assert_eq!(payment.common_fields.account, "rSender123"); + assert_eq!( + payment.common_fields.transaction_type, + TransactionType::Payment + ); + assert_eq!(payment.common_fields.fee, Some("12".into())); + assert_eq!(payment.common_fields.sequence, Some(100)); + assert_eq!(payment.destination, "rReceiver456"); + } + + #[test] + fn test_account_set_with_builder_pattern() { + let account_set = AccountSet { + common_fields: CommonFields::from_account("rAccount123") + .with_transaction_type(TransactionType::AccountSet) + .with_fee("12".into()) + .with_sequence(50), + domain: Some("6578616d706c652e636f6d".into()), // "example.com" + ..Default::default() + }; + + assert_eq!(account_set.common_fields.account, "rAccount123"); + assert_eq!( + account_set.common_fields.transaction_type, + TransactionType::AccountSet + ); + assert_eq!(account_set.common_fields.fee, Some("12".into())); + assert_eq!(account_set.common_fields.sequence, Some(50)); + assert_eq!(account_set.domain, Some("6578616d706c652e636f6d".into())); + } + + #[test] + fn test_complex_payment_with_memos() { + let memo = Memo { + memo_data: Some("Payment for services".to_string()), + memo_format: Some("text/plain".to_string()), + memo_type: Some("payment".to_string()), + }; + + let payment = Payment { + common_fields: "rSender123" + .parse::>() + .unwrap() + .with_transaction_type(TransactionType::Payment) + .with_fee("12".into()) + .with_sequence(100) + .with_last_ledger_sequence(596447) + .with_source_tag(12345) + .with_memo(memo.clone()), + amount: Amount::XRPAmount(XRPAmount::from("1000000")), + destination: "rReceiver456".into(), + destination_tag: Some(67890), + ..Default::default() + }; + + assert_eq!(payment.common_fields.memos, Some(alloc::vec![memo])); + assert_eq!(payment.common_fields.source_tag, Some(12345)); + assert_eq!(payment.destination_tag, Some(67890)); + } + + #[test] + fn test_builder_pattern_fluency() { + // Test that the builder pattern is truly fluent and readable + let payment = Payment { + common_fields: "rSender123" + .parse::>() + .unwrap() + .with_transaction_type(TransactionType::Payment) + .with_fee("12".into()) + .with_sequence(100) + .with_last_ledger_sequence(596447) + .with_source_tag(12345) + .with_network_id(1025) + .with_memo(Memo { + memo_data: Some("Test payment".to_string()), + memo_format: Some("text/plain".to_string()), + memo_type: Some("test".to_string()), + }), + amount: Amount::XRPAmount(XRPAmount::from("1000000")), + destination: "rReceiver456".into(), + destination_tag: Some(67890), + ..Default::default() + }; + + // Verify all fields are set correctly + assert_eq!(payment.common_fields.account, "rSender123"); + assert_eq!(payment.common_fields.fee, Some("12".into())); + assert_eq!(payment.common_fields.sequence, Some(100)); + assert_eq!(payment.common_fields.last_ledger_sequence, Some(596447)); + assert_eq!(payment.common_fields.source_tag, Some(12345)); + assert_eq!(payment.common_fields.network_id, Some(1025)); + assert_eq!(payment.destination_tag, Some(67890)); + assert!(payment.common_fields.memos.is_some()); + } + + #[test] + fn test_different_from_methods() { + // Test all different ways to create CommonFields + let account_str = "rAccount123"; + let account_string = String::from("rAccount123"); + let account_cow: Cow = "rAccount123".into(); + + let cf1: CommonFields = account_str.parse().unwrap(); + let cf2: CommonFields = CommonFields::from(account_string); + let cf3: CommonFields = CommonFields::from(account_cow); + let cf4: CommonFields = CommonFields::from_account("rAccount123"); + + assert_eq!(cf1.account, "rAccount123"); + assert_eq!(cf2.account, "rAccount123"); + assert_eq!(cf3.account, "rAccount123"); + assert_eq!(cf4.account, "rAccount123"); + } + + #[test] + fn test_fromstr_never_fails() { + // Test edge cases for FromStr + let empty: Result, _> = "".parse(); + assert!(empty.is_ok()); + assert_eq!(empty.unwrap().account, ""); + + let whitespace: Result, _> = " ".parse(); + assert!(whitespace.is_ok()); + assert_eq!(whitespace.unwrap().account, " "); + + let long_string: Result, _> = "r".repeat(1000).parse(); + assert!(long_string.is_ok()); + assert_eq!(long_string.unwrap().account.len(), 1000); + } + + #[test] + fn test_builder_pattern_overwrites() { + // Test that builder methods properly overwrite values + let common_fields = "rAccount123" + .parse::>() + .unwrap() + .with_fee("10".into()) + .with_fee("20".into()) // Should overwrite the first fee + .with_sequence(100) + .with_sequence(200); // Should overwrite the first sequence + + assert_eq!(common_fields.fee, Some("20".into())); + assert_eq!(common_fields.sequence, Some(200)); + } } diff --git a/src/models/transactions/nftoken_accept_offer.rs b/src/models/transactions/nftoken_accept_offer.rs index d4c6f991..235cc93b 100644 --- a/src/models/transactions/nftoken_accept_offer.rs +++ b/src/models/transactions/nftoken_accept_offer.rs @@ -9,7 +9,7 @@ use crate::models::amount::XRPAmount; use crate::models::{ amount::Amount, transactions::{Memo, Signer, Transaction, TransactionType}, - Model, + Model, ValidateCurrencies, }; use crate::models::{FlagCollection, NoFlags, XRPLModelException, XRPLModelResult}; @@ -20,7 +20,9 @@ use super::CommonFields; /// See NFTokenAcceptOffer: /// `` #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[derive( + Debug, Serialize, Deserialize, PartialEq, Eq, Clone, xrpl_rust_macros::ValidateCurrencies, +)] #[serde(rename_all = "PascalCase")] pub struct NFTokenAcceptOffer<'a> { // The base fields for all transaction models. @@ -58,8 +60,7 @@ impl<'a: 'static> Model for NFTokenAcceptOffer<'a> { fn get_errors(&self) -> XRPLModelResult<()> { self._get_brokered_mode_error()?; self._get_nftoken_broker_fee_error()?; - - Ok(()) + self.validate_currencies() } } diff --git a/src/models/transactions/nftoken_burn.rs b/src/models/transactions/nftoken_burn.rs index 33e9a546..42425ffc 100644 --- a/src/models/transactions/nftoken_burn.rs +++ b/src/models/transactions/nftoken_burn.rs @@ -7,7 +7,7 @@ use serde_with::skip_serializing_none; use crate::models::amount::XRPAmount; use crate::models::{ transactions::{Memo, Signer, Transaction, TransactionType}, - Model, + Model, ValidateCurrencies, }; use crate::models::{FlagCollection, NoFlags}; @@ -19,7 +19,9 @@ use super::CommonFields; /// See NFTokenBurn: /// `` #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[derive( + Debug, Serialize, Deserialize, PartialEq, Eq, Clone, xrpl_rust_macros::ValidateCurrencies, +)] #[serde(rename_all = "PascalCase")] pub struct NFTokenBurn<'a> { // The base fields for all transaction models. @@ -46,7 +48,11 @@ pub struct NFTokenBurn<'a> { pub owner: Option>, } -impl<'a> Model for NFTokenBurn<'a> {} +impl<'a> Model for NFTokenBurn<'a> { + fn get_errors(&self) -> crate::models::XRPLModelResult<()> { + self.validate_currencies() + } +} impl<'a> Transaction<'a, NoFlags> for NFTokenBurn<'a> { fn get_transaction_type(&self) -> &TransactionType { diff --git a/src/models/transactions/nftoken_cancel_offer.rs b/src/models/transactions/nftoken_cancel_offer.rs index e48ecd58..40d2c668 100644 --- a/src/models/transactions/nftoken_cancel_offer.rs +++ b/src/models/transactions/nftoken_cancel_offer.rs @@ -9,7 +9,7 @@ use crate::models::{ transactions::{Memo, Signer, Transaction, TransactionType}, Model, }; -use crate::models::{FlagCollection, NoFlags, XRPLModelResult}; +use crate::models::{FlagCollection, NoFlags, ValidateCurrencies, XRPLModelResult}; use super::CommonFields; @@ -18,7 +18,9 @@ use super::CommonFields; /// See NFTokenCancelOffer: /// `` #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[derive( + Debug, Serialize, Deserialize, PartialEq, Eq, Clone, xrpl_rust_macros::ValidateCurrencies, +)] #[serde(rename_all = "PascalCase")] pub struct NFTokenCancelOffer<'a> { // The base fields for all transaction models. @@ -48,8 +50,7 @@ pub struct NFTokenCancelOffer<'a> { impl<'a: 'static> Model for NFTokenCancelOffer<'a> { fn get_errors(&self) -> XRPLModelResult<()> { self._get_nftoken_offers_error()?; - - Ok(()) + self.validate_currencies() } } diff --git a/src/models/transactions/nftoken_create_offer.rs b/src/models/transactions/nftoken_create_offer.rs index 7295b7be..357ee2af 100644 --- a/src/models/transactions/nftoken_create_offer.rs +++ b/src/models/transactions/nftoken_create_offer.rs @@ -9,7 +9,7 @@ use strum_macros::{AsRefStr, Display, EnumIter}; use crate::models::{ transactions::{Memo, Signer, Transaction, TransactionType}, - Model, XRPLModelException, XRPLModelResult, + Model, ValidateCurrencies, XRPLModelException, XRPLModelResult, }; use crate::models::amount::{Amount, XRPAmount}; @@ -39,7 +39,9 @@ pub enum NFTokenCreateOfferFlag { /// See NFTokenCreateOffer: /// `` #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[derive( + Debug, Serialize, Deserialize, PartialEq, Eq, Clone, xrpl_rust_macros::ValidateCurrencies, +)] #[serde(rename_all = "PascalCase")] pub struct NFTokenCreateOffer<'a> { // The base fields for all transaction models. @@ -83,8 +85,7 @@ impl<'a: 'static> Model for NFTokenCreateOffer<'a> { self._get_amount_error()?; self._get_destination_error()?; self._get_owner_error()?; - - Ok(()) + self.validate_currencies() } } diff --git a/src/models/transactions/nftoken_mint.rs b/src/models/transactions/nftoken_mint.rs index d76e2bb4..9a1a6243 100644 --- a/src/models/transactions/nftoken_mint.rs +++ b/src/models/transactions/nftoken_mint.rs @@ -9,7 +9,7 @@ use crate::{ constants::{MAX_TRANSFER_FEE, MAX_URI_LENGTH}, models::{ transactions::{Memo, Signer, Transaction, TransactionType}, - Model, XRPLModelException, XRPLModelResult, + Model, ValidateCurrencies, XRPLModelException, XRPLModelResult, }, }; @@ -45,7 +45,9 @@ pub enum NFTokenMintFlag { /// See NFTokenMint: /// `` #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[derive( + Debug, Serialize, Deserialize, PartialEq, Eq, Clone, xrpl_rust_macros::ValidateCurrencies, +)] #[serde(rename_all = "PascalCase")] pub struct NFTokenMint<'a> { // The base fields for all transaction models. @@ -92,8 +94,7 @@ impl<'a> Model for NFTokenMint<'a> { self._get_issuer_error()?; self._get_transfer_fee_error()?; self._get_uri_error()?; - - Ok(()) + self.validate_currencies() } } diff --git a/src/models/transactions/offer_cancel.rs b/src/models/transactions/offer_cancel.rs index 361fc51d..25ef8888 100644 --- a/src/models/transactions/offer_cancel.rs +++ b/src/models/transactions/offer_cancel.rs @@ -7,7 +7,7 @@ use serde_with::skip_serializing_none; use crate::models::amount::XRPAmount; use crate::models::{ transactions::{Memo, Signer, Transaction, TransactionType}, - Model, + Model, ValidateCurrencies, }; use crate::models::{FlagCollection, NoFlags}; @@ -18,7 +18,9 @@ use super::CommonFields; /// See OfferCancel: /// `` #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[derive( + Debug, Serialize, Deserialize, PartialEq, Eq, Clone, xrpl_rust_macros::ValidateCurrencies, +)] #[serde(rename_all = "PascalCase")] pub struct OfferCancel<'a> { // The base fields for all transaction models. @@ -41,7 +43,11 @@ pub struct OfferCancel<'a> { pub offer_sequence: u32, } -impl<'a> Model for OfferCancel<'a> {} +impl<'a> Model for OfferCancel<'a> { + fn get_errors(&self) -> crate::models::XRPLModelResult<()> { + self.validate_currencies() + } +} impl<'a> Transaction<'a, NoFlags> for OfferCancel<'a> { fn get_transaction_type(&self) -> &TransactionType { diff --git a/src/models/transactions/offer_create.rs b/src/models/transactions/offer_create.rs index ad6e4995..3c44bb86 100644 --- a/src/models/transactions/offer_create.rs +++ b/src/models/transactions/offer_create.rs @@ -9,7 +9,7 @@ use strum_macros::{AsRefStr, Display, EnumIter}; use crate::models::{ amount::Amount, transactions::{Memo, Signer, Transaction, TransactionType}, - Model, + Model, ValidateCurrencies, }; use crate::models::amount::XRPAmount; @@ -52,7 +52,9 @@ pub enum OfferCreateFlag { /// See OfferCreate: /// `` #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[derive( + Debug, Serialize, Deserialize, PartialEq, Eq, Clone, xrpl_rust_macros::ValidateCurrencies, +)] #[serde(rename_all = "PascalCase")] pub struct OfferCreate<'a> { // The base fields for all transaction models. @@ -79,7 +81,11 @@ pub struct OfferCreate<'a> { pub offer_sequence: Option, } -impl<'a> Model for OfferCreate<'a> {} +impl<'a> Model for OfferCreate<'a> { + fn get_errors(&self) -> crate::models::XRPLModelResult<()> { + self.validate_currencies() + } +} impl<'a> Transaction<'a, OfferCreateFlag> for OfferCreate<'a> { fn has_flag(&self, flag: &OfferCreateFlag) -> bool { diff --git a/src/models/transactions/payment.rs b/src/models/transactions/payment.rs index 374880a7..9a32059a 100644 --- a/src/models/transactions/payment.rs +++ b/src/models/transactions/payment.rs @@ -8,7 +8,7 @@ use strum_macros::{AsRefStr, Display, EnumIter}; use crate::models::{ amount::Amount, transactions::{Memo, Signer, Transaction, TransactionType}, - Model, PathStep, XRPLModelResult, + Model, PathStep, ValidateCurrencies, XRPLModelResult, }; use crate::models::amount::XRPAmount; @@ -45,7 +45,16 @@ pub enum PaymentFlag { /// See Payment: /// `` #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[derive( + Debug, + Default, + Serialize, + Deserialize, + PartialEq, + Eq, + Clone, + xrpl_rust_macros::ValidateCurrencies, +)] #[serde(rename_all = "PascalCase")] pub struct Payment<'a> { // The base fields for all transaction models. @@ -92,8 +101,7 @@ impl<'a: 'static> Model for Payment<'a> { self._get_xrp_transaction_error()?; self._get_partial_payment_error()?; self._get_exchange_error()?; - - Ok(()) + self.validate_currencies() } } diff --git a/src/models/transactions/payment_channel_claim.rs b/src/models/transactions/payment_channel_claim.rs index f430c293..38d1eafe 100644 --- a/src/models/transactions/payment_channel_claim.rs +++ b/src/models/transactions/payment_channel_claim.rs @@ -8,7 +8,7 @@ use strum_macros::{AsRefStr, Display, EnumIter}; use crate::models::{ transactions::{Memo, Signer, Transaction, TransactionType}, - Model, + Model, ValidateCurrencies, }; use crate::models::amount::XRPAmount; @@ -48,7 +48,9 @@ pub enum PaymentChannelClaimFlag { /// See PaymentChannelClaim: /// `` #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[derive( + Debug, Serialize, Deserialize, PartialEq, Eq, Clone, xrpl_rust_macros::ValidateCurrencies, +)] #[serde(rename_all = "PascalCase")] pub struct PaymentChannelClaim<'a> { // The base fields for all transaction models. @@ -88,7 +90,11 @@ pub struct PaymentChannelClaim<'a> { pub public_key: Option>, } -impl<'a> Model for PaymentChannelClaim<'a> {} +impl<'a> Model for PaymentChannelClaim<'a> { + fn get_errors(&self) -> crate::models::XRPLModelResult<()> { + self.validate_currencies() + } +} impl<'a> Transaction<'a, PaymentChannelClaimFlag> for PaymentChannelClaim<'a> { fn has_flag(&self, flag: &PaymentChannelClaimFlag) -> bool { diff --git a/src/models/transactions/payment_channel_create.rs b/src/models/transactions/payment_channel_create.rs index b5079734..35ea95a0 100644 --- a/src/models/transactions/payment_channel_create.rs +++ b/src/models/transactions/payment_channel_create.rs @@ -7,7 +7,7 @@ use serde_with::skip_serializing_none; use crate::models::amount::XRPAmount; use crate::models::{ transactions::{Memo, Signer, Transaction, TransactionType}, - Model, + Model, ValidateCurrencies, }; use crate::models::{FlagCollection, NoFlags}; @@ -18,7 +18,9 @@ use super::CommonFields; /// See PaymentChannelCreate fields: /// `` #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[derive( + Debug, Serialize, Deserialize, PartialEq, Eq, Clone, xrpl_rust_macros::ValidateCurrencies, +)] #[serde(rename_all = "PascalCase")] pub struct PaymentChannelCreate<'a> { // The base fields for all transaction models. @@ -58,7 +60,11 @@ pub struct PaymentChannelCreate<'a> { pub destination_tag: Option, } -impl<'a> Model for PaymentChannelCreate<'a> {} +impl<'a> Model for PaymentChannelCreate<'a> { + fn get_errors(&self) -> crate::models::XRPLModelResult<()> { + self.validate_currencies() + } +} impl<'a> Transaction<'a, NoFlags> for PaymentChannelCreate<'a> { fn get_transaction_type(&self) -> &TransactionType { diff --git a/src/models/transactions/payment_channel_fund.rs b/src/models/transactions/payment_channel_fund.rs index ec268787..cdca6b79 100644 --- a/src/models/transactions/payment_channel_fund.rs +++ b/src/models/transactions/payment_channel_fund.rs @@ -7,7 +7,7 @@ use serde_with::skip_serializing_none; use crate::models::{ amount::XRPAmount, transactions::{Memo, Signer, Transaction, TransactionType}, - Model, + Model, ValidateCurrencies, }; use crate::models::{FlagCollection, NoFlags}; @@ -19,7 +19,9 @@ use super::CommonFields; /// See PaymentChannelFund: /// `` #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[derive( + Debug, Serialize, Deserialize, PartialEq, Eq, Clone, xrpl_rust_macros::ValidateCurrencies, +)] #[serde(rename_all = "PascalCase")] pub struct PaymentChannelFund<'a> { // The base fields for all transaction models. @@ -50,7 +52,11 @@ pub struct PaymentChannelFund<'a> { pub expiration: Option, } -impl<'a> Model for PaymentChannelFund<'a> {} +impl<'a> Model for PaymentChannelFund<'a> { + fn get_errors(&self) -> crate::models::XRPLModelResult<()> { + self.validate_currencies() + } +} impl<'a> Transaction<'a, NoFlags> for PaymentChannelFund<'a> { fn get_transaction_type(&self) -> &TransactionType { diff --git a/src/models/transactions/pseudo_transactions/enable_amendment.rs b/src/models/transactions/pseudo_transactions/enable_amendment.rs index 8f739fda..c1f304e4 100644 --- a/src/models/transactions/pseudo_transactions/enable_amendment.rs +++ b/src/models/transactions/pseudo_transactions/enable_amendment.rs @@ -88,7 +88,7 @@ impl<'a> EnableAmendment<'a> { Self { common_fields: CommonFields::new( account, - TransactionType::EnableAmendment, + TransactionType::EnableAmendment, account_txn_id, fee, Some(flags.unwrap_or_default()), diff --git a/src/models/transactions/pseudo_transactions/set_fee.rs b/src/models/transactions/pseudo_transactions/set_fee.rs index 719875ad..988ce794 100644 --- a/src/models/transactions/pseudo_transactions/set_fee.rs +++ b/src/models/transactions/pseudo_transactions/set_fee.rs @@ -75,7 +75,7 @@ impl<'a> SetFee<'a> { Self { common_fields: CommonFields::new( account, - TransactionType::SetFee, + TransactionType::SetFee, account_txn_id, fee, Some(FlagCollection::default()), diff --git a/src/models/transactions/set_regular_key.rs b/src/models/transactions/set_regular_key.rs index fc2d6e45..66d682a8 100644 --- a/src/models/transactions/set_regular_key.rs +++ b/src/models/transactions/set_regular_key.rs @@ -7,7 +7,7 @@ use serde_with::skip_serializing_none; use crate::models::amount::XRPAmount; use crate::models::{ transactions::{Memo, Signer, Transaction, TransactionType}, - Model, + Model, ValidateCurrencies, }; use crate::models::{FlagCollection, NoFlags}; @@ -22,7 +22,9 @@ use super::CommonFields; /// See SetRegularKey: /// `` #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[derive( + Debug, Serialize, Deserialize, PartialEq, Eq, Clone, xrpl_rust_macros::ValidateCurrencies, +)] #[serde(rename_all = "PascalCase")] pub struct SetRegularKey<'a> { // The base fields for all transaction models. @@ -45,7 +47,11 @@ pub struct SetRegularKey<'a> { pub regular_key: Option>, } -impl<'a> Model for SetRegularKey<'a> {} +impl<'a> Model for SetRegularKey<'a> { + fn get_errors(&self) -> crate::models::XRPLModelResult<()> { + self.validate_currencies() + } +} impl<'a> Transaction<'a, NoFlags> for SetRegularKey<'a> { fn get_transaction_type(&self) -> &TransactionType { @@ -77,7 +83,7 @@ impl<'a> SetRegularKey<'a> { Self { common_fields: CommonFields::new( account, - TransactionType::SetRegularKey, + TransactionType::SetRegularKey, account_txn_id, fee, Some(FlagCollection::default()), diff --git a/src/models/transactions/signer_list_set.rs b/src/models/transactions/signer_list_set.rs index e2780347..5b1c5151 100644 --- a/src/models/transactions/signer_list_set.rs +++ b/src/models/transactions/signer_list_set.rs @@ -14,7 +14,7 @@ use crate::models::XRPLModelResult; use crate::models::{ amount::XRPAmount, transactions::{Memo, Signer, Transaction, TransactionType}, - Model, + Model, ValidateCurrencies, }; use crate::serde_with_tag; @@ -37,7 +37,9 @@ serde_with_tag! { /// See TicketCreate: /// `` #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[derive( + Debug, Serialize, Deserialize, PartialEq, Eq, Clone, xrpl_rust_macros::ValidateCurrencies, +)] #[serde(rename_all = "PascalCase")] pub struct SignerListSet<'a> { // The base fields for all transaction models. @@ -68,8 +70,7 @@ impl<'a> Model for SignerListSet<'a> { fn get_errors(&self) -> XRPLModelResult<()> { self._get_signer_entries_error()?; self._get_signer_quorum_error()?; - - Ok(()) + self.validate_currencies() } } diff --git a/src/models/transactions/ticket_create.rs b/src/models/transactions/ticket_create.rs index 1387c0ab..b36a04d3 100644 --- a/src/models/transactions/ticket_create.rs +++ b/src/models/transactions/ticket_create.rs @@ -6,7 +6,7 @@ use serde_with::skip_serializing_none; use crate::models::amount::XRPAmount; use crate::models::{ transactions::{Memo, Signer, Transaction, TransactionType}, - Model, + Model, ValidateCurrencies, }; use crate::models::{FlagCollection, NoFlags}; @@ -17,7 +17,9 @@ use super::CommonFields; /// See TicketCreate: /// `` #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[derive( + Debug, Serialize, Deserialize, PartialEq, Eq, Clone, xrpl_rust_macros::ValidateCurrencies, +)] #[serde(rename_all = "PascalCase")] pub struct TicketCreate<'a> { // The base fields for all transaction models. @@ -39,7 +41,11 @@ pub struct TicketCreate<'a> { pub ticket_count: u32, } -impl<'a> Model for TicketCreate<'a> {} +impl<'a> Model for TicketCreate<'a> { + fn get_errors(&self) -> crate::models::XRPLModelResult<()> { + self.validate_currencies() + } +} impl<'a> Transaction<'a, NoFlags> for TicketCreate<'a> { fn get_transaction_type(&self) -> &TransactionType { diff --git a/src/models/transactions/trust_set.rs b/src/models/transactions/trust_set.rs index 0aeb0ce1..7af1e65b 100644 --- a/src/models/transactions/trust_set.rs +++ b/src/models/transactions/trust_set.rs @@ -8,7 +8,7 @@ use strum_macros::{AsRefStr, Display, EnumIter}; use crate::models::{ transactions::{Memo, Signer, Transaction, TransactionType}, - Model, + Model, ValidateCurrencies, }; use crate::models::amount::{IssuedCurrencyAmount, XRPAmount}; @@ -43,7 +43,9 @@ pub enum TrustSetFlag { /// See TrustSet: /// `` #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[derive( + Debug, Serialize, Deserialize, PartialEq, Eq, Clone, xrpl_rust_macros::ValidateCurrencies, +)] #[serde(rename_all = "PascalCase")] pub struct TrustSet<'a> { // The base fields for all transaction models. @@ -70,7 +72,11 @@ pub struct TrustSet<'a> { pub quality_out: Option, } -impl<'a> Model for TrustSet<'a> {} +impl<'a> Model for TrustSet<'a> { + fn get_errors(&self) -> crate::models::XRPLModelResult<()> { + self.validate_currencies() + } +} impl<'a> Transaction<'a, TrustSetFlag> for TrustSet<'a> { fn has_flag(&self, flag: &TrustSetFlag) -> bool { diff --git a/src/models/transactions/xchain_account_create_commit.rs b/src/models/transactions/xchain_account_create_commit.rs index 9ec934e7..b10fb006 100644 --- a/src/models/transactions/xchain_account_create_commit.rs +++ b/src/models/transactions/xchain_account_create_commit.rs @@ -4,12 +4,14 @@ use alloc::{borrow::Cow, vec::Vec}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; -use crate::models::{Amount, FlagCollection, Model, NoFlags, XChainBridge, XRPAmount}; +use crate::models::{ + Amount, FlagCollection, Model, NoFlags, ValidateCurrencies, XChainBridge, XRPAmount, +}; use super::{CommonFields, Memo, Signer, Transaction, TransactionType}; #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, xrpl_rust_macros::ValidateCurrencies)] #[serde(rename_all = "PascalCase")] pub struct XChainAccountCreateCommit<'a> { #[serde(flatten)] @@ -21,7 +23,13 @@ pub struct XChainAccountCreateCommit<'a> { pub signature_reward: Option>, } -impl Model for XChainAccountCreateCommit<'_> {} +impl Model for XChainAccountCreateCommit<'_> { + fn get_errors(&self) -> crate::models::XRPLModelResult<()> { + self.validate_currencies()?; + + Ok(()) + } +} impl<'a> Transaction<'a, NoFlags> for XChainAccountCreateCommit<'a> { fn get_transaction_type(&self) -> &super::TransactionType { @@ -56,7 +64,7 @@ impl<'a> XChainAccountCreateCommit<'a> { XChainAccountCreateCommit { common_fields: CommonFields::new( account, - TransactionType::XChainAccountCreateCommit, + TransactionType::XChainAccountCreateCommit, account_txn_id, fee, Some(FlagCollection::default()), @@ -79,29 +87,131 @@ impl<'a> XChainAccountCreateCommit<'a> { } #[cfg(test)] -mod test_serde { +mod test { use super::XChainAccountCreateCommit; + use crate::models::{IssuedCurrency, XChainBridge, XRPAmount, XRP}; + use alloc::borrow::Cow; + + use super::*; + + const ACCOUNT: &str = "r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ"; + const ACCOUNT2: &str = "rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo"; + const ISSUER: &str = "rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf"; + const GENESIS: &str = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + + fn xrp_bridge<'a>() -> XChainBridge<'a> { + XChainBridge { + locking_chain_door: Cow::Borrowed(ACCOUNT), + locking_chain_issue: XRP::new().into(), + issuing_chain_door: Cow::Borrowed(GENESIS), + issuing_chain_issue: XRP::new().into(), + } + } + + fn iou_bridge<'a>() -> XChainBridge<'a> { + XChainBridge { + locking_chain_door: Cow::Borrowed(ACCOUNT), + locking_chain_issue: IssuedCurrency { + currency: Cow::Borrowed("USD"), + issuer: Cow::Borrowed(ISSUER), + } + .into(), + issuing_chain_door: Cow::Borrowed(ACCOUNT2), + issuing_chain_issue: IssuedCurrency { + currency: Cow::Borrowed("USD"), + issuer: Cow::Borrowed(ACCOUNT2), + } + .into(), + } + } #[test] fn test_deserialize() { let json = r#"{ - "Account": "rwEqJ2UaQHe7jihxGqmx6J4xdbGiiyMaGa", - "Destination": "rD323VyRjgzzhY4bFpo44rmyh2neB5d8Mo", - "TransactionType": "XChainAccountCreateCommit", - "Amount": "20000000", - "SignatureReward": "100", - "XChainBridge": { - "LockingChainDoor": "rMAXACCrp3Y8PpswXcg3bKggHX76V3F8M4", - "LockingChainIssue": { - "currency": "XRP" - }, - "IssuingChainDoor": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", - "IssuingChainIssue": { - "currency": "XRP" + "Account": "rwEqJ2UaQHe7jihxGqmx6J4xdbGiiyMaGa", + "Destination": "rD323VyRjgzzhY4bFpo44rmyh2neB5d8Mo", + "TransactionType": "XChainAccountCreateCommit", + "Amount": "20000000", + "SignatureReward": "100", + "XChainBridge": { + "LockingChainDoor": "rMAXACCrp3Y8PpswXcg3bKggHX76V3F8M4", + "LockingChainIssue": { + "currency": "XRP" + }, + "IssuingChainDoor": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "IssuingChainIssue": { + "currency": "XRP" + } } - } - }"#; + }"#; let txn: XChainAccountCreateCommit<'_> = serde_json::from_str(json).unwrap(); assert_eq!(txn.amount, "20000000".into()); } + + #[test] + fn test_successful() { + let txn = XChainAccountCreateCommit::new( + Cow::Borrowed(ACCOUNT), + None, + None, + None, + None, + None, + None, + None, + None, + XRPAmount::from("1000000").into(), + Cow::Borrowed(ACCOUNT2), + xrp_bridge(), + Some(XRPAmount::from("200").into()), + ); + assert_eq!(txn.amount, XRPAmount::from("1000000").into()); + assert_eq!(txn.signature_reward, Some(XRPAmount::from("200").into())); + } + + #[test] + #[should_panic] + fn test_bad_signature_reward() { + // Simulate a bad signature_reward by using a non-numeric string if your Amount type panics or errors on parse + let tx = XChainAccountCreateCommit::new( + Cow::Borrowed(ACCOUNT), + None, + None, + None, + None, + None, + None, + None, + None, + XRPAmount::from("1000000").into(), + Cow::Borrowed(ACCOUNT2), + xrp_bridge(), + Some(XRPAmount::from("hello").into()), // Should error + ); + + tx.validate().unwrap(); + } + + #[test] + #[should_panic] + fn test_bad_amount() { + // Simulate a bad amount by using a non-numeric string if your Amount type panics or errors on parse + let tx = XChainAccountCreateCommit::new( + Cow::Borrowed(ACCOUNT), + None, + None, + None, + None, + None, + None, + None, + None, + XRPAmount::from("hello").into(), // Should error + Cow::Borrowed(ACCOUNT2), + xrp_bridge(), + Some(XRPAmount::from("200").into()), + ); + + tx.validate().unwrap(); + } } diff --git a/src/models/transactions/xchain_add_account_create_attestation.rs b/src/models/transactions/xchain_add_account_create_attestation.rs index d3f080fe..9bb3c0ba 100644 --- a/src/models/transactions/xchain_add_account_create_attestation.rs +++ b/src/models/transactions/xchain_add_account_create_attestation.rs @@ -2,12 +2,14 @@ use alloc::{borrow::Cow, vec::Vec}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; -use crate::models::{Amount, FlagCollection, Model, NoFlags, XChainBridge, XRPAmount}; +use crate::models::{ + Amount, FlagCollection, Model, NoFlags, ValidateCurrencies, XChainBridge, XRPAmount, +}; use super::{CommonFields, Memo, Signer, Transaction, TransactionType}; #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, xrpl_rust_macros::ValidateCurrencies)] #[serde(rename_all = "PascalCase")] pub struct XChainAddAccountCreateAttestation<'a> { #[serde(flatten)] @@ -27,7 +29,11 @@ pub struct XChainAddAccountCreateAttestation<'a> { pub xchain_bridge: XChainBridge<'a>, } -impl Model for XChainAddAccountCreateAttestation<'_> {} +impl Model for XChainAddAccountCreateAttestation<'_> { + fn get_errors(&self) -> crate::models::XRPLModelResult<()> { + self.validate_currencies() + } +} impl<'a> Transaction<'a, NoFlags> for XChainAddAccountCreateAttestation<'a> { fn get_transaction_type(&self) -> &super::TransactionType { @@ -147,3 +153,186 @@ mod test_serde { assert_eq!(actual, expected); } } + +#[cfg(test)] +mod test_xchain_claim { + use crate::models::{ + transactions::xchain_claim::XChainClaim, Amount, IssuedCurrency, IssuedCurrencyAmount, + Model, XChainBridge, XRPAmount, XRP, + }; + use alloc::{borrow::Cow, string::ToString}; + + const ACCOUNT: &str = "r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ"; + const ACCOUNT2: &str = "rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo"; + const FEE: &str = "0.00001"; + const SEQUENCE: u32 = 19048; + const ISSUER: &str = "rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf"; + const GENESIS: &str = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + const DESTINATION: &str = "rJrRMgiRgrU6hDF4pgu5DXQdWyPbY35ErN"; + const CLAIM_ID: u64 = 3; + const XRP_AMOUNT: &str = "123456789"; + + fn xrp_bridge<'a>() -> XChainBridge<'a> { + XChainBridge { + locking_chain_door: Cow::Borrowed(ACCOUNT), + locking_chain_issue: XRP::new().into(), + issuing_chain_door: Cow::Borrowed(GENESIS), + issuing_chain_issue: XRP::new().into(), + } + } + + fn iou_bridge<'a>() -> XChainBridge<'a> { + XChainBridge { + locking_chain_door: Cow::Borrowed(ACCOUNT), + locking_chain_issue: IssuedCurrency { + currency: Cow::Borrowed("USD"), + issuer: Cow::Borrowed(ISSUER), + } + .into(), + issuing_chain_door: Cow::Borrowed(ACCOUNT2), + issuing_chain_issue: IssuedCurrency { + currency: Cow::Borrowed("USD"), + issuer: Cow::Borrowed(ACCOUNT2), + } + .into(), + } + } + + fn iou_amount<'a>() -> Amount<'a> { + IssuedCurrencyAmount { + currency: Cow::Borrowed("USD"), + issuer: Cow::Borrowed(ISSUER), + value: Cow::Borrowed("123"), + } + .into() + } + + #[test] + fn test_successful_claim_xrp() { + let claim = XChainClaim::new( + Cow::Borrowed(ACCOUNT), + None, + Some(XRPAmount::from(FEE)), + None, + None, + Some(SEQUENCE), + None, + None, + None, + XRPAmount::from(XRP_AMOUNT).into(), + Cow::Borrowed(DESTINATION), + xrp_bridge(), + CLAIM_ID.to_string().into(), + None, + ); + assert!(claim.validate().is_ok()); + } + + #[test] + fn test_successful_claim_iou() { + let claim = XChainClaim::new( + Cow::Borrowed(ACCOUNT), + None, + Some(XRPAmount::from(FEE)), + None, + None, + Some(SEQUENCE), + None, + None, + None, + iou_amount(), + Cow::Borrowed(DESTINATION), + iou_bridge(), + CLAIM_ID.to_string().into(), + None, + ); + assert!(claim.validate().is_ok()); + } + + #[test] + fn test_successful_claim_destination_tag() { + let claim = XChainClaim::new( + Cow::Borrowed(ACCOUNT), + None, + Some(XRPAmount::from(FEE)), + None, + None, + Some(SEQUENCE), + None, + Some(12345), + None, + XRPAmount::from(XRP_AMOUNT).into(), + Cow::Borrowed(DESTINATION), + xrp_bridge(), + CLAIM_ID.to_string().into(), + None, + ); + assert!(claim.validate().is_ok()); + } + + #[test] + fn test_successful_claim_str_claim_id() { + let claim_id_str = CLAIM_ID.to_string(); + let claim = XChainClaim::new( + Cow::Borrowed(ACCOUNT), + None, + Some(XRPAmount::from(FEE)), + None, + None, + Some(SEQUENCE), + None, + None, + None, + XRPAmount::from(XRP_AMOUNT).into(), + Cow::Borrowed(DESTINATION), + xrp_bridge(), + claim_id_str.as_str().into(), + None, + ); + assert!(claim.validate().is_ok()); + } + + #[test] + #[should_panic] + fn test_xrp_bridge_iou_amount() { + let claim = XChainClaim::new( + Cow::Borrowed(ACCOUNT), + None, + Some(XRPAmount::from(FEE)), + None, + None, + Some(SEQUENCE), + None, + None, + None, + iou_amount(), + Cow::Borrowed(DESTINATION), + xrp_bridge(), + CLAIM_ID.to_string().into(), + None, + ); + claim.validate().unwrap(); + } + + #[test] + #[should_panic] + fn test_iou_bridge_xrp_amount() { + let claim = XChainClaim::new( + Cow::Borrowed(ACCOUNT), + None, + Some(XRPAmount::from(FEE)), + None, + None, + Some(SEQUENCE), + None, + None, + None, + XRPAmount::from(XRP_AMOUNT).into(), + Cow::Borrowed(DESTINATION), + iou_bridge(), + CLAIM_ID.to_string().into(), + None, + ); + claim.validate().unwrap(); + } +} diff --git a/src/models/transactions/xchain_add_claim_attestation.rs b/src/models/transactions/xchain_add_claim_attestation.rs index 674aa5f9..41e87c37 100644 --- a/src/models/transactions/xchain_add_claim_attestation.rs +++ b/src/models/transactions/xchain_add_claim_attestation.rs @@ -2,12 +2,12 @@ use alloc::{borrow::Cow, vec::Vec}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; -use crate::models::{Amount, FlagCollection, Model, NoFlags, XChainBridge}; +use crate::models::{Amount, FlagCollection, Model, NoFlags, ValidateCurrencies, XChainBridge}; use super::{CommonFields, Transaction, TransactionType}; #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, xrpl_rust_macros::ValidateCurrencies)] #[serde(rename_all = "PascalCase")] pub struct XChainAddClaimAttestation<'a> { #[serde(flatten)] @@ -26,7 +26,11 @@ pub struct XChainAddClaimAttestation<'a> { pub destination: Option>, } -impl Model for XChainAddClaimAttestation<'_> {} +impl Model for XChainAddClaimAttestation<'_> { + fn get_errors(&self) -> crate::models::XRPLModelResult<()> { + self.validate_currencies() + } +} impl<'a> Transaction<'a, NoFlags> for XChainAddClaimAttestation<'a> { fn get_transaction_type(&self) -> &super::TransactionType { diff --git a/src/models/transactions/xchain_claim.rs b/src/models/transactions/xchain_claim.rs index 03a54411..e27bc1e2 100644 --- a/src/models/transactions/xchain_claim.rs +++ b/src/models/transactions/xchain_claim.rs @@ -4,13 +4,13 @@ use serde_with::skip_serializing_none; use crate::models::{ transactions::exceptions::XRPLXChainClaimException, Amount, Currency, FlagCollection, Model, - NoFlags, XChainBridge, XRPLModelResult, + NoFlags, ValidateCurrencies, XChainBridge, XRPLModelResult, }; use super::{CommonFields, Memo, Signer, Transaction, TransactionType}; #[skip_serializing_none] -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, xrpl_rust_macros::ValidateCurrencies)] #[serde(rename_all = "PascalCase")] pub struct XChainClaim<'a> { #[serde(flatten)] @@ -27,6 +27,7 @@ pub struct XChainClaim<'a> { impl Model for XChainClaim<'_> { fn get_errors(&self) -> XRPLModelResult<()> { + self.validate_currencies()?; self.get_amount_mismatch_error() } } diff --git a/src/models/transactions/xchain_commit.rs b/src/models/transactions/xchain_commit.rs index 507a1c09..56ecbc06 100644 --- a/src/models/transactions/xchain_commit.rs +++ b/src/models/transactions/xchain_commit.rs @@ -2,12 +2,14 @@ use alloc::{borrow::Cow, vec::Vec}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; -use crate::models::{Amount, FlagCollection, Model, NoFlags, XChainBridge, XRPAmount}; +use crate::models::{ + Amount, FlagCollection, Model, NoFlags, ValidateCurrencies, XChainBridge, XRPAmount, +}; use super::{CommonFields, Memo, Signer, Transaction, TransactionType}; #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, xrpl_rust_macros::ValidateCurrencies)] #[serde(rename_all = "PascalCase")] pub struct XChainCommit<'a> { #[serde(flatten)] @@ -20,7 +22,11 @@ pub struct XChainCommit<'a> { pub other_chain_destination: Option>, } -impl Model for XChainCommit<'_> {} +impl Model for XChainCommit<'_> { + fn get_errors(&self) -> crate::models::XRPLModelResult<()> { + self.validate_currencies() + } +} impl<'a> Transaction<'a, NoFlags> for XChainCommit<'a> { fn get_common_fields(&self) -> &CommonFields<'_, NoFlags> { diff --git a/src/models/transactions/xchain_create_bridge.rs b/src/models/transactions/xchain_create_bridge.rs index 03492275..0eea8c05 100644 --- a/src/models/transactions/xchain_create_bridge.rs +++ b/src/models/transactions/xchain_create_bridge.rs @@ -4,13 +4,13 @@ use serde_with::skip_serializing_none; use crate::models::{ transactions::exceptions::XRPLXChainCreateBridgeException, Amount, FlagCollection, Model, - NoFlags, XChainBridge, XRPAmount, XRPLModelResult, XRP, + NoFlags, ValidateCurrencies, XChainBridge, XRPAmount, XRPLModelResult, XRP, }; use super::{CommonFields, Memo, Signer, Transaction, TransactionType}; #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, xrpl_rust_macros::ValidateCurrencies)] #[serde(rename_all = "PascalCase")] pub struct XChainCreateBridge<'a> { #[serde(flatten)] @@ -23,6 +23,7 @@ pub struct XChainCreateBridge<'a> { impl Model for XChainCreateBridge<'_> { fn get_errors(&self) -> XRPLModelResult<()> { + self.validate_currencies()?; self.get_same_door_error()?; self.get_account_door_mismatch_error()?; self.get_cross_currency_bridge_not_allowed_error()?; @@ -93,7 +94,7 @@ impl<'a> XChainCreateBridge<'a> { fn get_account_door_mismatch_error(&self) -> XRPLModelResult<()> { let bridge = &self.xchain_bridge; - if [&bridge.issuing_chain_door, &bridge.locking_chain_door] + if ![&bridge.issuing_chain_door, &bridge.locking_chain_door] .contains(&&self.common_fields.account) { Err(XRPLXChainCreateBridgeException::AccountDoorMismatch.into()) @@ -124,3 +125,266 @@ impl<'a> XChainCreateBridge<'a> { } } } + +#[cfg(test)] +mod test_xchain_create_bridge { + use super::XChainCreateBridge; + use crate::models::{Amount, IssuedCurrency, Model, XChainBridge, XRPAmount, XRP}; + use alloc::borrow::Cow; + + const ACCOUNT: &str = "r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ"; + const ACCOUNT2: &str = "rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo"; + const FEE: &str = "0.00001"; + const SEQUENCE: u32 = 19048; + const ISSUER: &str = "rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf"; + const GENESIS: &str = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + + fn xrp_bridge<'a>() -> XChainBridge<'a> { + XChainBridge { + locking_chain_door: Cow::Borrowed(ACCOUNT), + locking_chain_issue: XRP::new().into(), + issuing_chain_door: Cow::Borrowed(GENESIS), + issuing_chain_issue: XRP::new().into(), + } + } + + fn iou_bridge<'a>() -> XChainBridge<'a> { + XChainBridge { + locking_chain_door: Cow::Borrowed(ACCOUNT), + locking_chain_issue: IssuedCurrency { + currency: Cow::Borrowed("USD"), + issuer: Cow::Borrowed(ISSUER), + } + .into(), + issuing_chain_door: Cow::Borrowed(ACCOUNT2), + issuing_chain_issue: IssuedCurrency { + currency: Cow::Borrowed("USD"), + issuer: Cow::Borrowed(ACCOUNT2), + } + .into(), + } + } + + #[test] + fn test_successful_xrp_xrp_bridge() { + let bridge = xrp_bridge(); + let txn = XChainCreateBridge::new( + Cow::Borrowed(ACCOUNT), + None, + Some(XRPAmount::from(FEE)), + None, + None, + Some(SEQUENCE), + None, + None, + None, + XRPAmount::from("200").into(), + bridge, + Some(XRPAmount::from("1000000")), + ); + assert!(txn.validate().is_ok()); + } + + #[test] + fn test_successful_iou_iou_bridge() { + let bridge = iou_bridge(); + let txn = XChainCreateBridge::new( + Cow::Borrowed(ACCOUNT), + None, + Some(XRPAmount::from(FEE)), + None, + None, + Some(SEQUENCE), + None, + None, + None, + XRPAmount::from("200").into(), + bridge, + None, + ); + assert!(txn.validate().is_ok()); + } + + #[test] + #[should_panic] + fn test_same_door_accounts() { + let bridge = XChainBridge { + locking_chain_door: Cow::Borrowed(ACCOUNT), + locking_chain_issue: IssuedCurrency { + currency: Cow::Borrowed("USD"), + issuer: Cow::Borrowed(ISSUER), + } + .into(), + issuing_chain_door: Cow::Borrowed(ACCOUNT), + issuing_chain_issue: IssuedCurrency { + currency: Cow::Borrowed("USD"), + issuer: Cow::Borrowed(ACCOUNT), + } + .into(), + }; + let txn = XChainCreateBridge::new( + Cow::Borrowed(ACCOUNT), + None, + Some(XRPAmount::from(FEE)), + None, + None, + Some(SEQUENCE), + None, + None, + None, + XRPAmount::from("200").into(), + bridge, + None, + ); + txn.validate().unwrap(); + } + + #[test] + #[should_panic] + fn test_xrp_iou_bridge() { + let bridge = XChainBridge { + locking_chain_door: Cow::Borrowed(ACCOUNT), + locking_chain_issue: XRP::new().into(), + issuing_chain_door: Cow::Borrowed(ACCOUNT), + issuing_chain_issue: IssuedCurrency { + currency: Cow::Borrowed("USD"), + issuer: Cow::Borrowed(ACCOUNT), + } + .into(), + }; + let txn = XChainCreateBridge::new( + Cow::Borrowed(ACCOUNT), + None, + Some(XRPAmount::from(FEE)), + None, + None, + Some(SEQUENCE), + None, + None, + None, + XRPAmount::from("200").into(), + bridge, + None, + ); + txn.validate().unwrap(); + } + + #[test] + #[should_panic] + fn test_iou_xrp_bridge() { + let bridge = XChainBridge { + locking_chain_door: Cow::Borrowed(ACCOUNT), + locking_chain_issue: IssuedCurrency { + currency: Cow::Borrowed("USD"), + issuer: Cow::Borrowed(ISSUER), + } + .into(), + issuing_chain_door: Cow::Borrowed(ACCOUNT), + issuing_chain_issue: XRP::new().into(), + }; + let txn = XChainCreateBridge::new( + Cow::Borrowed(ACCOUNT), + None, + Some(XRPAmount::from(FEE)), + None, + None, + Some(SEQUENCE), + None, + None, + None, + XRPAmount::from("200").into(), + bridge, + None, + ); + txn.validate().unwrap(); + } + + #[test] + #[should_panic] + fn test_account_not_in_bridge() { + let bridge = XChainBridge { + locking_chain_door: Cow::Borrowed(ACCOUNT), + locking_chain_issue: XRP::new().into(), + issuing_chain_door: Cow::Borrowed(ACCOUNT2), + issuing_chain_issue: XRP::new().into(), + }; + let txn = XChainCreateBridge::new( + Cow::Borrowed(GENESIS), + None, + Some(XRPAmount::from(FEE)), + None, + None, + Some(SEQUENCE), + None, + None, + None, + XRPAmount::from("200").into(), + bridge, + None, + ); + txn.validate().unwrap(); + } + + #[test] + #[should_panic] + fn test_iou_iou_min_account_create_amount() { + let bridge = iou_bridge(); + let txn = XChainCreateBridge::new( + Cow::Borrowed(ACCOUNT), + None, + Some(XRPAmount::from(FEE)), + None, + None, + Some(SEQUENCE), + None, + None, + None, + XRPAmount::from("200").into(), + bridge, + Some(XRPAmount::from("1000000")), + ); + txn.validate().unwrap(); + } + + #[test] + #[should_panic] + fn test_invalid_signature_reward() { + let bridge = xrp_bridge(); + let txn = XChainCreateBridge::new( + Cow::Borrowed(ACCOUNT), + None, + Some(XRPAmount::from(FEE)), + None, + None, + Some(SEQUENCE), + None, + None, + None, + Amount::from("hello"), + bridge, + Some(XRPAmount::from("1000000")), + ); + txn.validate().unwrap(); + } + + #[test] + #[should_panic] + fn test_invalid_min_account_create_amount() { + let bridge = xrp_bridge(); + let txn = XChainCreateBridge::new( + Cow::Borrowed(ACCOUNT), + None, + Some(XRPAmount::from(FEE)), + None, + None, + Some(SEQUENCE), + None, + None, + None, + Amount::from("-200"), + bridge, + Some(XRPAmount::from("hello")), + ); + txn.validate().unwrap(); + } +} diff --git a/src/models/transactions/xchain_create_claim_id.rs b/src/models/transactions/xchain_create_claim_id.rs index bd474729..6d570d40 100644 --- a/src/models/transactions/xchain_create_claim_id.rs +++ b/src/models/transactions/xchain_create_claim_id.rs @@ -6,26 +6,27 @@ use crate::{ core::addresscodec::is_valid_classic_address, models::{ transactions::exceptions::XRPLXChainCreateClaimIDException, FlagCollection, Model, NoFlags, - XChainBridge, XRPAmount, XRPLModelResult, + ValidateCurrencies, XChainBridge, XRPAmount, XRPLModelResult, }, }; use super::{CommonFields, Memo, Signer, Transaction, TransactionType}; #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, xrpl_rust_macros::ValidateCurrencies)] #[serde(rename_all = "PascalCase")] pub struct XChainCreateClaimID<'a> { #[serde(flatten)] pub common_fields: CommonFields<'a, NoFlags>, pub other_chain_source: Cow<'a, str>, - pub signature_reward: Cow<'a, str>, + pub signature_reward: XRPAmount<'a>, #[serde(rename = "XChainBridge")] pub xchain_bridge: XChainBridge<'a>, } impl Model for XChainCreateClaimID<'_> { fn get_errors(&self) -> XRPLModelResult<()> { + self.validate_currencies()?; self.get_other_chain_source_is_invalid_error() } } @@ -56,13 +57,13 @@ impl<'a> XChainCreateClaimID<'a> { source_tag: Option, ticket_sequence: Option, other_chain_source: Cow<'a, str>, - signature_reward: Cow<'a, str>, + signature_reward: XRPAmount<'a>, xchain_bridge: XChainBridge<'a>, ) -> XChainCreateClaimID<'a> { XChainCreateClaimID { common_fields: CommonFields::new( account, - TransactionType::XChainCreateClaimID, + TransactionType::XChainCreateClaimID, account_txn_id, fee, Some(FlagCollection::default()), @@ -90,3 +91,85 @@ impl<'a> XChainCreateClaimID<'a> { } } } + +#[cfg(test)] +mod test_xchain_create_claim_id { + use super::XChainCreateClaimID; + use crate::models::{Model, XChainBridge, XRP}; + use alloc::borrow::Cow; + + const ACCOUNT: &str = "r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ"; + const ACCOUNT2: &str = "rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo"; + const ISSUER: &str = "rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf"; + const GENESIS: &str = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + const SOURCE: &str = "rJrRMgiRgrU6hDF4pgu5DXQdWyPbY35ErN"; + const SIGNATURE_REWARD: &str = "200"; + + fn xrp_bridge<'a>() -> XChainBridge<'a> { + XChainBridge { + locking_chain_door: Cow::Borrowed(ACCOUNT), + locking_chain_issue: XRP::new().into(), + issuing_chain_door: Cow::Borrowed(GENESIS), + issuing_chain_issue: XRP::new().into(), + } + } + + #[test] + fn test_successful() { + let txn = XChainCreateClaimID::new( + Cow::Borrowed(ACCOUNT), + None, + None, + None, + None, + None, + None, + None, + None, + Cow::Borrowed(SOURCE), + Cow::Borrowed(SIGNATURE_REWARD).into(), + xrp_bridge(), + ); + assert!(txn.validate().is_ok()); + } + + #[test] + #[should_panic] + fn test_bad_signature_reward() { + let txn = XChainCreateClaimID::new( + Cow::Borrowed(ACCOUNT), + None, + None, + None, + None, + None, + None, + None, + None, + Cow::Borrowed(SOURCE), + Cow::Borrowed("hello").into(), + xrp_bridge(), + ); + txn.validate().unwrap(); + } + + #[test] + #[should_panic] + fn test_bad_other_chain_source() { + let txn = XChainCreateClaimID::new( + Cow::Borrowed(ACCOUNT), + None, + None, + None, + None, + None, + None, + None, + None, + Cow::Borrowed("hello"), + Cow::Borrowed(SIGNATURE_REWARD).into(), + xrp_bridge(), + ); + txn.validate().unwrap(); + } +} diff --git a/src/models/transactions/xchain_modify_bridge.rs b/src/models/transactions/xchain_modify_bridge.rs index 33be343b..a8aedeb7 100644 --- a/src/models/transactions/xchain_modify_bridge.rs +++ b/src/models/transactions/xchain_modify_bridge.rs @@ -6,7 +6,7 @@ use strum_macros::{AsRefStr, Display, EnumIter}; use crate::models::{ transactions::exceptions::XRPLXChainModifyBridgeException, Amount, FlagCollection, Model, - XChainBridge, XRPAmount, XRPLModelResult, XRP, + ValidateCurrencies, XChainBridge, XRPAmount, XRPLModelResult, XRP, }; use super::{CommonFields, Memo, Signer, Transaction, TransactionType}; @@ -21,7 +21,7 @@ pub enum XChainModifyBridgeFlags { } #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, xrpl_rust_macros::ValidateCurrencies)] #[serde(rename_all = "PascalCase")] pub struct XChainModifyBridge<'a> { #[serde(flatten)] @@ -34,6 +34,7 @@ pub struct XChainModifyBridge<'a> { impl Model for XChainModifyBridge<'_> { fn get_errors(&self) -> XRPLModelResult<()> { + self.validate_currencies()?; self.get_must_change_or_clear_error()?; self.get_account_door_mismatch_error()?; self.get_cannot_have_min_account_create_amount()?; @@ -107,7 +108,7 @@ impl<'a> XChainModifyBridge<'a> { fn get_account_door_mismatch_error(&self) -> XRPLModelResult<()> { let bridge = &self.xchain_bridge; - if [&bridge.locking_chain_door, &bridge.issuing_chain_door] + if ![&bridge.locking_chain_door, &bridge.issuing_chain_door] .contains(&&self.get_common_fields().account) { Err(XRPLXChainModifyBridgeException::AccountDoorMismatch.into()) @@ -127,3 +128,208 @@ impl<'a> XChainModifyBridge<'a> { } } } + +#[cfg(test)] +mod test_xchain_modify_bridge { + use super::XChainModifyBridge; + use crate::models::{Amount, IssuedCurrency, Model, XChainBridge, XRPAmount, XRP}; + use alloc::borrow::Cow; + + const ACCOUNT: &str = "r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ"; + const ACCOUNT2: &str = "rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo"; + const FEE: &str = "0.00001"; + const SEQUENCE: u32 = 19048; + const ISSUER: &str = "rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf"; + const GENESIS: &str = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + + fn xrp_bridge<'a>() -> XChainBridge<'a> { + XChainBridge { + locking_chain_door: Cow::Borrowed(ACCOUNT), + locking_chain_issue: XRP::new().into(), + issuing_chain_door: Cow::Borrowed(GENESIS), + issuing_chain_issue: XRP::new().into(), + } + } + + fn iou_bridge<'a>() -> XChainBridge<'a> { + XChainBridge { + locking_chain_door: Cow::Borrowed(ACCOUNT), + locking_chain_issue: IssuedCurrency { + currency: Cow::Borrowed("USD"), + issuer: Cow::Borrowed(ISSUER), + } + .into(), + issuing_chain_door: Cow::Borrowed(ACCOUNT2), + issuing_chain_issue: IssuedCurrency { + currency: Cow::Borrowed("USD"), + issuer: Cow::Borrowed(ACCOUNT2), + } + .into(), + } + } + + #[test] + fn test_successful_modify_bridge() { + let txn = XChainModifyBridge::new( + Cow::Borrowed(ACCOUNT), + None, + Some(XRPAmount::from(FEE)), + None, + None, + None, + Some(SEQUENCE), + None, + None, + None, + xrp_bridge(), + Some(XRPAmount::from("1000000").into()), + Some(XRPAmount::from("200").into()), + ); + assert!(txn.validate().is_ok()); + } + + #[test] + fn test_successful_modify_bridge_only_signature_reward() { + let txn = XChainModifyBridge::new( + Cow::Borrowed(ACCOUNT), + None, + Some(XRPAmount::from(FEE)), + None, + None, + None, + Some(SEQUENCE), + None, + None, + None, + iou_bridge(), + None, + Some(XRPAmount::from("200").into()), + ); + assert!(txn.validate().is_ok()); + } + + #[test] + fn test_successful_modify_bridge_only_min_account_create_amount() { + let txn = XChainModifyBridge::new( + Cow::Borrowed(ACCOUNT), + None, + Some(XRPAmount::from(FEE)), + None, + None, + None, + Some(SEQUENCE), + None, + None, + None, + xrp_bridge(), + Some(XRPAmount::from("1000000").into()), + None, + ); + assert!(txn.validate().is_ok()); + } + + #[test] + #[should_panic] + fn test_modify_bridge_empty() { + let txn = XChainModifyBridge::new( + Cow::Borrowed(ACCOUNT), + None, + Some(XRPAmount::from(FEE)), + None, + None, + None, + Some(SEQUENCE), + None, + None, + None, + iou_bridge(), + None, + None, + ); + txn.validate().unwrap(); + } + + #[test] + #[should_panic] + fn test_account_not_in_bridge() { + let txn = XChainModifyBridge::new( + Cow::Borrowed(ACCOUNT2), + None, + Some(XRPAmount::from(FEE)), + None, + None, + None, + Some(SEQUENCE), + None, + None, + None, + xrp_bridge(), + None, + Some(XRPAmount::from("200").into()), + ); + txn.validate().unwrap(); + } + + #[test] + #[should_panic] + fn test_iou_iou_min_account_create_amount() { + let txn = XChainModifyBridge::new( + Cow::Borrowed(ACCOUNT), + None, + Some(XRPAmount::from(FEE)), + None, + None, + None, + Some(SEQUENCE), + None, + None, + None, + iou_bridge(), + Some(XRPAmount::from("1000000").into()), + None, + ); + txn.validate().unwrap(); + } + + #[test] + #[should_panic] + fn test_invalid_signature_reward() { + let txn = XChainModifyBridge::new( + Cow::Borrowed(ACCOUNT), + None, + Some(XRPAmount::from(FEE)), + None, + None, + None, + Some(SEQUENCE), + None, + None, + None, + xrp_bridge(), + Some(XRPAmount::from("1000000").into()), + Some(Amount::from("hello")), + ); + txn.validate().unwrap(); + } + + #[test] + #[should_panic] + fn test_invalid_min_account_create_amount() { + let txn = XChainModifyBridge::new( + Cow::Borrowed(ACCOUNT), + None, + Some(XRPAmount::from(FEE)), + None, + None, + None, + Some(SEQUENCE), + None, + None, + None, + xrp_bridge(), + Some(Amount::from("hello")), + Some(XRPAmount::from("200").into()), + ); + txn.validate().unwrap(); + } +} diff --git a/xrpl-rust-macros/Cargo.toml b/xrpl-rust-macros/Cargo.toml new file mode 100644 index 00000000..dfbef813 --- /dev/null +++ b/xrpl-rust-macros/Cargo.toml @@ -0,0 +1,12 @@ +[lib] +proc-macro = true + +[package] +name = "xrpl-rust-macros" +version = "0.1.0" +edition = "2024" + +[dependencies] +syn = "2.0" +quote = "1.0" +proc-macro2 = "1.0" diff --git a/xrpl-rust-macros/src/lib.rs b/xrpl-rust-macros/src/lib.rs new file mode 100644 index 00000000..74b337b8 --- /dev/null +++ b/xrpl-rust-macros/src/lib.rs @@ -0,0 +1,107 @@ +#![no_std] + +extern crate alloc; +extern crate proc_macro; + +use proc_macro::TokenStream; +use quote::quote; +use syn::{Data, DeriveInput, Fields, Type, parse_macro_input}; + +/// Derive macro to implement `ValidateCurrencies` trait for structs. +/// This macro checks for fields of type `Amount`, `XRPAmount`, `IssuedCurrencyAmount`, `Currency`, `XRP`, or `IssuedCurrency`. +/// It generates a `validate_currencies` method that validates these values. +#[proc_macro_derive(ValidateCurrencies)] +pub fn derive_validate_currencies(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = input.ident; + + let fields = match input.data { + Data::Struct(data_struct) => match data_struct.fields { + Fields::Named(fields_named) => fields_named.named, + _ => { + return syn::Error::new_spanned(name, "Only named fields supported") + .to_compile_error() + .into(); + } + }, + _ => { + return syn::Error::new_spanned(name, "Only structs are supported") + .to_compile_error() + .into(); + } + }; + + let amount_field_validations = fields.iter().filter_map(|field| { + let ident = &field.ident; + match &field.ty { + // Handle Option where T is one of the valid types + Type::Path(type_path) => { + use alloc::string::ToString; + let segments = &type_path.path.segments; + if segments.len() == 1 && segments[0].ident == "Option" { + // Extract T from Option + if let syn::PathArguments::AngleBracketed(angle_bracketed) = + &segments[0].arguments + { + if let Some(syn::GenericArgument::Type(Type::Path(inner_type_path))) = + angle_bracketed.args.first() + { + let inner_ident = &inner_type_path.path.segments.last().unwrap().ident; + if [ + "Amount", + "XRPAmount", + "IssuedCurrencyAmount", + "Currency", + "XRP", + "IssuedCurrency", + ] + .contains(&inner_ident.to_string().as_str()) + { + return Some(quote! { + if let Some(x) = &self.#ident { + x.validate()?; + } + }); + } + } + } + } + + // Handle direct fields: Amount, XRPAmount, etc. + let type_ident = &segments.last().unwrap().ident; + if [ + "Amount", + "XRPAmount", + "IssuedCurrencyAmount", + "Currency", + "XRP", + "IssuedCurrency", + ] + .contains(&type_ident.to_string().as_str()) + { + return Some(quote! { + self.#ident.validate()?; + }); + } + + None + } + _ => None, + } + }); + + let generics = input.generics; + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + let expanded = quote! { + impl #impl_generics ValidateCurrencies for #name #ty_generics #where_clause { + fn validate_currencies(&self) -> crate::models::XRPLModelResult<()> { + #(#amount_field_validations)* + + Ok(()) + } + } + }; + + TokenStream::from(expanded) +}