From c3d2a05dd01a218785579a5f62d2b5968a42b0e0 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Mon, 13 Apr 2026 13:43:23 -0700 Subject: [PATCH] feat: Support XLS-39 Clawback amendment in xrpl-rust --- CHANGELOG.md | 1 + src/asynch/clients/async_client.rs | 2 +- src/models/exceptions.rs | 12 +- src/models/results/server_state.rs | 2 + src/models/transactions/account_set.rs | 16 +- src/models/transactions/clawback.rs | 373 +++++++++++++++++++++++++ src/models/transactions/exceptions.rs | 14 + src/models/transactions/mod.rs | 3 + tests/transactions/account_set.rs | 76 ++++- tests/transactions/clawback.rs | 142 ++++++++++ tests/transactions/mod.rs | 1 + 11 files changed, 629 insertions(+), 13 deletions(-) create mode 100644 src/models/transactions/clawback.rs create mode 100644 tests/transactions/clawback.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b985b43..ef0132ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [[Unreleased]] ### Added +- Support for `XLS-39` Clawback amendment (Amendment ID: 56B241D7A43D40354D02A9DC4C8DF5C7A1F930D92A9035C4E12291B3CA3E1C2B) ### Fixed diff --git a/src/asynch/clients/async_client.rs b/src/asynch/clients/async_client.rs index ce9781e3..f32095b1 100644 --- a/src/asynch/clients/async_client.rs +++ b/src/asynch/clients/async_client.rs @@ -17,7 +17,7 @@ pub trait XRPLAsyncClient: XRPLClient { let server_state = self.request(ServerState::new(None).into()).await?; let server_state: ServerStateResult = server_state.try_into()?; let common_fields = CommonFields { - network_id: None, // TODO Server state has no network ID. + network_id: server_state.state.network_id, build_version: Some(server_state.state.build_version), }; diff --git a/src/models/exceptions.rs b/src/models/exceptions.rs index ccbc80e5..736503cd 100644 --- a/src/models/exceptions.rs +++ b/src/models/exceptions.rs @@ -10,9 +10,9 @@ use crate::XRPLSerdeJsonError; use super::{ results::exceptions::XRPLResultException, transactions::exceptions::{ - XRPLAccountSetException, XRPLNFTokenCancelOfferException, XRPLNFTokenCreateOfferException, - XRPLPaymentException, XRPLSignerListSetException, XRPLTransactionException, - XRPLXChainClaimException, XRPLXChainCreateBridgeException, + XRPLAccountSetException, XRPLClawbackException, XRPLNFTokenCancelOfferException, + XRPLNFTokenCreateOfferException, XRPLPaymentException, XRPLSignerListSetException, + XRPLTransactionException, XRPLXChainClaimException, XRPLXChainCreateBridgeException, XRPLXChainCreateClaimIDException, XRPLXChainModifyBridgeException, }, }; @@ -152,3 +152,9 @@ impl From for XRPLModelException { XRPLModelException::XRPLTransactionError(error.into()) } } + +impl From for XRPLModelException { + fn from(error: XRPLClawbackException) -> Self { + XRPLModelException::XRPLTransactionError(error.into()) + } +} diff --git a/src/models/results/server_state.rs b/src/models/results/server_state.rs index a0211861..0519c871 100644 --- a/src/models/results/server_state.rs +++ b/src/models/results/server_state.rs @@ -40,6 +40,8 @@ pub struct State<'a> { pub peer_disconnects: Option>, /// Count of resource-related peer disconnections pub peer_disconnects_resources: Option>, + /// The network ID of the server's network + pub network_id: Option, /// Number of other rippled servers currently connected pub peers: Option, /// Public key used for peer-to-peer communications diff --git a/src/models/transactions/account_set.rs b/src/models/transactions/account_set.rs index 292bc330..65b58bb4 100644 --- a/src/models/transactions/account_set.rs +++ b/src/models/transactions/account_set.rs @@ -65,16 +65,18 @@ pub enum AccountSetFlag { /// on this account's behalf. Specify the authorized account in the /// NFTokenMinter field of the AccountRoot object. AsfAuthorizedNFTokenMinter = 10, + /// Disallow incoming NFToken offers from other accounts. + AsfDisallowIncomingNFTokenOffer = 12, /// Disallow incoming Checks from other accounts. - AsfDisallowIncomingCheck = 11, + AsfDisallowIncomingCheck = 13, /// Disallow incoming Payment Channels from other accounts. - AsfDisallowIncomingPayChan = 12, + AsfDisallowIncomingPayChan = 14, /// Disallow incoming trust lines from other accounts. - AsfDisallowIncomingTrustline = 13, - /// Disallow incoming NFToken offers from other accounts. - AsfDisallowIncomingNFTokenOffer = 14, - /// Allow other accounts to mint NFTokens with this account set as the issuer. - AsfAllowTrustLineClawback = 15, + AsfDisallowIncomingTrustline = 15, + /// Allow clawback of tokens issued by this account. + /// This flag can only be set if the account has no trust lines. + /// Once set, it cannot be unset. + AsfAllowTrustLineClawback = 16, } /// An AccountSet transaction modifies the properties of an diff --git a/src/models/transactions/clawback.rs b/src/models/transactions/clawback.rs new file mode 100644 index 00000000..d25af837 --- /dev/null +++ b/src/models/transactions/clawback.rs @@ -0,0 +1,373 @@ +use alloc::borrow::Cow; +use alloc::vec::Vec; + +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::amount::XRPAmount; +use crate::models::transactions::CommonFields; +use crate::models::{ + amount::Amount, + transactions::{Transaction, TransactionType}, + Model, ValidateCurrencies, +}; +use crate::models::{FlagCollection, NoFlags}; + +use super::exceptions::XRPLClawbackException; +use super::{CommonTransactionBuilder, Memo, Signer}; + +/// Claw back tokens from a token holder. +/// +/// The issuer can only claw back issued tokens if the issuer has set +/// the `asfAllowTrustLineClawback` flag on their account using an +/// AccountSet transaction. +/// +/// See Clawback: +/// `` +#[skip_serializing_none] +#[derive( + Debug, + Default, + Serialize, + Deserialize, + PartialEq, + Eq, + Clone, + xrpl_rust_macros::ValidateCurrencies, +)] +#[serde(rename_all = "PascalCase")] +pub struct Clawback<'a> { + /// The base fields for all transaction models. + /// + /// See Transaction Common Fields: + /// `` + #[serde(flatten)] + pub common_fields: CommonFields<'a, NoFlags>, + /// The amount of currency to claw back. For fungible tokens, the `issuer` + /// field of the Amount object is the token holder's address (the account + /// from which tokens are being clawed back). + pub amount: Amount<'a>, + /// For MPT (Multi-Purpose Token) clawback only. The address of the token + /// holder from which tokens should be clawed back. Must not be present + /// for standard issued-currency clawback. + pub holder: Option>, +} + +pub trait ClawbackError { + fn _get_amount_error(&self) -> crate::models::XRPLModelResult<()>; + fn _get_holder_error(&self) -> crate::models::XRPLModelResult<()>; +} + +impl<'a> ClawbackError for Clawback<'a> { + /// Validate that the Amount is not XRP. + fn _get_amount_error(&self) -> crate::models::XRPLModelResult<()> { + if self.amount.is_xrp() { + Err(XRPLClawbackException::AmountMustNotBeXRP.into()) + } else { + Ok(()) + } + } + + /// Validate that the Holder field is not present for standard IOU clawback. + fn _get_holder_error(&self) -> crate::models::XRPLModelResult<()> { + if let Amount::IssuedCurrencyAmount(_) = &self.amount { + if self.holder.is_some() { + return Err(XRPLClawbackException::HolderMustNotBePresentForIOU.into()); + } + } + Ok(()) + } +} + +impl<'a> Model for Clawback<'a> { + fn get_errors(&self) -> crate::models::XRPLModelResult<()> { + self._get_amount_error()?; + self._get_holder_error()?; + self.validate_currencies() + } +} + +impl<'a> Transaction<'a, NoFlags> for Clawback<'a> { + fn get_transaction_type(&self) -> &TransactionType { + self.common_fields.get_transaction_type() + } + + fn get_common_fields(&self) -> &CommonFields<'_, NoFlags> { + self.common_fields.get_common_fields() + } + + fn get_mut_common_fields(&mut self) -> &mut CommonFields<'a, NoFlags> { + self.common_fields.get_mut_common_fields() + } +} + +impl<'a> CommonTransactionBuilder<'a, NoFlags> for Clawback<'a> { + fn get_mut_common_fields(&mut self) -> &mut CommonFields<'a, NoFlags> { + &mut self.common_fields + } + + fn into_self(self) -> Self { + self + } +} + +impl<'a> Clawback<'a> { + pub fn new( + account: Cow<'a, str>, + account_txn_id: Option>, + fee: Option>, + last_ledger_sequence: Option, + memos: Option>, + sequence: Option, + signers: Option>, + source_tag: Option, + ticket_sequence: Option, + amount: Amount<'a>, + holder: Option>, + ) -> Self { + Self { + common_fields: CommonFields::new( + account, + TransactionType::Clawback, + account_txn_id, + fee, + Some(FlagCollection::default()), + last_ledger_sequence, + memos, + None, + sequence, + signers, + None, + source_tag, + ticket_sequence, + None, + ), + amount, + holder, + } + } + + pub fn with_holder(mut self, holder: Cow<'a, str>) -> Self { + self.holder = Some(holder); + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::models::amount::IssuedCurrencyAmount; + + #[test] + fn test_serde() { + let default_txn = Clawback { + common_fields: CommonFields { + account: "rp6abvbTbjoce8ZDJkT6snvxTZSYMBCC9S".into(), + transaction_type: TransactionType::Clawback, + fee: Some("12".into()), + signing_pub_key: Some("".into()), + ..Default::default() + }, + amount: Amount::IssuedCurrencyAmount(IssuedCurrencyAmount::new( + "FOO".into(), + "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW".into(), + "314.159".into(), + )), + holder: None, + }; + + let default_json_str = r#"{"Account":"rp6abvbTbjoce8ZDJkT6snvxTZSYMBCC9S","TransactionType":"Clawback","Fee":"12","Flags":0,"SigningPubKey":"","Amount":{"currency":"FOO","issuer":"rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW","value":"314.159"}}"#; + + let default_json_value = serde_json::to_value(default_json_str).unwrap(); + let serialized_string = serde_json::to_string(&default_txn).unwrap(); + let serialized_value = serde_json::to_value(&serialized_string).unwrap(); + assert_eq!(serialized_value, default_json_value); + + let deserialized: Clawback = serde_json::from_str(default_json_str).unwrap(); + assert_eq!(default_txn, deserialized); + } + + #[test] + fn test_serde_with_holder() { + let txn = Clawback { + common_fields: CommonFields { + account: "rp6abvbTbjoce8ZDJkT6snvxTZSYMBCC9S".into(), + transaction_type: TransactionType::Clawback, + fee: Some("12".into()), + signing_pub_key: Some("".into()), + ..Default::default() + }, + amount: Amount::IssuedCurrencyAmount(IssuedCurrencyAmount::new( + "FOO".into(), + "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW".into(), + "314.159".into(), + )), + holder: Some("rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW".into()), + }; + + let json_str = r#"{"Account":"rp6abvbTbjoce8ZDJkT6snvxTZSYMBCC9S","TransactionType":"Clawback","Fee":"12","Flags":0,"SigningPubKey":"","Amount":{"currency":"FOO","issuer":"rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW","value":"314.159"},"Holder":"rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW"}"#; + + let serialized_string = serde_json::to_string(&txn).unwrap(); + let serialized_value = serde_json::to_value(&serialized_string).unwrap(); + let expected_value = serde_json::to_value(json_str).unwrap(); + assert_eq!(serialized_value, expected_value); + + let deserialized: Clawback = serde_json::from_str(json_str).unwrap(); + assert_eq!(txn, deserialized); + } + + #[test] + fn test_builder_pattern() { + let clawback = Clawback { + common_fields: CommonFields { + account: "rp6abvbTbjoce8ZDJkT6snvxTZSYMBCC9S".into(), + transaction_type: TransactionType::Clawback, + ..Default::default() + }, + amount: Amount::IssuedCurrencyAmount(IssuedCurrencyAmount::new( + "FOO".into(), + "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW".into(), + "314.159".into(), + )), + ..Default::default() + } + .with_fee("12".into()) + .with_sequence(123) + .with_last_ledger_sequence(7108682) + .with_source_tag(12345); + + assert_eq!( + clawback.common_fields.account, + "rp6abvbTbjoce8ZDJkT6snvxTZSYMBCC9S" + ); + assert_eq!(clawback.common_fields.fee.as_ref().unwrap().0, "12"); + assert_eq!(clawback.common_fields.sequence, Some(123)); + assert_eq!(clawback.common_fields.last_ledger_sequence, Some(7108682)); + assert_eq!(clawback.common_fields.source_tag, Some(12345)); + assert_eq!( + clawback.common_fields.transaction_type, + TransactionType::Clawback + ); + assert!(clawback.holder.is_none()); + } + + #[test] + fn test_validation_xrp_amount_rejected() { + use crate::models::amount::XRPAmount; + use crate::models::transactions::exceptions::{ + XRPLClawbackException, XRPLTransactionException, + }; + use crate::models::XRPLModelException; + + let clawback = Clawback { + common_fields: CommonFields { + account: "rp6abvbTbjoce8ZDJkT6snvxTZSYMBCC9S".into(), + transaction_type: TransactionType::Clawback, + ..Default::default() + }, + amount: Amount::XRPAmount(XRPAmount::from("1000000")), + ..Default::default() + }; + + let err = clawback.validate().unwrap_err(); + assert!( + matches!( + err, + XRPLModelException::XRPLTransactionError( + XRPLTransactionException::XRPLClawbackError( + XRPLClawbackException::AmountMustNotBeXRP + ) + ) + ), + "Expected AmountMustNotBeXRP, got: {:?}", + err + ); + } + + #[test] + fn test_validation_holder_present_for_iou_rejected() { + use crate::models::transactions::exceptions::{ + XRPLClawbackException, XRPLTransactionException, + }; + use crate::models::XRPLModelException; + + let clawback = Clawback { + common_fields: CommonFields { + account: "rp6abvbTbjoce8ZDJkT6snvxTZSYMBCC9S".into(), + transaction_type: TransactionType::Clawback, + ..Default::default() + }, + amount: Amount::IssuedCurrencyAmount(IssuedCurrencyAmount::new( + "FOO".into(), + "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW".into(), + "314.159".into(), + )), + holder: Some("rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW".into()), + }; + + let err = clawback.validate().unwrap_err(); + assert!( + matches!( + err, + XRPLModelException::XRPLTransactionError( + XRPLTransactionException::XRPLClawbackError( + XRPLClawbackException::HolderMustNotBePresentForIOU + ) + ) + ), + "Expected HolderMustNotBePresentForIOU, got: {:?}", + err + ); + } + + #[test] + fn test_validation_valid_iou_clawback() { + let clawback = Clawback { + common_fields: CommonFields { + account: "rp6abvbTbjoce8ZDJkT6snvxTZSYMBCC9S".into(), + transaction_type: TransactionType::Clawback, + ..Default::default() + }, + amount: Amount::IssuedCurrencyAmount(IssuedCurrencyAmount::new( + "FOO".into(), + "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW".into(), + "314.159".into(), + )), + holder: None, + }; + + assert!( + clawback.validate().is_ok(), + "Valid IOU clawback should pass validation" + ); + } + + #[test] + fn test_default() { + let clawback = Clawback { + common_fields: CommonFields { + account: "rp6abvbTbjoce8ZDJkT6snvxTZSYMBCC9S".into(), + transaction_type: TransactionType::Clawback, + ..Default::default() + }, + amount: Amount::IssuedCurrencyAmount(IssuedCurrencyAmount::new( + "USD".into(), + "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW".into(), + "100".into(), + )), + ..Default::default() + }; + + assert_eq!( + clawback.common_fields.account, + "rp6abvbTbjoce8ZDJkT6snvxTZSYMBCC9S" + ); + assert_eq!( + clawback.common_fields.transaction_type, + TransactionType::Clawback + ); + assert!(clawback.holder.is_none()); + assert!(clawback.common_fields.fee.is_none()); + assert!(clawback.common_fields.sequence.is_none()); + } +} diff --git a/src/models/transactions/exceptions.rs b/src/models/transactions/exceptions.rs index d5535c60..21a4e64a 100644 --- a/src/models/transactions/exceptions.rs +++ b/src/models/transactions/exceptions.rs @@ -28,6 +28,8 @@ pub enum XRPLTransactionException { #[error("{0}")] XRPLAMMCreateError(#[from] XRPLAMMCreateException), #[error("{0}")] + XRPLClawbackError(#[from] XRPLClawbackException), + #[error("{0}")] XRPLCoreError(#[from] XRPLCoreException), #[error("The transaction must be signed")] TxMustBeSigned, @@ -214,3 +216,15 @@ pub enum XRPLAMMCreateException { #[cfg(feature = "std")] impl alloc::error::Error for XRPLAMMCreateException {} + +#[derive(Debug, Clone, PartialEq, Eq, Error)] +#[non_exhaustive] +pub enum XRPLClawbackException { + #[error("Clawback amount must not be XRP — only issued currencies can be clawed back")] + AmountMustNotBeXRP, + #[error("The `holder` field must not be present for standard issued-currency (IOU) clawback")] + HolderMustNotBePresentForIOU, +} + +#[cfg(feature = "std")] +impl alloc::error::Error for XRPLClawbackException {} diff --git a/src/models/transactions/mod.rs b/src/models/transactions/mod.rs index 167d56c8..0c8e670d 100644 --- a/src/models/transactions/mod.rs +++ b/src/models/transactions/mod.rs @@ -9,6 +9,7 @@ pub mod amm_withdraw; pub mod check_cancel; pub mod check_cash; pub mod check_create; +pub mod clawback; pub mod deposit_preauth; pub mod escrow_cancel; pub mod escrow_create; @@ -76,6 +77,7 @@ pub enum TransactionType { CheckCancel, CheckCash, CheckCreate, + Clawback, DepositPreauth, EscrowCancel, EscrowCreate, @@ -173,6 +175,7 @@ where /// The network ID of the chain this transaction is intended for. /// MUST BE OMITTED for Mainnet and some test networks. /// REQUIRED on chains whose network ID is 1025 or higher. + #[serde(rename = "NetworkID")] pub network_id: Option, /// The sequence number of the account sending the transaction. /// A transaction is only valid if the Sequence number is exactly diff --git a/tests/transactions/account_set.rs b/tests/transactions/account_set.rs index 4c377cc1..ddc19cd8 100644 --- a/tests/transactions/account_set.rs +++ b/tests/transactions/account_set.rs @@ -3,9 +3,14 @@ // Scenarios: // - base: set domain field with hex-encoded value // - with_memo: attach a memo to the transaction +// - clawback_flag_*: XLS-0039 Section 3.2 – AllowTrustLineClawback flag rules -use crate::common::{generate_funded_wallet, test_transaction, with_blockchain_lock}; -use xrpl::models::transactions::{account_set::AccountSet, Memo}; +use crate::common::{generate_funded_wallet, get_client, test_transaction, with_blockchain_lock}; +use xrpl::asynch::clients::XRPLAsyncClient; +use xrpl::models::transactions::{ + account_set::{AccountSet, AccountSetFlag}, + Memo, +}; #[tokio::test] async fn test_account_set_base() { @@ -72,3 +77,70 @@ async fn test_account_set_with_memo() { }) .await; } + +// --------------------------------------------------------------------------- +// XLS-0039 Section 3.2 – AllowTrustLineClawback flag +// --------------------------------------------------------------------------- + +/// baseline: a fresh account with an empty owner directory can +/// successfully enable AsfAllowTrustLineClawback. +#[tokio::test] +async fn test_clawback_flag_set_on_empty_account() { + with_blockchain_lock(|| async { + let wallet = generate_funded_wallet().await; + + let mut tx = AccountSet::new( + wallet.classic_address.clone().into(), + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, // clear_flag + None, // domain + None, // email_hash + None, // message_key + Some(AccountSetFlag::AsfAllowTrustLineClawback), // set_flag + None, // transfer_rate + None, // tick_size + None, // nftoken_minter + ); + + test_transaction(&mut tx, &wallet).await; + + // Verify the flag is now set via account_info + let client = get_client().await; + let request = xrpl::models::requests::account_info::AccountInfo::new( + None, + wallet.classic_address.clone().into(), + None, + Some("current".into()), + None, + None, + None, + ); + let response = client.request(request.into()).await.unwrap(); + let account_info = + xrpl::models::results::account_info::AccountInfoVersionMap::try_from(response).unwrap(); + let account_flags = match &account_info { + xrpl::models::results::account_info::AccountInfoVersionMap::Default(i) => { + &i.base.account_flags + } + xrpl::models::results::account_info::AccountInfoVersionMap::V1(i) => { + &i.base.account_flags + } + }; + assert!( + account_flags + .as_ref() + .expect("account_flags should be present") + .allow_trust_line_clawback, + "AllowTrustLineClawback should be true after AccountSet" + ); + }) + .await; +} diff --git a/tests/transactions/clawback.rs b/tests/transactions/clawback.rs new file mode 100644 index 00000000..35a48e98 --- /dev/null +++ b/tests/transactions/clawback.rs @@ -0,0 +1,142 @@ +// Clawback integration tests +// +// Scenarios: +// - base: claw back issued currency from a holder +// +// Prerequisites clawback test (IOU case): +// Note: It is impractical to test the MPT clawback test-cases because xrpl-rust does not have the necessary MPT-related models. +// 1. Fund an issuer wallet and enable AsfAllowTrustLineClawback via AccountSet +// 2. Fund a holder wallet +// 3. Create a trust line from holder to issuer (TrustSet) +// 4. Issue tokens from issuer to holder (Payment) +// 5. Execute the Clawback transaction + +use crate::common::{generate_funded_wallet, test_transaction, with_blockchain_lock}; +use xrpl::models::{ + transactions::{ + account_set::{AccountSet, AccountSetFlag}, + clawback::Clawback, + payment::Payment, + trust_set::TrustSet, + }, + Amount, IssuedCurrencyAmount, +}; + +/// Set up the issuer account with the AllowTrustLineClawback flag, +/// create a trust line from holder to issuer, and issue tokens. +/// Returns the issued currency amount identifier for use in Clawback. +async fn setup_clawback_prerequisites( + issuer: &xrpl::wallet::Wallet, + holder: &xrpl::wallet::Wallet, + currency: &str, + trust_limit: &str, + issue_amount: &str, +) { + let currency = currency.to_string(); + let trust_limit = trust_limit.to_string(); + let issue_amount = issue_amount.to_string(); + + // Step 1: Enable AllowTrustLineClawback on the issuer account + let mut account_set = AccountSet::new( + issuer.classic_address.clone().into(), + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, // clear_flag + None, // domain + None, // email_hash + None, // message_key + Some(AccountSetFlag::AsfAllowTrustLineClawback), // set_flag + None, // transfer_rate + None, // tick_size + None, // nftoken_minter + ); + test_transaction(&mut account_set, issuer).await; + + // Step 2: Create trust line from holder to issuer + let mut trust_set = TrustSet::new( + holder.classic_address.clone().into(), + None, + None, + None, + None, + None, + None, + None, + None, + None, + IssuedCurrencyAmount::new( + currency.clone().into(), + issuer.classic_address.clone().into(), + trust_limit.into(), + ), + None, + None, + ); + test_transaction(&mut trust_set, holder).await; + + // Step 3: Issue tokens from issuer to holder + let mut payment = Payment::new( + issuer.classic_address.clone().into(), + None, + None, + None, + None, + None, + None, + None, + None, + None, + Amount::IssuedCurrencyAmount(IssuedCurrencyAmount::new( + currency.into(), + issuer.classic_address.clone().into(), + issue_amount.into(), + )), + holder.classic_address.clone().into(), + None, + None, + None, + None, + None, + ); + test_transaction(&mut payment, issuer).await; +} + +#[tokio::test] +async fn test_clawback_base() { + with_blockchain_lock(|| async { + let issuer = generate_funded_wallet().await; + let holder = generate_funded_wallet().await; + + setup_clawback_prerequisites(&issuer, &holder, "USD", "1000", "500").await; + + // Claw back 100 USD from holder. + // Note: for IOU clawback the Amount issuer field is the *holder* address. + let mut tx = Clawback::new( + issuer.classic_address.clone().into(), + None, // account_txn_id + None, // fee + None, // last_ledger_sequence + None, // memos + None, // sequence + None, // signers + None, // source_tag + None, // ticket_sequence + Amount::IssuedCurrencyAmount(IssuedCurrencyAmount::new( + "USD".into(), + holder.classic_address.clone().into(), + "100".into(), + )), + None, // holder (must be None for IOU clawback) + ); + + test_transaction(&mut tx, &issuer).await; + }) + .await; +} diff --git a/tests/transactions/mod.rs b/tests/transactions/mod.rs index c0e13b8d..8363a745 100644 --- a/tests/transactions/mod.rs +++ b/tests/transactions/mod.rs @@ -9,6 +9,7 @@ pub mod amm_withdraw; pub mod check_cancel; pub mod check_cash; pub mod check_create; +pub mod clawback; pub mod deposit_preauth; pub mod escrow_cancel; pub mod escrow_create;