openzeppelin_relayer/domain/transaction/stellar/
validation.rs

1//! Validation logic for Stellar transactions
2//!
3//! This module focuses on business logic validations that aren't
4//! already handled by XDR parsing or the type system.
5
6use crate::constants::STELLAR_DEFAULT_TRANSACTION_FEE;
7use crate::constants::STELLAR_MAX_OPERATIONS;
8use crate::domain::relayer::xdr_utils::{
9    extract_operations, extract_source_account, muxed_account_to_string,
10};
11use crate::domain::transaction::stellar::token::get_token_balance;
12use crate::domain::transaction::stellar::utils::{
13    asset_to_asset_id, convert_xlm_fee_to_token, estimate_fee, extract_time_bounds,
14};
15use crate::domain::xdr_needs_simulation;
16use crate::models::RelayerStellarPolicy;
17use crate::models::{MemoSpec, OperationSpec, StellarValidationError, TransactionError};
18use crate::services::provider::StellarProviderTrait;
19use crate::services::stellar_dex::StellarDexServiceTrait;
20use chrono::{DateTime, Duration, Utc};
21use serde::Serialize;
22use soroban_rs::xdr::{
23    AccountId, HostFunction, InvokeHostFunctionOp, LedgerKey, OperationBody, PaymentOp,
24    PublicKey as XdrPublicKey, ScAddress, SorobanCredentials, TransactionEnvelope,
25};
26use stellar_strkey::ed25519::PublicKey;
27use thiserror::Error;
28#[derive(Debug, Error, Serialize)]
29pub enum StellarTransactionValidationError {
30    #[error("Validation error: {0}")]
31    ValidationError(String),
32    #[error("Policy violation: {0}")]
33    PolicyViolation(String),
34    #[error("Invalid asset identifier: {0}")]
35    InvalidAssetIdentifier(String),
36    #[error("Token not allowed: {0}")]
37    TokenNotAllowed(String),
38    #[error("Insufficient token payment: expected {0}, got {1}")]
39    InsufficientTokenPayment(u64, u64),
40    #[error("Max fee exceeded: {0}")]
41    MaxFeeExceeded(u64),
42}
43
44/// Validate operations for business rules
45pub fn validate_operations(ops: &[OperationSpec]) -> Result<(), TransactionError> {
46    // Basic sanity checks
47    if ops.is_empty() {
48        return Err(StellarValidationError::EmptyOperations.into());
49    }
50
51    if ops.len() > STELLAR_MAX_OPERATIONS {
52        return Err(StellarValidationError::TooManyOperations {
53            count: ops.len(),
54            max: STELLAR_MAX_OPERATIONS,
55        }
56        .into());
57    }
58
59    // Check Soroban exclusivity - this is a specific business rule
60    validate_soroban_exclusivity(ops)?;
61
62    Ok(())
63}
64
65/// Validate that Soroban operations are exclusive
66fn validate_soroban_exclusivity(ops: &[OperationSpec]) -> Result<(), TransactionError> {
67    let soroban_ops = ops.iter().filter(|op| is_soroban_operation(op)).count();
68
69    if soroban_ops > 1 {
70        return Err(StellarValidationError::MultipleSorobanOperations.into());
71    }
72
73    if soroban_ops == 1 && ops.len() > 1 {
74        return Err(StellarValidationError::SorobanNotExclusive.into());
75    }
76
77    Ok(())
78}
79
80/// Check if an operation is a Soroban operation
81fn is_soroban_operation(op: &OperationSpec) -> bool {
82    matches!(
83        op,
84        OperationSpec::InvokeContract { .. }
85            | OperationSpec::CreateContract { .. }
86            | OperationSpec::UploadWasm { .. }
87    )
88}
89
90/// Validate that Soroban operations don't have a non-None memo
91pub fn validate_soroban_memo_restriction(
92    ops: &[OperationSpec],
93    memo: &Option<MemoSpec>,
94) -> Result<(), TransactionError> {
95    let has_soroban = ops.iter().any(is_soroban_operation);
96
97    if has_soroban && memo.is_some() && !matches!(memo, Some(MemoSpec::None)) {
98        return Err(StellarValidationError::SorobanWithMemo.into());
99    }
100
101    Ok(())
102}
103
104/// Validator for Stellar transactions and policies
105pub struct StellarTransactionValidator;
106
107impl StellarTransactionValidator {
108    /// Validate fee_token structure
109    ///
110    /// Validates that the fee_token is in a valid format:
111    /// - "native" or "XLM" for native XLM
112    /// - "CODE:ISSUER" for classic assets (CODE: 1-12 chars, ISSUER: 56 chars starting with 'G')
113    /// - Contract address starting with "C" (56 chars) for Soroban contract tokens
114    pub fn validate_fee_token_structure(
115        fee_token: &str,
116    ) -> Result<(), StellarTransactionValidationError> {
117        // Handle native XLM
118        if fee_token == "native" || fee_token == "XLM" || fee_token.is_empty() {
119            return Ok(());
120        }
121
122        // Check if it's a contract address (starts with 'C', 56 chars)
123        if fee_token.starts_with('C') && fee_token.len() == 56 && !fee_token.contains(':') {
124            // Validate it's a valid contract address using StrKey
125            if stellar_strkey::Contract::from_string(fee_token).is_ok() {
126                return Ok(());
127            }
128            return Err(StellarTransactionValidationError::InvalidAssetIdentifier(
129                format!(
130                    "Invalid contract address format: {fee_token} (must be 56 characters and valid StrKey)"
131                ),
132            ));
133        }
134
135        // Otherwise, must be CODE:ISSUER format
136        let parts: Vec<&str> = fee_token.split(':').collect();
137        if parts.len() != 2 {
138            return Err(StellarTransactionValidationError::InvalidAssetIdentifier(format!(
139                "Invalid fee_token format: {fee_token}. Expected 'native', 'CODE:ISSUER', or contract address (C...)"
140            )));
141        }
142
143        let code = parts[0];
144        let issuer = parts[1];
145
146        // Validate CODE length (1-12 characters)
147        if code.is_empty() || code.len() > 12 {
148            return Err(StellarTransactionValidationError::InvalidAssetIdentifier(
149                format!("Invalid asset code length: {code} (must be 1-12 characters)"),
150            ));
151        }
152
153        // Validate ISSUER format (56 chars, starts with 'G')
154        if issuer.len() != 56 {
155            return Err(StellarTransactionValidationError::InvalidAssetIdentifier(
156                format!("Invalid issuer address length: {issuer} (must be 56 characters)"),
157            ));
158        }
159
160        if !issuer.starts_with('G') {
161            return Err(StellarTransactionValidationError::InvalidAssetIdentifier(
162                format!("Invalid issuer address prefix: {issuer} (must start with 'G')"),
163            ));
164        }
165
166        // Validate issuer is a valid Stellar public key
167        if stellar_strkey::ed25519::PublicKey::from_string(issuer).is_err() {
168            return Err(StellarTransactionValidationError::InvalidAssetIdentifier(
169                format!(
170                    "Invalid issuer address format: {issuer} (must be a valid Stellar public key)"
171                ),
172            ));
173        }
174
175        Ok(())
176    }
177
178    /// Validate that an asset identifier is in the allowed tokens list
179    pub fn validate_allowed_token(
180        asset: &str,
181        policy: &RelayerStellarPolicy,
182    ) -> Result<(), StellarTransactionValidationError> {
183        let allowed_tokens = policy.get_allowed_tokens();
184
185        if allowed_tokens.is_empty() {
186            // If no allowed tokens specified, all tokens are allowed
187            return Ok(());
188        }
189
190        // Check if native XLM is allowed
191        if asset == "native" || asset.is_empty() {
192            let native_allowed = allowed_tokens
193                .iter()
194                .any(|token| token.asset == "native" || token.asset.is_empty());
195            if !native_allowed {
196                return Err(StellarTransactionValidationError::TokenNotAllowed(
197                    "Native XLM not in allowed tokens list".to_string(),
198                ));
199            }
200            return Ok(());
201        }
202
203        // Check if the asset is in the allowed list
204        let is_allowed = allowed_tokens.iter().any(|token| token.asset == asset);
205
206        if !is_allowed {
207            return Err(StellarTransactionValidationError::TokenNotAllowed(format!(
208                "Token {asset} not in allowed tokens list"
209            )));
210        }
211
212        Ok(())
213    }
214
215    /// Validate that a fee amount doesn't exceed the maximum allowed fee
216    pub fn validate_max_fee(
217        fee: u64,
218        policy: &RelayerStellarPolicy,
219    ) -> Result<(), StellarTransactionValidationError> {
220        if let Some(max_fee) = policy.max_fee {
221            if fee > max_fee as u64 {
222                return Err(StellarTransactionValidationError::MaxFeeExceeded(fee));
223            }
224        }
225
226        Ok(())
227    }
228
229    /// Validate that a specific token's max_allowed_fee is not exceeded
230    pub fn validate_token_max_fee(
231        asset_id: &str,
232        fee: u64,
233        policy: &RelayerStellarPolicy,
234    ) -> Result<(), StellarTransactionValidationError> {
235        if let Some(token_entry) = policy.get_allowed_token_entry(asset_id) {
236            if let Some(max_allowed_fee) = token_entry.max_allowed_fee {
237                if fee > max_allowed_fee {
238                    return Err(StellarTransactionValidationError::MaxFeeExceeded(fee));
239                }
240            }
241        }
242
243        Ok(())
244    }
245
246    /// Extract payment operations from a transaction envelope that pay to the relayer
247    ///
248    /// Returns a vector of (asset_id, amount) tuples for payments to the relayer
249    pub fn extract_relayer_payments(
250        envelope: &TransactionEnvelope,
251        relayer_address: &str,
252    ) -> Result<Vec<(String, u64)>, StellarTransactionValidationError> {
253        let operations = extract_operations(envelope).map_err(|e| {
254            StellarTransactionValidationError::ValidationError(format!(
255                "Failed to extract operations: {e}"
256            ))
257        })?;
258
259        let mut payments = Vec::new();
260
261        for op in operations.iter() {
262            if let OperationBody::Payment(PaymentOp {
263                destination,
264                asset,
265                amount,
266            }) = &op.body
267            {
268                // Convert destination to string
269                let dest_str = muxed_account_to_string(destination).map_err(|e| {
270                    StellarTransactionValidationError::ValidationError(format!(
271                        "Failed to parse destination: {e}"
272                    ))
273                })?;
274
275                // Check if payment is to relayer
276                if dest_str == relayer_address {
277                    // Convert asset to identifier string
278                    let asset_id = asset_to_asset_id(asset).map_err(|e| {
279                        StellarTransactionValidationError::InvalidAssetIdentifier(format!(
280                            "Failed to convert asset to asset_id: {e}"
281                        ))
282                    })?;
283                    // Validate amount is non-negative before converting from i64 to u64
284                    if *amount < 0 {
285                        return Err(StellarTransactionValidationError::ValidationError(
286                            "Negative payment amount".to_string(),
287                        ));
288                    }
289                    let amount_u64 = *amount as u64;
290                    payments.push((asset_id, amount_u64));
291                }
292            }
293        }
294
295        Ok(payments)
296    }
297
298    /// Validate token payment in transaction
299    ///
300    /// Checks that:
301    /// 1. Payment operation to relayer exists
302    /// 2. Token is in allowed_tokens list
303    /// 3. Payment amount matches expected fee (within tolerance)
304    pub fn validate_token_payment(
305        envelope: &TransactionEnvelope,
306        relayer_address: &str,
307        expected_fee_token: &str,
308        expected_fee_amount: u64,
309        policy: &RelayerStellarPolicy,
310    ) -> Result<(), StellarTransactionValidationError> {
311        // Extract payments to relayer
312        let payments = Self::extract_relayer_payments(envelope, relayer_address)?;
313
314        if payments.is_empty() {
315            return Err(StellarTransactionValidationError::ValidationError(
316                "No payment operation found to relayer".to_string(),
317            ));
318        }
319
320        // Find payment matching the expected token
321        let matching_payment = payments
322            .iter()
323            .find(|(asset_id, _)| asset_id == expected_fee_token);
324
325        match matching_payment {
326            Some((asset_id, amount)) => {
327                // Validate token is allowed
328                Self::validate_allowed_token(asset_id, policy)?;
329
330                // Validate amount matches expected (allow 1% tolerance for rounding)
331                let tolerance = (expected_fee_amount as f64 * 0.01) as u64;
332                if *amount < expected_fee_amount.saturating_sub(tolerance) {
333                    return Err(StellarTransactionValidationError::InsufficientTokenPayment(
334                        expected_fee_amount,
335                        *amount,
336                    ));
337                }
338
339                // Validate max fee
340                Self::validate_token_max_fee(asset_id, *amount, policy)?;
341
342                Ok(())
343            }
344            None => Err(StellarTransactionValidationError::ValidationError(format!(
345                "No payment found for expected token: {expected_fee_token}. Found payments: {payments:?}"
346            ))),
347        }
348    }
349
350    /// Validate that the source account is not the relayer address
351    ///
352    /// This prevents malicious attempts to drain the relayer's funds by
353    /// using the relayer as the transaction source.
354    fn validate_source_account_not_relayer(
355        envelope: &TransactionEnvelope,
356        relayer_address: &str,
357    ) -> Result<(), StellarTransactionValidationError> {
358        let source_account = extract_source_account(envelope).map_err(|e| {
359            StellarTransactionValidationError::ValidationError(format!(
360                "Failed to extract source account: {e}"
361            ))
362        })?;
363
364        if source_account == relayer_address {
365            return Err(StellarTransactionValidationError::ValidationError(
366                "Transaction source account cannot be the relayer address. This is a security measure to prevent relayer fund drainage.".to_string(),
367            ));
368        }
369
370        Ok(())
371    }
372
373    /// Validate transaction type
374    ///
375    /// Rejects fee-bump transactions as they are not suitable for gasless transactions.
376    fn validate_transaction_type(
377        envelope: &TransactionEnvelope,
378    ) -> Result<(), StellarTransactionValidationError> {
379        match envelope {
380            soroban_rs::xdr::TransactionEnvelope::TxFeeBump(_) => {
381                Err(StellarTransactionValidationError::ValidationError(
382                    "Fee-bump transactions are not supported for gasless transactions".to_string(),
383                ))
384            }
385            _ => Ok(()),
386        }
387    }
388
389    /// Validate that operations don't target the relayer (except for fee payment)
390    ///
391    /// This prevents operations that could drain the relayer's funds or manipulate
392    /// the relayer's account state. Fee payment operations are expected and allowed.
393    fn validate_operations_not_targeting_relayer(
394        envelope: &TransactionEnvelope,
395        relayer_address: &str,
396    ) -> Result<(), StellarTransactionValidationError> {
397        let operations = extract_operations(envelope).map_err(|e| {
398            StellarTransactionValidationError::ValidationError(format!(
399                "Failed to extract operations: {e}"
400            ))
401        })?;
402
403        for op in operations.iter() {
404            match &op.body {
405                OperationBody::Payment(PaymentOp { destination, .. }) => {
406                    let dest_str = muxed_account_to_string(destination).map_err(|e| {
407                        StellarTransactionValidationError::ValidationError(format!(
408                            "Failed to parse destination: {e}"
409                        ))
410                    })?;
411
412                    // Payment to relayer is allowed (for fee payment), but we log it
413                    if dest_str == relayer_address {
414                        // This is expected for fee payment, but we should ensure
415                        // it's the last operation added by the relayer
416                        continue;
417                    }
418                }
419                OperationBody::AccountMerge(destination) => {
420                    let dest_str = muxed_account_to_string(destination).map_err(|e| {
421                        StellarTransactionValidationError::ValidationError(format!(
422                            "Failed to parse merge destination: {e}"
423                        ))
424                    })?;
425
426                    if dest_str == relayer_address {
427                        return Err(StellarTransactionValidationError::ValidationError(
428                            "Account merge operations targeting the relayer are not allowed"
429                                .to_string(),
430                        ));
431                    }
432                }
433                OperationBody::SetOptions(_) => {
434                    // SetOptions operations could potentially modify account settings
435                    // We should reject them if they target relayer, but SetOptions doesn't have a target
436                    // However, SetOptions on the source account could be problematic
437                    // For now, we allow SetOptions but could add more specific checks
438                }
439                _ => {
440                    // Other operation types are checked in validate_operation_types
441                }
442            }
443        }
444
445        Ok(())
446    }
447
448    /// Validate operations count
449    ///
450    /// Ensures the transaction has a reasonable number of operations.
451    fn validate_operations_count(
452        envelope: &TransactionEnvelope,
453    ) -> Result<(), StellarTransactionValidationError> {
454        let operations = extract_operations(envelope).map_err(|e| {
455            StellarTransactionValidationError::ValidationError(format!(
456                "Failed to extract operations: {e}"
457            ))
458        })?;
459
460        if operations.is_empty() {
461            return Err(StellarTransactionValidationError::ValidationError(
462                "Transaction must contain at least one operation".to_string(),
463            ));
464        }
465
466        if operations.len() > STELLAR_MAX_OPERATIONS {
467            return Err(StellarTransactionValidationError::ValidationError(format!(
468                "Transaction contains too many operations: {} (maximum is {})",
469                operations.len(),
470                STELLAR_MAX_OPERATIONS
471            )));
472        }
473
474        Ok(())
475    }
476
477    /// Convert AccountId to string representation
478    fn account_id_to_string(
479        account_id: &AccountId,
480    ) -> Result<String, StellarTransactionValidationError> {
481        match &account_id.0 {
482            XdrPublicKey::PublicKeyTypeEd25519(uint256) => {
483                let bytes: [u8; 32] = uint256.0;
484                let pk = PublicKey(bytes);
485                Ok(pk.to_string())
486            }
487        }
488    }
489
490    /// Check if a footprint key targets relayer-owned storage
491    #[allow(dead_code)]
492    fn footprint_key_targets_relayer(
493        key: &LedgerKey,
494        relayer_address: &str,
495    ) -> Result<bool, StellarTransactionValidationError> {
496        match key {
497            LedgerKey::Account(account_key) => {
498                // Extract account ID from the key
499                let account_str = Self::account_id_to_string(&account_key.account_id)?;
500                Ok(account_str == relayer_address)
501            }
502            LedgerKey::Trustline(trustline_key) => {
503                // Check if trustline belongs to relayer
504                let account_str = Self::account_id_to_string(&trustline_key.account_id)?;
505                Ok(account_str == relayer_address)
506            }
507            LedgerKey::ContractData(contract_data_key) => {
508                // Check if contract data key references relayer account
509                match &contract_data_key.contract {
510                    ScAddress::Account(acc_id) => {
511                        let account_str = Self::account_id_to_string(acc_id)?;
512                        Ok(account_str == relayer_address)
513                    }
514                    ScAddress::Contract(_) => {
515                        // Contract storage keys are allowed
516                        Ok(false)
517                    }
518                    ScAddress::MuxedAccount(_)
519                    | ScAddress::ClaimableBalance(_)
520                    | ScAddress::LiquidityPool(_) => {
521                        // These are not account addresses, so they're safe
522                        Ok(false)
523                    }
524                }
525            }
526            LedgerKey::ContractCode(_) => {
527                // Contract code keys are allowed
528                Ok(false)
529            }
530            _ => {
531                // Other ledger key types are allowed
532                Ok(false)
533            }
534        }
535    }
536
537    /// Validate contract invocation operation
538    ///
539    /// Performs comprehensive security validation for Soroban contract invocations:
540    /// 1. Validates host function type is allowed
541    /// 2. Validates Soroban auth entries don't require relayer
542    fn validate_contract_invocation(
543        invoke: &InvokeHostFunctionOp,
544        op_idx: usize,
545        relayer_address: &str,
546        _policy: &RelayerStellarPolicy,
547    ) -> Result<(), StellarTransactionValidationError> {
548        // 1. Validate host function type
549        match &invoke.host_function {
550            HostFunction::InvokeContract(_) => {
551                // Contract invocations are allowed by default
552            }
553            HostFunction::CreateContract(_) => {
554                return Err(StellarTransactionValidationError::ValidationError(format!(
555                    "Op {op_idx}: CreateContract not allowed for gasless transactions"
556                )));
557            }
558            HostFunction::UploadContractWasm(_) => {
559                return Err(StellarTransactionValidationError::ValidationError(format!(
560                    "Op {op_idx}: UploadContractWasm not allowed for gasless transactions"
561                )));
562            }
563            _ => {
564                return Err(StellarTransactionValidationError::ValidationError(format!(
565                    "Op {op_idx}: Unsupported host function"
566                )));
567            }
568        }
569
570        // Validate Soroban auth entries
571        for (i, entry) in invoke.auth.iter().enumerate() {
572            // Validate that relayer is NOT required signer
573            match &entry.credentials {
574                SorobanCredentials::SourceAccount => {
575                    // We've already validated that the source account is not the relayer,
576                    // so SourceAccount credentials are safe.
577                }
578                SorobanCredentials::Address(address_creds) => {
579                    // Check if the address is the relayer
580                    match &address_creds.address {
581                        ScAddress::Account(acc_id) => {
582                            // Convert account ID to string for comparison
583                            let account_str = Self::account_id_to_string(acc_id)?;
584                            if account_str == relayer_address {
585                                return Err(StellarTransactionValidationError::ValidationError(
586                                    format!(
587                                        "Op {op_idx}: Soroban auth entry {i} requires relayer ({relayer_address}). Forbidden."
588                                    ),
589                                ));
590                            }
591                        }
592                        ScAddress::Contract(_) => {
593                            // Contract addresses in auth are allowed
594                        }
595                        ScAddress::MuxedAccount(_) => {
596                            // Muxed accounts are allowed
597                        }
598                        ScAddress::ClaimableBalance(_) | ScAddress::LiquidityPool(_) => {
599                            // These are not account addresses, so they're safe
600                        }
601                    }
602                }
603            }
604        }
605
606        Ok(())
607    }
608
609    /// Validate operation types
610    ///
611    /// Ensures only allowed operation types are present in the transaction.
612    /// Currently allows common operation types but can be extended based on policy.
613    fn validate_operation_types(
614        envelope: &TransactionEnvelope,
615        relayer_address: &str,
616        policy: &RelayerStellarPolicy,
617    ) -> Result<(), StellarTransactionValidationError> {
618        let operations = extract_operations(envelope).map_err(|e| {
619            StellarTransactionValidationError::ValidationError(format!(
620                "Failed to extract operations: {e}"
621            ))
622        })?;
623
624        for (idx, op) in operations.iter().enumerate() {
625            match &op.body {
626                // Prevent account merges (could drain account before payment executes)
627                OperationBody::AccountMerge(_) => {
628                    return Err(StellarTransactionValidationError::ValidationError(format!(
629                        "Operation {idx}: AccountMerge operations are not allowed"
630                    )));
631                }
632
633                // Prevent SetOptions that could lock out the account
634                OperationBody::SetOptions(_set_opts) => {
635                    return Err(StellarTransactionValidationError::ValidationError(format!(
636                        "Operation {idx}: SetOptions operations are not allowed"
637                    )));
638                }
639
640                // Validate smart contract invocations
641                OperationBody::InvokeHostFunction(invoke) => {
642                    Self::validate_contract_invocation(invoke, idx, relayer_address, policy)?;
643                }
644
645                // Allow common operations
646                OperationBody::Payment(_)
647                | OperationBody::PathPaymentStrictReceive(_)
648                | OperationBody::PathPaymentStrictSend(_)
649                | OperationBody::ManageSellOffer(_)
650                | OperationBody::ManageBuyOffer(_)
651                | OperationBody::CreatePassiveSellOffer(_)
652                | OperationBody::ChangeTrust(_)
653                | OperationBody::ManageData(_)
654                | OperationBody::BumpSequence(_)
655                | OperationBody::CreateClaimableBalance(_)
656                | OperationBody::ClaimClaimableBalance(_)
657                | OperationBody::BeginSponsoringFutureReserves(_)
658                | OperationBody::EndSponsoringFutureReserves
659                | OperationBody::RevokeSponsorship(_)
660                | OperationBody::Clawback(_)
661                | OperationBody::ClawbackClaimableBalance(_)
662                | OperationBody::SetTrustLineFlags(_)
663                | OperationBody::LiquidityPoolDeposit(_)
664                | OperationBody::LiquidityPoolWithdraw(_) => {
665                    // These are generally safe
666                }
667
668                // Deprecated operations
669                OperationBody::CreateAccount(_) | OperationBody::AllowTrust(_) => {
670                    return Err(StellarTransactionValidationError::ValidationError(format!(
671                        "Operation {idx}: Deprecated operation type not allowed"
672                    )));
673                }
674
675                // Other operations
676                OperationBody::Inflation
677                | OperationBody::ExtendFootprintTtl(_)
678                | OperationBody::RestoreFootprint(_) => {
679                    // These are allowed
680                }
681            }
682        }
683
684        Ok(())
685    }
686
687    /// Validate sequence number
688    ///
689    /// Validates that the transaction sequence number is valid for the source account.
690    /// Note: The relayer will fee-bump this transaction, so the relayer's sequence will be consumed.
691    /// However, the inner transaction (user's tx) must still have a valid sequence number.
692    ///
693    /// The transaction sequence must be strictly greater than the account's current sequence number.
694    /// Future sequence numbers are allowed (user can queue transactions), but equal sequences are rejected.
695    pub async fn validate_sequence_number<P>(
696        envelope: &TransactionEnvelope,
697        provider: &P,
698    ) -> Result<(), StellarTransactionValidationError>
699    where
700        P: StellarProviderTrait + Send + Sync,
701    {
702        // Extract source account
703        let source_account = extract_source_account(envelope).map_err(|e| {
704            StellarTransactionValidationError::ValidationError(format!(
705                "Failed to extract source account: {e}"
706            ))
707        })?;
708
709        // Get account's current sequence number from chain
710        let account_entry = provider.get_account(&source_account).await.map_err(|e| {
711            StellarTransactionValidationError::ValidationError(format!(
712                "Failed to get account sequence: {e}"
713            ))
714        })?;
715        let account_seq_num = account_entry.seq_num.0;
716
717        // Extract transaction sequence number
718        let tx_seq_num = match envelope {
719            TransactionEnvelope::TxV0(e) => e.tx.seq_num.0,
720            TransactionEnvelope::Tx(e) => e.tx.seq_num.0,
721            TransactionEnvelope::TxFeeBump(_) => {
722                return Err(StellarTransactionValidationError::ValidationError(
723                    "Fee-bump transactions are not supported for gasless transactions".to_string(),
724                ));
725            }
726        };
727
728        // Validate that transaction sequence number is strictly greater than account's current sequence
729        // Stellar requires tx_seq_num > account_seq_num (not >=). Equal sequences are invalid.
730        // The user can set a future sequence number, but not a past or equal one
731        if tx_seq_num <= account_seq_num {
732            return Err(StellarTransactionValidationError::ValidationError(format!(
733                "Transaction sequence number {tx_seq_num} is invalid. Account's current sequence is {account_seq_num}. \
734                The transaction sequence must be strictly greater than the account's current sequence."
735            )));
736        }
737
738        Ok(())
739    }
740
741    /// Comprehensive validation for gasless transactions
742    ///
743    /// Performs all security and policy validations on a transaction envelope
744    /// before it's processed for gasless execution.
745    ///
746    /// This includes:
747    /// - Validating source account is not relayer
748    /// - Validating transaction type
749    /// - Validating operations don't target relayer (except fee payment)
750    /// - Validating operations count
751    /// - Validating operation types
752    /// - Validating sequence number
753    /// - Validating transaction validity duration (if max_validity_duration is provided)
754    ///
755    /// # Arguments
756    /// * `envelope` - The transaction envelope to validate
757    /// * `relayer_address` - The relayer's Stellar address
758    /// * `policy` - The relayer policy
759    /// * `provider` - Provider for Stellar RPC operations
760    /// * `max_validity_duration` - Optional maximum allowed transaction validity duration. If provided,
761    ///   validates that the transaction's time bounds don't exceed this duration. This protects against
762    ///   price fluctuations for user-paid fee transactions.
763    pub async fn gasless_transaction_validation<P>(
764        envelope: &TransactionEnvelope,
765        relayer_address: &str,
766        policy: &RelayerStellarPolicy,
767        provider: &P,
768        max_validity_duration: Option<Duration>,
769    ) -> Result<(), StellarTransactionValidationError>
770    where
771        P: StellarProviderTrait + Send + Sync,
772    {
773        Self::validate_source_account_not_relayer(envelope, relayer_address)?;
774        Self::validate_transaction_type(envelope)?;
775        Self::validate_operations_not_targeting_relayer(envelope, relayer_address)?;
776        Self::validate_operations_count(envelope)?;
777        Self::validate_operation_types(envelope, relayer_address, policy)?;
778        Self::validate_sequence_number(envelope, provider).await?;
779
780        // Validate that transaction time bounds are not expired
781        Self::validate_time_bounds_not_expired(envelope)?;
782
783        // Validate transaction validity duration if max_validity_duration is provided
784        if let Some(max_duration) = max_validity_duration {
785            Self::validate_transaction_validity_duration(envelope, max_duration)?;
786        }
787
788        Ok(())
789    }
790
791    /// Validate that transaction time bounds are valid and not expired
792    ///
793    /// Checks that:
794    /// 1. Time bounds exist (if envelope has them)
795    /// 2. Current time is within the bounds (min_time <= now <= max_time)
796    /// 3. Transaction has not expired (now <= max_time)
797    ///
798    /// # Arguments
799    /// * `envelope` - The transaction envelope to validate
800    ///
801    /// # Returns
802    /// Ok(()) if validation passes, StellarTransactionValidationError if validation fails
803    pub fn validate_time_bounds_not_expired(
804        envelope: &TransactionEnvelope,
805    ) -> Result<(), StellarTransactionValidationError> {
806        let time_bounds = extract_time_bounds(envelope);
807
808        if let Some(bounds) = time_bounds {
809            let now = Utc::now().timestamp() as u64;
810            let min_time = bounds.min_time.0;
811            let max_time = bounds.max_time.0;
812
813            // Check if transaction has expired
814            // max_time == 0 means unbounded in Stellar (no upper limit)
815            if max_time != 0 && now > max_time {
816                return Err(StellarTransactionValidationError::ValidationError(format!(
817                    "Transaction has expired: max_time={max_time}, current_time={now}"
818                )));
819            }
820
821            // Check if transaction is not yet valid (optional check, but good to have)
822            if min_time > 0 && now < min_time {
823                return Err(StellarTransactionValidationError::ValidationError(format!(
824                    "Transaction is not yet valid: min_time={min_time}, current_time={now}"
825                )));
826            }
827        }
828        // If no time bounds are set, we don't fail here (some transactions may not have them)
829        // The caller can decide if time bounds are required
830
831        Ok(())
832    }
833
834    /// Validate that transaction validity duration is within the maximum allowed time
835    ///
836    /// This prevents price fluctuations and protects the relayer from losses.
837    /// The transaction must have time bounds set and the validity duration must not exceed
838    /// the maximum allowed duration.
839    ///
840    /// # Arguments
841    /// * `envelope` - The transaction envelope to validate
842    /// * `max_duration` - Maximum allowed validity duration
843    ///
844    /// # Returns
845    /// Ok(()) if validation passes, StellarTransactionValidationError if validation fails
846    pub fn validate_transaction_validity_duration(
847        envelope: &TransactionEnvelope,
848        max_duration: Duration,
849    ) -> Result<(), StellarTransactionValidationError> {
850        let time_bounds = extract_time_bounds(envelope);
851
852        if let Some(bounds) = time_bounds {
853            // max_time == 0 means unbounded in Stellar (no upper limit)
854            // For duration validation, we require a bounded max_time
855            if bounds.max_time.0 == 0 {
856                return Err(StellarTransactionValidationError::ValidationError(
857                    "Transaction has unbounded validity (max_time=0), but bounded validity is required".to_string(),
858                ));
859            }
860
861            let max_time =
862                DateTime::from_timestamp(bounds.max_time.0 as i64, 0).ok_or_else(|| {
863                    StellarTransactionValidationError::ValidationError(
864                        "Invalid max_time in time bounds".to_string(),
865                    )
866                })?;
867            let now = Utc::now();
868            let duration = max_time - now;
869
870            if duration > max_duration {
871                return Err(StellarTransactionValidationError::ValidationError(format!(
872                    "Transaction validity duration ({duration:?}) exceeds maximum allowed duration ({max_duration:?})"
873                )));
874            }
875        } else {
876            return Err(StellarTransactionValidationError::ValidationError(
877                "Transaction must have time bounds set".to_string(),
878            ));
879        }
880
881        Ok(())
882    }
883
884    /// Comprehensive validation for user fee payment transactions
885    ///
886    /// This function performs all validations required for user-paid fee transactions.
887    /// It validates:
888    /// 1. Transaction structure and operations (via gasless_transaction_validation)
889    /// 2. Fee payment operations exist and are valid
890    /// 3. Allowed token validation
891    /// 4. Token max fee validation
892    /// 5. Payment amount is sufficient (compares with required fee including margin)
893    /// 6. Transaction validity duration (if max_validity_duration is provided)
894    ///
895    /// This function is used by both fee-bump and sign-transaction flows.
896    /// For sign-transaction flows, pass `max_validity_duration` to enforce time bounds.
897    /// For fee-bump flows, pass `None` as transactions may not have time bounds set yet.
898    ///
899    /// # Arguments
900    /// * `envelope` - The transaction envelope to validate
901    /// * `relayer_address` - The relayer's Stellar address
902    /// * `policy` - The relayer policy containing fee payment strategy and token settings
903    /// * `provider` - Provider for Stellar RPC operations
904    /// * `dex_service` - DEX service for fetching quotes to validate payment amounts
905    /// * `max_validity_duration` - Optional maximum allowed transaction validity duration.
906    ///   If provided, validates that the transaction's time bounds don't exceed this duration.
907    ///   This protects against price fluctuations for user-paid fee transactions when signing.
908    ///   Pass `None` for fee-bump flows where time bounds may not be set yet.
909    ///
910    /// # Returns
911    /// Ok(()) if validation passes, StellarTransactionValidationError if validation fails
912    pub async fn validate_user_fee_payment_transaction<P, D>(
913        envelope: &TransactionEnvelope,
914        relayer_address: &str,
915        policy: &RelayerStellarPolicy,
916        provider: &P,
917        dex_service: &D,
918        max_validity_duration: Option<Duration>,
919    ) -> Result<(), StellarTransactionValidationError>
920    where
921        P: StellarProviderTrait + Send + Sync,
922        D: StellarDexServiceTrait + Send + Sync,
923    {
924        // Step 1: Comprehensive security validation for gasless transactions
925        // Include duration validation if max_validity_duration is provided
926        Self::gasless_transaction_validation(
927            envelope,
928            relayer_address,
929            policy,
930            provider,
931            max_validity_duration,
932        )
933        .await?;
934
935        // Step 2: Validate fee payment amounts
936        Self::validate_user_fee_payment_amounts(
937            envelope,
938            relayer_address,
939            policy,
940            provider,
941            dex_service,
942        )
943        .await?;
944
945        Ok(())
946    }
947
948    /// Validate fee payment amounts for user-paid fee transactions
949    ///
950    /// This function validates that the fee payment operation exists, is valid,
951    /// and the payment amount is sufficient. It's separated from the core validation
952    /// to allow reuse in different flows.
953    ///
954    /// # Arguments
955    /// * `envelope` - The transaction envelope to validate
956    /// * `relayer_address` - The relayer's Stellar address
957    /// * `policy` - The relayer policy containing fee payment strategy and token settings
958    /// * `provider` - Provider for Stellar RPC operations
959    /// * `dex_service` - DEX service for fetching quotes to validate payment amounts
960    ///
961    /// # Returns
962    /// Ok(()) if validation passes, StellarTransactionValidationError if validation fails
963    async fn validate_user_fee_payment_amounts<P, D>(
964        envelope: &TransactionEnvelope,
965        relayer_address: &str,
966        policy: &RelayerStellarPolicy,
967        provider: &P,
968        dex_service: &D,
969    ) -> Result<(), StellarTransactionValidationError>
970    where
971        P: StellarProviderTrait + Send + Sync,
972        D: StellarDexServiceTrait + Send + Sync,
973    {
974        // Extract the fee payment for amount validation
975        let payments = Self::extract_relayer_payments(envelope, relayer_address)?;
976        if payments.is_empty() {
977            return Err(StellarTransactionValidationError::ValidationError(
978                "Gasless transactions must include a fee payment operation to the relayer"
979                    .to_string(),
980            ));
981        }
982
983        // Validate only one fee payment operation
984        if payments.len() > 1 {
985            return Err(StellarTransactionValidationError::ValidationError(format!(
986                "Gasless transactions must include exactly one fee payment operation to the relayer, found {}",
987                payments.len()
988            )));
989        }
990
991        // Extract the single payment
992        let (asset_id, amount) = &payments[0];
993
994        // Validate fee payment token
995        Self::validate_allowed_token(asset_id, policy)?;
996
997        // Validate max fee
998        Self::validate_token_max_fee(asset_id, *amount, policy)?;
999
1000        // Calculate required XLM fee using estimate_fee (handles Soroban transactions correctly)
1001
1002        let mut required_xlm_fee = estimate_fee(envelope, provider, None).await.map_err(|e| {
1003            StellarTransactionValidationError::ValidationError(format!(
1004                "Failed to estimate fee: {e}",
1005            ))
1006        })?;
1007
1008        let is_soroban = xdr_needs_simulation(envelope).unwrap_or(false);
1009        if !is_soroban {
1010            // For regular transactions, fee-bump needs base fee (100 stroops)
1011            required_xlm_fee += STELLAR_DEFAULT_TRANSACTION_FEE as u64;
1012        }
1013
1014        let fee_quote = convert_xlm_fee_to_token(dex_service, policy, required_xlm_fee, asset_id)
1015            .await
1016            .map_err(|e| {
1017                StellarTransactionValidationError::ValidationError(format!(
1018                    "Failed to convert XLM fee to token {asset_id}: {e}",
1019                ))
1020            })?;
1021
1022        // Compare payment amount with required token amount (from convert_xlm_fee_to_token which includes margin)
1023        if *amount < fee_quote.fee_in_token {
1024            return Err(StellarTransactionValidationError::InsufficientTokenPayment(
1025                fee_quote.fee_in_token,
1026                *amount,
1027            ));
1028        }
1029
1030        // Validate user token balance
1031        Self::validate_user_token_balance(envelope, asset_id, fee_quote.fee_in_token, provider)
1032            .await?;
1033
1034        Ok(())
1035    }
1036
1037    /// Validate that user has sufficient token balance to pay the transaction fee
1038    ///
1039    /// This function checks that the user's account has enough balance of the specified
1040    /// fee token to cover the required transaction fee. This prevents users from getting
1041    /// quotes or building transactions they cannot afford.
1042    ///
1043    /// # Arguments
1044    /// * `envelope` - The transaction envelope to extract source account from
1045    /// * `fee_token` - The token identifier (e.g., "native" or "USDC:GA5Z...")
1046    /// * `required_fee_amount` - The required fee amount in token's smallest unit (stroops)
1047    /// * `provider` - Provider for Stellar RPC operations to fetch balance
1048    ///
1049    /// # Returns
1050    /// Ok(()) if validation passes, StellarTransactionValidationError if validation fails
1051    pub async fn validate_user_token_balance<P>(
1052        envelope: &TransactionEnvelope,
1053        fee_token: &str,
1054        required_fee_amount: u64,
1055        provider: &P,
1056    ) -> Result<(), StellarTransactionValidationError>
1057    where
1058        P: StellarProviderTrait + Send + Sync,
1059    {
1060        // Extract source account from envelope
1061        let source_account = extract_source_account(envelope).map_err(|e| {
1062            StellarTransactionValidationError::ValidationError(format!(
1063                "Failed to extract source account: {e}"
1064            ))
1065        })?;
1066
1067        // Fetch user's token balance
1068        let user_balance = get_token_balance(provider, &source_account, fee_token)
1069            .await
1070            .map_err(|e| {
1071                StellarTransactionValidationError::ValidationError(format!(
1072                    "Failed to fetch user balance for token {fee_token}: {e}",
1073                ))
1074            })?;
1075
1076        // Check if balance is sufficient
1077        if user_balance < required_fee_amount {
1078            return Err(StellarTransactionValidationError::ValidationError(format!(
1079                "Insufficient balance: user has {user_balance} {fee_token} but needs {required_fee_amount} {fee_token} for transaction fee"
1080            )));
1081        }
1082
1083        Ok(())
1084    }
1085}
1086
1087#[cfg(test)]
1088mod tests {
1089    use super::*;
1090    use crate::domain::transaction::stellar::test_helpers::{
1091        create_account_id, create_muxed_account, create_native_payment_operation,
1092        create_simple_v1_envelope, TEST_CONTRACT, TEST_PK, TEST_PK_2,
1093    };
1094    use crate::models::{AssetSpec, StellarAllowedTokensPolicy};
1095    use crate::services::provider::MockStellarProviderTrait;
1096    use crate::services::stellar_dex::MockStellarDexServiceTrait;
1097    use futures::future::ready;
1098    use soroban_rs::xdr::{
1099        AccountEntry, AccountEntryExt, Asset as XdrAsset, ChangeTrustAsset, ChangeTrustOp,
1100        HostFunction, InvokeContractArgs, InvokeHostFunctionOp, Operation, OperationBody,
1101        ScAddress, ScSymbol, SequenceNumber, SorobanAuthorizationEntry, SorobanAuthorizedFunction,
1102        SorobanCredentials, Thresholds, TimeBounds, TimePoint, Transaction, TransactionEnvelope,
1103        TransactionExt, TransactionV1Envelope,
1104    };
1105
1106    #[test]
1107    fn test_empty_operations_rejected() {
1108        let result = validate_operations(&[]);
1109        assert!(result.is_err());
1110        assert!(result
1111            .unwrap_err()
1112            .to_string()
1113            .contains("at least one operation"));
1114    }
1115
1116    #[test]
1117    fn test_too_many_operations_rejected() {
1118        let ops = vec![
1119            OperationSpec::Payment {
1120                destination: TEST_PK.to_string(),
1121                amount: 1000,
1122                asset: AssetSpec::Native,
1123            };
1124            101
1125        ];
1126        let result = validate_operations(&ops);
1127        assert!(result.is_err());
1128        assert!(result
1129            .unwrap_err()
1130            .to_string()
1131            .contains("maximum allowed is 100"));
1132    }
1133
1134    #[test]
1135    fn test_soroban_exclusivity_enforced() {
1136        // Multiple Soroban operations should fail
1137        let ops = vec![
1138            OperationSpec::InvokeContract {
1139                contract_address: TEST_CONTRACT.to_string(),
1140                function_name: "test".to_string(),
1141                args: vec![],
1142                auth: None,
1143            },
1144            OperationSpec::CreateContract {
1145                source: crate::models::ContractSource::Address {
1146                    address: TEST_PK.to_string(),
1147                },
1148                wasm_hash: "abc123".to_string(),
1149                salt: None,
1150                constructor_args: None,
1151                auth: None,
1152            },
1153        ];
1154        let result = validate_operations(&ops);
1155        assert!(result.is_err());
1156
1157        // Soroban mixed with non-Soroban should fail
1158        let ops = vec![
1159            OperationSpec::InvokeContract {
1160                contract_address: TEST_CONTRACT.to_string(),
1161                function_name: "test".to_string(),
1162                args: vec![],
1163                auth: None,
1164            },
1165            OperationSpec::Payment {
1166                destination: TEST_PK.to_string(),
1167                amount: 1000,
1168                asset: AssetSpec::Native,
1169            },
1170        ];
1171        let result = validate_operations(&ops);
1172        assert!(result.is_err());
1173        assert!(result
1174            .unwrap_err()
1175            .to_string()
1176            .contains("Soroban operations must be exclusive"));
1177    }
1178
1179    #[test]
1180    fn test_soroban_memo_restriction() {
1181        let soroban_op = vec![OperationSpec::InvokeContract {
1182            contract_address: TEST_CONTRACT.to_string(),
1183            function_name: "test".to_string(),
1184            args: vec![],
1185            auth: None,
1186        }];
1187
1188        // Soroban with text memo should fail
1189        let result = validate_soroban_memo_restriction(
1190            &soroban_op,
1191            &Some(MemoSpec::Text {
1192                value: "test".to_string(),
1193            }),
1194        );
1195        assert!(result.is_err());
1196
1197        // Soroban with MemoNone should succeed
1198        let result = validate_soroban_memo_restriction(&soroban_op, &Some(MemoSpec::None));
1199        assert!(result.is_ok());
1200
1201        // Soroban with no memo should succeed
1202        let result = validate_soroban_memo_restriction(&soroban_op, &None);
1203        assert!(result.is_ok());
1204    }
1205
1206    mod validate_fee_token_structure_tests {
1207        use super::*;
1208
1209        #[test]
1210        fn test_native_xlm_valid() {
1211            assert!(StellarTransactionValidator::validate_fee_token_structure("native").is_ok());
1212            assert!(StellarTransactionValidator::validate_fee_token_structure("XLM").is_ok());
1213            assert!(StellarTransactionValidator::validate_fee_token_structure("").is_ok());
1214        }
1215
1216        #[test]
1217        fn test_contract_address_valid() {
1218            assert!(
1219                StellarTransactionValidator::validate_fee_token_structure(TEST_CONTRACT).is_ok()
1220            );
1221        }
1222
1223        #[test]
1224        fn test_contract_address_invalid_length() {
1225            let result = StellarTransactionValidator::validate_fee_token_structure("C123");
1226            assert!(result.is_err());
1227            assert!(result
1228                .unwrap_err()
1229                .to_string()
1230                .contains("Invalid fee_token format"));
1231        }
1232
1233        #[test]
1234        fn test_classic_asset_valid() {
1235            let result = StellarTransactionValidator::validate_fee_token_structure(&format!(
1236                "USDC:{TEST_PK}"
1237            ));
1238            assert!(result.is_ok());
1239        }
1240
1241        #[test]
1242        fn test_classic_asset_code_too_long() {
1243            let result = StellarTransactionValidator::validate_fee_token_structure(&format!(
1244                "VERYLONGCODE1:{TEST_PK}"
1245            ));
1246            assert!(result.is_err());
1247            assert!(result
1248                .unwrap_err()
1249                .to_string()
1250                .contains("Invalid asset code length"));
1251        }
1252
1253        #[test]
1254        fn test_classic_asset_invalid_issuer_length() {
1255            let result = StellarTransactionValidator::validate_fee_token_structure("USDC:GSHORT");
1256            assert!(result.is_err());
1257            assert!(result
1258                .unwrap_err()
1259                .to_string()
1260                .contains("Invalid issuer address length"));
1261        }
1262
1263        #[test]
1264        fn test_classic_asset_invalid_issuer_prefix() {
1265            let result = StellarTransactionValidator::validate_fee_token_structure(
1266                "USDC:SAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF",
1267            );
1268            assert!(result.is_err());
1269            assert!(result
1270                .unwrap_err()
1271                .to_string()
1272                .contains("Invalid issuer address prefix"));
1273        }
1274
1275        #[test]
1276        fn test_invalid_format_multiple_colons() {
1277            let result =
1278                StellarTransactionValidator::validate_fee_token_structure("USDC:ISSUER:EXTRA");
1279            assert!(result.is_err());
1280            assert!(result
1281                .unwrap_err()
1282                .to_string()
1283                .contains("Invalid fee_token format"));
1284        }
1285    }
1286
1287    mod validate_allowed_token_tests {
1288        use super::*;
1289
1290        #[test]
1291        fn test_empty_allowed_list_allows_all() {
1292            let policy = RelayerStellarPolicy::default();
1293            assert!(StellarTransactionValidator::validate_allowed_token("native", &policy).is_ok());
1294            assert!(
1295                StellarTransactionValidator::validate_allowed_token(TEST_CONTRACT, &policy).is_ok()
1296            );
1297        }
1298
1299        #[test]
1300        fn test_native_allowed() {
1301            let mut policy = RelayerStellarPolicy::default();
1302            policy.allowed_tokens = Some(vec![StellarAllowedTokensPolicy {
1303                asset: "native".to_string(),
1304                metadata: None,
1305                swap_config: None,
1306                max_allowed_fee: None,
1307            }]);
1308            assert!(StellarTransactionValidator::validate_allowed_token("native", &policy).is_ok());
1309            assert!(StellarTransactionValidator::validate_allowed_token("", &policy).is_ok());
1310        }
1311
1312        #[test]
1313        fn test_native_not_allowed() {
1314            let mut policy = RelayerStellarPolicy::default();
1315            policy.allowed_tokens = Some(vec![StellarAllowedTokensPolicy {
1316                asset: format!("USDC:{TEST_PK}"),
1317                metadata: None,
1318                swap_config: None,
1319                max_allowed_fee: None,
1320            }]);
1321            let result = StellarTransactionValidator::validate_allowed_token("native", &policy);
1322            assert!(result.is_err());
1323            assert!(result
1324                .unwrap_err()
1325                .to_string()
1326                .contains("Native XLM not in allowed tokens list"));
1327        }
1328
1329        #[test]
1330        fn test_token_allowed() {
1331            let token = format!("USDC:{TEST_PK}");
1332            let mut policy = RelayerStellarPolicy::default();
1333            policy.allowed_tokens = Some(vec![StellarAllowedTokensPolicy {
1334                asset: token.clone(),
1335                metadata: None,
1336                swap_config: None,
1337                max_allowed_fee: None,
1338            }]);
1339            assert!(StellarTransactionValidator::validate_allowed_token(&token, &policy).is_ok());
1340        }
1341
1342        #[test]
1343        fn test_token_not_allowed() {
1344            let mut policy = RelayerStellarPolicy::default();
1345            policy.allowed_tokens = Some(vec![StellarAllowedTokensPolicy {
1346                asset: format!("USDC:{TEST_PK}"),
1347                metadata: None,
1348                swap_config: None,
1349                max_allowed_fee: None,
1350            }]);
1351            let result = StellarTransactionValidator::validate_allowed_token(
1352                &format!("AQUA:{TEST_PK_2}"),
1353                &policy,
1354            );
1355            assert!(result.is_err());
1356            assert!(result
1357                .unwrap_err()
1358                .to_string()
1359                .contains("not in allowed tokens list"));
1360        }
1361    }
1362
1363    mod validate_max_fee_tests {
1364        use super::*;
1365
1366        #[test]
1367        fn test_no_max_fee_allows_any() {
1368            let policy = RelayerStellarPolicy::default();
1369            assert!(StellarTransactionValidator::validate_max_fee(1_000_000, &policy).is_ok());
1370        }
1371
1372        #[test]
1373        fn test_fee_within_limit() {
1374            let mut policy = RelayerStellarPolicy::default();
1375            policy.max_fee = Some(1_000_000);
1376            assert!(StellarTransactionValidator::validate_max_fee(500_000, &policy).is_ok());
1377        }
1378
1379        #[test]
1380        fn test_fee_exceeds_limit() {
1381            let mut policy = RelayerStellarPolicy::default();
1382            policy.max_fee = Some(1_000_000);
1383            let result = StellarTransactionValidator::validate_max_fee(2_000_000, &policy);
1384            assert!(result.is_err());
1385            assert!(result.unwrap_err().to_string().contains("Max fee exceeded"));
1386        }
1387    }
1388
1389    mod validate_token_max_fee_tests {
1390        use super::*;
1391
1392        #[test]
1393        fn test_no_token_entry() {
1394            let policy = RelayerStellarPolicy::default();
1395            assert!(StellarTransactionValidator::validate_token_max_fee(
1396                "USDC:ISSUER",
1397                1_000_000,
1398                &policy
1399            )
1400            .is_ok());
1401        }
1402
1403        #[test]
1404        fn test_no_max_allowed_fee_in_entry() {
1405            let mut policy = RelayerStellarPolicy::default();
1406            policy.allowed_tokens = Some(vec![StellarAllowedTokensPolicy {
1407                asset: "USDC:ISSUER".to_string(),
1408                metadata: None,
1409                swap_config: None,
1410                max_allowed_fee: None,
1411            }]);
1412            assert!(StellarTransactionValidator::validate_token_max_fee(
1413                "USDC:ISSUER",
1414                1_000_000,
1415                &policy
1416            )
1417            .is_ok());
1418        }
1419
1420        #[test]
1421        fn test_fee_within_token_limit() {
1422            let mut policy = RelayerStellarPolicy::default();
1423            policy.allowed_tokens = Some(vec![StellarAllowedTokensPolicy {
1424                asset: "USDC:ISSUER".to_string(),
1425                metadata: None,
1426                swap_config: None,
1427                max_allowed_fee: Some(1_000_000),
1428            }]);
1429            assert!(StellarTransactionValidator::validate_token_max_fee(
1430                "USDC:ISSUER",
1431                500_000,
1432                &policy
1433            )
1434            .is_ok());
1435        }
1436
1437        #[test]
1438        fn test_fee_exceeds_token_limit() {
1439            let mut policy = RelayerStellarPolicy::default();
1440            policy.allowed_tokens = Some(vec![StellarAllowedTokensPolicy {
1441                asset: "USDC:ISSUER".to_string(),
1442                metadata: None,
1443                swap_config: None,
1444                max_allowed_fee: Some(1_000_000),
1445            }]);
1446            let result = StellarTransactionValidator::validate_token_max_fee(
1447                "USDC:ISSUER",
1448                2_000_000,
1449                &policy,
1450            );
1451            assert!(result.is_err());
1452            assert!(result.unwrap_err().to_string().contains("Max fee exceeded"));
1453        }
1454    }
1455
1456    mod extract_relayer_payments_tests {
1457        use super::*;
1458
1459        #[test]
1460        fn test_extract_single_payment() {
1461            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
1462            let payments =
1463                StellarTransactionValidator::extract_relayer_payments(&envelope, TEST_PK_2)
1464                    .unwrap();
1465            assert_eq!(payments.len(), 1);
1466            assert_eq!(payments[0].0, "native");
1467            assert_eq!(payments[0].1, 1_000_000);
1468        }
1469
1470        #[test]
1471        fn test_extract_no_payments_to_relayer() {
1472            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
1473            let payments =
1474                StellarTransactionValidator::extract_relayer_payments(&envelope, TEST_PK).unwrap();
1475            assert_eq!(payments.len(), 0);
1476        }
1477
1478        #[test]
1479        fn test_extract_negative_amount_rejected() {
1480            let payment_op = Operation {
1481                source_account: None,
1482                body: OperationBody::Payment(soroban_rs::xdr::PaymentOp {
1483                    destination: create_muxed_account(TEST_PK_2),
1484                    asset: XdrAsset::Native,
1485                    amount: -100, // Negative amount
1486                }),
1487            };
1488
1489            let tx = Transaction {
1490                source_account: create_muxed_account(TEST_PK),
1491                fee: 100,
1492                seq_num: SequenceNumber(1),
1493                cond: soroban_rs::xdr::Preconditions::None,
1494                memo: soroban_rs::xdr::Memo::None,
1495                operations: vec![payment_op].try_into().unwrap(),
1496                ext: TransactionExt::V0,
1497            };
1498
1499            let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1500                tx,
1501                signatures: vec![].try_into().unwrap(),
1502            });
1503
1504            let result =
1505                StellarTransactionValidator::extract_relayer_payments(&envelope, TEST_PK_2);
1506            assert!(result.is_err());
1507            assert!(result
1508                .unwrap_err()
1509                .to_string()
1510                .contains("Negative payment amount"));
1511        }
1512    }
1513
1514    mod validate_time_bounds_tests {
1515        use super::*;
1516
1517        #[test]
1518        fn test_no_time_bounds_is_ok() {
1519            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
1520            assert!(
1521                StellarTransactionValidator::validate_time_bounds_not_expired(&envelope).is_ok()
1522            );
1523        }
1524
1525        #[test]
1526        fn test_valid_time_bounds() {
1527            let now = Utc::now().timestamp() as u64;
1528            let payment_op = create_native_payment_operation(TEST_PK_2, 1_000_000);
1529
1530            let tx = Transaction {
1531                source_account: create_muxed_account(TEST_PK),
1532                fee: 100,
1533                seq_num: SequenceNumber(1),
1534                cond: soroban_rs::xdr::Preconditions::Time(TimeBounds {
1535                    min_time: TimePoint(now - 60),
1536                    max_time: TimePoint(now + 60),
1537                }),
1538                memo: soroban_rs::xdr::Memo::None,
1539                operations: vec![payment_op].try_into().unwrap(),
1540                ext: TransactionExt::V0,
1541            };
1542
1543            let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1544                tx,
1545                signatures: vec![].try_into().unwrap(),
1546            });
1547
1548            assert!(
1549                StellarTransactionValidator::validate_time_bounds_not_expired(&envelope).is_ok()
1550            );
1551        }
1552
1553        #[test]
1554        fn test_expired_transaction() {
1555            let now = Utc::now().timestamp() as u64;
1556            let payment_op = create_native_payment_operation(TEST_PK_2, 1_000_000);
1557
1558            let tx = Transaction {
1559                source_account: create_muxed_account(TEST_PK),
1560                fee: 100,
1561                seq_num: SequenceNumber(1),
1562                cond: soroban_rs::xdr::Preconditions::Time(TimeBounds {
1563                    min_time: TimePoint(now - 120),
1564                    max_time: TimePoint(now - 60), // Expired
1565                }),
1566                memo: soroban_rs::xdr::Memo::None,
1567                operations: vec![payment_op].try_into().unwrap(),
1568                ext: TransactionExt::V0,
1569            };
1570
1571            let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1572                tx,
1573                signatures: vec![].try_into().unwrap(),
1574            });
1575
1576            let result = StellarTransactionValidator::validate_time_bounds_not_expired(&envelope);
1577            assert!(result.is_err());
1578            assert!(result.unwrap_err().to_string().contains("has expired"));
1579        }
1580
1581        #[test]
1582        fn test_not_yet_valid_transaction() {
1583            let now = Utc::now().timestamp() as u64;
1584            let payment_op = create_native_payment_operation(TEST_PK_2, 1_000_000);
1585
1586            let tx = Transaction {
1587                source_account: create_muxed_account(TEST_PK),
1588                fee: 100,
1589                seq_num: SequenceNumber(1),
1590                cond: soroban_rs::xdr::Preconditions::Time(TimeBounds {
1591                    min_time: TimePoint(now + 60), // Not yet valid
1592                    max_time: TimePoint(now + 120),
1593                }),
1594                memo: soroban_rs::xdr::Memo::None,
1595                operations: vec![payment_op].try_into().unwrap(),
1596                ext: TransactionExt::V0,
1597            };
1598
1599            let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1600                tx,
1601                signatures: vec![].try_into().unwrap(),
1602            });
1603
1604            let result = StellarTransactionValidator::validate_time_bounds_not_expired(&envelope);
1605            assert!(result.is_err());
1606            assert!(result.unwrap_err().to_string().contains("not yet valid"));
1607        }
1608    }
1609
1610    mod validate_transaction_validity_duration_tests {
1611        use super::*;
1612
1613        #[test]
1614        fn test_duration_within_limit() {
1615            let now = Utc::now().timestamp() as u64;
1616            let payment_op = create_native_payment_operation(TEST_PK_2, 1_000_000);
1617
1618            let tx = Transaction {
1619                source_account: create_muxed_account(TEST_PK),
1620                fee: 100,
1621                seq_num: SequenceNumber(1),
1622                cond: soroban_rs::xdr::Preconditions::Time(TimeBounds {
1623                    min_time: TimePoint(0),
1624                    max_time: TimePoint(now + 60), // 1 minute from now
1625                }),
1626                memo: soroban_rs::xdr::Memo::None,
1627                operations: vec![payment_op].try_into().unwrap(),
1628                ext: TransactionExt::V0,
1629            };
1630
1631            let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1632                tx,
1633                signatures: vec![].try_into().unwrap(),
1634            });
1635
1636            let max_duration = Duration::minutes(5);
1637            assert!(
1638                StellarTransactionValidator::validate_transaction_validity_duration(
1639                    &envelope,
1640                    max_duration
1641                )
1642                .is_ok()
1643            );
1644        }
1645
1646        #[test]
1647        fn test_duration_exceeds_limit() {
1648            let now = Utc::now().timestamp() as u64;
1649            let payment_op = create_native_payment_operation(TEST_PK_2, 1_000_000);
1650
1651            let tx = Transaction {
1652                source_account: create_muxed_account(TEST_PK),
1653                fee: 100,
1654                seq_num: SequenceNumber(1),
1655                cond: soroban_rs::xdr::Preconditions::Time(TimeBounds {
1656                    min_time: TimePoint(0),
1657                    max_time: TimePoint(now + 600), // 10 minutes from now
1658                }),
1659                memo: soroban_rs::xdr::Memo::None,
1660                operations: vec![payment_op].try_into().unwrap(),
1661                ext: TransactionExt::V0,
1662            };
1663
1664            let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1665                tx,
1666                signatures: vec![].try_into().unwrap(),
1667            });
1668
1669            let max_duration = Duration::minutes(5);
1670            let result = StellarTransactionValidator::validate_transaction_validity_duration(
1671                &envelope,
1672                max_duration,
1673            );
1674            assert!(result.is_err());
1675            assert!(result
1676                .unwrap_err()
1677                .to_string()
1678                .contains("exceeds maximum allowed duration"));
1679        }
1680
1681        #[test]
1682        fn test_no_time_bounds_rejected() {
1683            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
1684            let max_duration = Duration::minutes(5);
1685            let result = StellarTransactionValidator::validate_transaction_validity_duration(
1686                &envelope,
1687                max_duration,
1688            );
1689            assert!(result.is_err());
1690            assert!(result
1691                .unwrap_err()
1692                .to_string()
1693                .contains("must have time bounds set"));
1694        }
1695    }
1696
1697    mod validate_sequence_number_tests {
1698        use super::*;
1699
1700        #[tokio::test]
1701        async fn test_valid_sequence_number() {
1702            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
1703
1704            let mut provider = MockStellarProviderTrait::new();
1705            provider.expect_get_account().returning(|_| {
1706                Box::pin(ready(Ok(AccountEntry {
1707                    account_id: create_account_id(TEST_PK),
1708                    balance: 1_000_000_000,
1709                    seq_num: SequenceNumber(0), // Current sequence is 0, tx sequence is 1
1710                    num_sub_entries: 0,
1711                    inflation_dest: None,
1712                    flags: 0,
1713                    home_domain: Default::default(),
1714                    thresholds: Thresholds([0; 4]),
1715                    signers: Default::default(),
1716                    ext: AccountEntryExt::V0,
1717                })))
1718            });
1719
1720            assert!(
1721                StellarTransactionValidator::validate_sequence_number(&envelope, &provider)
1722                    .await
1723                    .is_ok()
1724            );
1725        }
1726
1727        #[tokio::test]
1728        async fn test_equal_sequence_rejected() {
1729            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
1730
1731            let mut provider = MockStellarProviderTrait::new();
1732            provider.expect_get_account().returning(|_| {
1733                Box::pin(ready(Ok(AccountEntry {
1734                    account_id: create_account_id(TEST_PK),
1735                    balance: 1_000_000_000,
1736                    seq_num: SequenceNumber(1), // Same as tx sequence
1737                    num_sub_entries: 0,
1738                    inflation_dest: None,
1739                    flags: 0,
1740                    home_domain: Default::default(),
1741                    thresholds: Thresholds([0; 4]),
1742                    signers: Default::default(),
1743                    ext: AccountEntryExt::V0,
1744                })))
1745            });
1746
1747            let result =
1748                StellarTransactionValidator::validate_sequence_number(&envelope, &provider).await;
1749            assert!(result.is_err());
1750            assert!(result
1751                .unwrap_err()
1752                .to_string()
1753                .contains("strictly greater than"));
1754        }
1755
1756        #[tokio::test]
1757        async fn test_past_sequence_rejected() {
1758            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
1759
1760            let mut provider = MockStellarProviderTrait::new();
1761            provider.expect_get_account().returning(|_| {
1762                Box::pin(ready(Ok(AccountEntry {
1763                    account_id: create_account_id(TEST_PK),
1764                    balance: 1_000_000_000,
1765                    seq_num: SequenceNumber(10), // Higher than tx sequence
1766                    num_sub_entries: 0,
1767                    inflation_dest: None,
1768                    flags: 0,
1769                    home_domain: Default::default(),
1770                    thresholds: Thresholds([0; 4]),
1771                    signers: Default::default(),
1772                    ext: AccountEntryExt::V0,
1773                })))
1774            });
1775
1776            let result =
1777                StellarTransactionValidator::validate_sequence_number(&envelope, &provider).await;
1778            assert!(result.is_err());
1779            assert!(result.unwrap_err().to_string().contains("is invalid"));
1780        }
1781    }
1782
1783    mod validate_operations_count_tests {
1784        use super::*;
1785
1786        #[test]
1787        fn test_valid_operations_count() {
1788            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
1789            assert!(StellarTransactionValidator::validate_operations_count(&envelope).is_ok());
1790        }
1791
1792        #[test]
1793        fn test_too_many_operations() {
1794            // VecM has a max of 100, so we can't actually create an envelope with 101 operations
1795            // Instead, we test that the validation logic works correctly by checking the limit
1796            // This test verifies the validation function would reject if it could receive such an envelope
1797
1798            // Create an envelope with exactly 100 operations (the maximum)
1799            let operations: Vec<Operation> = (0..100)
1800                .map(|_| create_native_payment_operation(TEST_PK_2, 100))
1801                .collect();
1802
1803            let tx = Transaction {
1804                source_account: create_muxed_account(TEST_PK),
1805                fee: 100,
1806                seq_num: SequenceNumber(1),
1807                cond: soroban_rs::xdr::Preconditions::None,
1808                memo: soroban_rs::xdr::Memo::None,
1809                operations: operations.try_into().unwrap(),
1810                ext: TransactionExt::V0,
1811            };
1812
1813            let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1814                tx,
1815                signatures: vec![].try_into().unwrap(),
1816            });
1817
1818            // 100 operations should be OK
1819            let result = StellarTransactionValidator::validate_operations_count(&envelope);
1820            assert!(result.is_ok());
1821        }
1822    }
1823
1824    mod validate_source_account_tests {
1825        use super::*;
1826
1827        #[test]
1828        fn test_source_account_not_relayer() {
1829            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
1830            assert!(
1831                StellarTransactionValidator::validate_source_account_not_relayer(
1832                    &envelope, TEST_PK_2
1833                )
1834                .is_ok()
1835            );
1836        }
1837
1838        #[test]
1839        fn test_source_account_is_relayer_rejected() {
1840            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
1841            let result = StellarTransactionValidator::validate_source_account_not_relayer(
1842                &envelope, TEST_PK,
1843            );
1844            assert!(result.is_err());
1845            assert!(result
1846                .unwrap_err()
1847                .to_string()
1848                .contains("cannot be the relayer address"));
1849        }
1850    }
1851
1852    mod validate_operation_types_tests {
1853        use super::*;
1854
1855        #[test]
1856        fn test_payment_operation_allowed() {
1857            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
1858            let policy = RelayerStellarPolicy::default();
1859            assert!(StellarTransactionValidator::validate_operation_types(
1860                &envelope, TEST_PK_2, &policy
1861            )
1862            .is_ok());
1863        }
1864
1865        #[test]
1866        fn test_account_merge_rejected() {
1867            let operation = Operation {
1868                source_account: None,
1869                body: OperationBody::AccountMerge(create_muxed_account(TEST_PK_2)),
1870            };
1871
1872            let tx = Transaction {
1873                source_account: create_muxed_account(TEST_PK),
1874                fee: 100,
1875                seq_num: SequenceNumber(1),
1876                cond: soroban_rs::xdr::Preconditions::None,
1877                memo: soroban_rs::xdr::Memo::None,
1878                operations: vec![operation].try_into().unwrap(),
1879                ext: TransactionExt::V0,
1880            };
1881
1882            let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1883                tx,
1884                signatures: vec![].try_into().unwrap(),
1885            });
1886
1887            let policy = RelayerStellarPolicy::default();
1888            let result = StellarTransactionValidator::validate_operation_types(
1889                &envelope, TEST_PK_2, &policy,
1890            );
1891            assert!(result.is_err());
1892            assert!(result
1893                .unwrap_err()
1894                .to_string()
1895                .contains("AccountMerge operations are not allowed"));
1896        }
1897
1898        #[test]
1899        fn test_set_options_rejected() {
1900            let operation = Operation {
1901                source_account: None,
1902                body: OperationBody::SetOptions(soroban_rs::xdr::SetOptionsOp {
1903                    inflation_dest: None,
1904                    clear_flags: None,
1905                    set_flags: None,
1906                    master_weight: None,
1907                    low_threshold: None,
1908                    med_threshold: None,
1909                    high_threshold: None,
1910                    home_domain: None,
1911                    signer: None,
1912                }),
1913            };
1914
1915            let tx = Transaction {
1916                source_account: create_muxed_account(TEST_PK),
1917                fee: 100,
1918                seq_num: SequenceNumber(1),
1919                cond: soroban_rs::xdr::Preconditions::None,
1920                memo: soroban_rs::xdr::Memo::None,
1921                operations: vec![operation].try_into().unwrap(),
1922                ext: TransactionExt::V0,
1923            };
1924
1925            let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1926                tx,
1927                signatures: vec![].try_into().unwrap(),
1928            });
1929
1930            let policy = RelayerStellarPolicy::default();
1931            let result = StellarTransactionValidator::validate_operation_types(
1932                &envelope, TEST_PK_2, &policy,
1933            );
1934            assert!(result.is_err());
1935            assert!(result
1936                .unwrap_err()
1937                .to_string()
1938                .contains("SetOptions operations are not allowed"));
1939        }
1940
1941        #[test]
1942        fn test_change_trust_allowed() {
1943            let operation = Operation {
1944                source_account: None,
1945                body: OperationBody::ChangeTrust(ChangeTrustOp {
1946                    line: ChangeTrustAsset::CreditAlphanum4(soroban_rs::xdr::AlphaNum4 {
1947                        asset_code: soroban_rs::xdr::AssetCode4(*b"USDC"),
1948                        issuer: create_account_id(TEST_PK_2),
1949                    }),
1950                    limit: 1_000_000_000,
1951                }),
1952            };
1953
1954            let tx = Transaction {
1955                source_account: create_muxed_account(TEST_PK),
1956                fee: 100,
1957                seq_num: SequenceNumber(1),
1958                cond: soroban_rs::xdr::Preconditions::None,
1959                memo: soroban_rs::xdr::Memo::None,
1960                operations: vec![operation].try_into().unwrap(),
1961                ext: TransactionExt::V0,
1962            };
1963
1964            let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1965                tx,
1966                signatures: vec![].try_into().unwrap(),
1967            });
1968
1969            let policy = RelayerStellarPolicy::default();
1970            assert!(StellarTransactionValidator::validate_operation_types(
1971                &envelope, TEST_PK_2, &policy
1972            )
1973            .is_ok());
1974        }
1975    }
1976
1977    mod validate_token_payment_tests {
1978        use super::*;
1979
1980        #[test]
1981        fn test_valid_native_payment() {
1982            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
1983            let policy = RelayerStellarPolicy::default();
1984
1985            let result = StellarTransactionValidator::validate_token_payment(
1986                &envelope, TEST_PK_2, "native", 1_000_000, &policy,
1987            );
1988            assert!(result.is_ok());
1989        }
1990
1991        #[test]
1992        fn test_no_payment_to_relayer() {
1993            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
1994            let policy = RelayerStellarPolicy::default();
1995
1996            // Wrong relayer address - no payments will match
1997            let result = StellarTransactionValidator::validate_token_payment(
1998                &envelope, TEST_PK, // Different from destination
1999                "native", 1_000_000, &policy,
2000            );
2001            assert!(result.is_err());
2002            assert!(result
2003                .unwrap_err()
2004                .to_string()
2005                .contains("No payment operation found to relayer"));
2006        }
2007
2008        #[test]
2009        fn test_wrong_token_in_payment() {
2010            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
2011            let policy = RelayerStellarPolicy::default();
2012
2013            // Expecting USDC but envelope has native payment
2014            let result = StellarTransactionValidator::validate_token_payment(
2015                &envelope,
2016                TEST_PK_2,
2017                &format!("USDC:{TEST_PK}"),
2018                1_000_000,
2019                &policy,
2020            );
2021            assert!(result.is_err());
2022            assert!(result
2023                .unwrap_err()
2024                .to_string()
2025                .contains("No payment found for expected token"));
2026        }
2027
2028        #[test]
2029        fn test_insufficient_payment_amount() {
2030            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
2031            let policy = RelayerStellarPolicy::default();
2032
2033            // Expecting 2M but envelope has 1M payment
2034            let result = StellarTransactionValidator::validate_token_payment(
2035                &envelope, TEST_PK_2, "native", 2_000_000, &policy,
2036            );
2037            assert!(result.is_err());
2038            assert!(result
2039                .unwrap_err()
2040                .to_string()
2041                .contains("Insufficient token payment"));
2042        }
2043
2044        #[test]
2045        fn test_payment_within_tolerance() {
2046            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
2047            let policy = RelayerStellarPolicy::default();
2048
2049            let result = StellarTransactionValidator::validate_token_payment(
2050                &envelope, TEST_PK_2, "native", 990_000, &policy,
2051            );
2052            assert!(result.is_ok());
2053        }
2054
2055        #[test]
2056        fn test_token_not_in_allowed_list() {
2057            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
2058            let mut policy = RelayerStellarPolicy::default();
2059            policy.allowed_tokens = Some(vec![StellarAllowedTokensPolicy {
2060                asset: format!("USDC:{TEST_PK}"),
2061                metadata: None,
2062                swap_config: None,
2063                max_allowed_fee: None,
2064            }]);
2065
2066            // Native payment but only USDC is allowed
2067            let result = StellarTransactionValidator::validate_token_payment(
2068                &envelope, TEST_PK_2, "native", 1_000_000, &policy,
2069            );
2070            assert!(result.is_err());
2071            assert!(result
2072                .unwrap_err()
2073                .to_string()
2074                .contains("not in allowed tokens list"));
2075        }
2076
2077        #[test]
2078        fn test_payment_exceeds_token_max_fee() {
2079            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
2080            let mut policy = RelayerStellarPolicy::default();
2081            policy.allowed_tokens = Some(vec![StellarAllowedTokensPolicy {
2082                asset: "native".to_string(),
2083                metadata: None,
2084                swap_config: None,
2085                max_allowed_fee: Some(500_000), // Max 0.5 XLM
2086            }]);
2087
2088            // Payment is 1M but max allowed is 500K
2089            let result = StellarTransactionValidator::validate_token_payment(
2090                &envelope, TEST_PK_2, "native", 1_000_000, &policy,
2091            );
2092            assert!(result.is_err());
2093            assert!(result.unwrap_err().to_string().contains("Max fee exceeded"));
2094        }
2095
2096        #[test]
2097        fn test_classic_asset_payment() {
2098            let usdc_asset = format!("USDC:{TEST_PK}");
2099            let payment_op = Operation {
2100                source_account: None,
2101                body: OperationBody::Payment(soroban_rs::xdr::PaymentOp {
2102                    destination: create_muxed_account(TEST_PK_2),
2103                    asset: XdrAsset::CreditAlphanum4(soroban_rs::xdr::AlphaNum4 {
2104                        asset_code: soroban_rs::xdr::AssetCode4(*b"USDC"),
2105                        issuer: create_account_id(TEST_PK),
2106                    }),
2107                    amount: 1_000_000,
2108                }),
2109            };
2110
2111            let tx = Transaction {
2112                source_account: create_muxed_account(TEST_PK),
2113                fee: 100,
2114                seq_num: SequenceNumber(1),
2115                cond: soroban_rs::xdr::Preconditions::None,
2116                memo: soroban_rs::xdr::Memo::None,
2117                operations: vec![payment_op].try_into().unwrap(),
2118                ext: TransactionExt::V0,
2119            };
2120
2121            let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
2122                tx,
2123                signatures: vec![].try_into().unwrap(),
2124            });
2125
2126            let mut policy = RelayerStellarPolicy::default();
2127            policy.allowed_tokens = Some(vec![StellarAllowedTokensPolicy {
2128                asset: usdc_asset.clone(),
2129                metadata: None,
2130                swap_config: None,
2131                max_allowed_fee: None,
2132            }]);
2133
2134            let result = StellarTransactionValidator::validate_token_payment(
2135                &envelope,
2136                TEST_PK_2,
2137                &usdc_asset,
2138                1_000_000,
2139                &policy,
2140            );
2141            assert!(result.is_ok());
2142        }
2143
2144        #[test]
2145        fn test_multiple_payments_finds_correct_token() {
2146            // Create envelope with two payments: one USDC to relayer, one XLM to someone else
2147            let usdc_asset = format!("USDC:{TEST_PK}");
2148            let usdc_payment = Operation {
2149                source_account: None,
2150                body: OperationBody::Payment(soroban_rs::xdr::PaymentOp {
2151                    destination: create_muxed_account(TEST_PK_2),
2152                    asset: XdrAsset::CreditAlphanum4(soroban_rs::xdr::AlphaNum4 {
2153                        asset_code: soroban_rs::xdr::AssetCode4(*b"USDC"),
2154                        issuer: create_account_id(TEST_PK),
2155                    }),
2156                    amount: 500_000,
2157                }),
2158            };
2159
2160            let xlm_payment = create_native_payment_operation(TEST_PK, 1_000_000);
2161
2162            let tx = Transaction {
2163                source_account: create_muxed_account(TEST_PK),
2164                fee: 100,
2165                seq_num: SequenceNumber(1),
2166                cond: soroban_rs::xdr::Preconditions::None,
2167                memo: soroban_rs::xdr::Memo::None,
2168                operations: vec![xlm_payment, usdc_payment].try_into().unwrap(),
2169                ext: TransactionExt::V0,
2170            };
2171
2172            let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
2173                tx,
2174                signatures: vec![].try_into().unwrap(),
2175            });
2176
2177            let policy = RelayerStellarPolicy::default();
2178
2179            // Should find the USDC payment to TEST_PK_2
2180            let result = StellarTransactionValidator::validate_token_payment(
2181                &envelope,
2182                TEST_PK_2,
2183                &usdc_asset,
2184                500_000,
2185                &policy,
2186            );
2187            assert!(result.is_ok());
2188        }
2189    }
2190
2191    mod validate_user_fee_payment_amounts_tests {
2192        use super::*;
2193        use soroban_rs::stellar_rpc_client::{
2194            GetLatestLedgerResponse, SimulateTransactionResponse,
2195        };
2196        use soroban_rs::xdr::WriteXdr;
2197
2198        const USDC_ISSUER: &str = TEST_PK;
2199
2200        fn create_usdc_payment_envelope(
2201            source: &str,
2202            destination: &str,
2203            amount: i64,
2204        ) -> TransactionEnvelope {
2205            let payment_op = Operation {
2206                source_account: None,
2207                body: OperationBody::Payment(PaymentOp {
2208                    destination: create_muxed_account(destination),
2209                    asset: soroban_rs::xdr::Asset::CreditAlphanum4(soroban_rs::xdr::AlphaNum4 {
2210                        asset_code: soroban_rs::xdr::AssetCode4(*b"USDC"),
2211                        issuer: create_account_id(USDC_ISSUER),
2212                    }),
2213                    amount,
2214                }),
2215            };
2216
2217            let tx = Transaction {
2218                source_account: create_muxed_account(source),
2219                fee: 100,
2220                seq_num: SequenceNumber(1),
2221                cond: soroban_rs::xdr::Preconditions::None,
2222                memo: soroban_rs::xdr::Memo::None,
2223                operations: vec![payment_op].try_into().unwrap(),
2224                ext: TransactionExt::V0,
2225            };
2226
2227            TransactionEnvelope::Tx(TransactionV1Envelope {
2228                tx,
2229                signatures: vec![].try_into().unwrap(),
2230            })
2231        }
2232
2233        fn create_usdc_policy() -> RelayerStellarPolicy {
2234            let usdc_asset = format!("USDC:{USDC_ISSUER}");
2235            let mut policy = RelayerStellarPolicy::default();
2236            policy.allowed_tokens = Some(vec![StellarAllowedTokensPolicy {
2237                asset: usdc_asset,
2238                metadata: None,
2239                swap_config: None,
2240                max_allowed_fee: None,
2241            }]);
2242            policy
2243        }
2244
2245        fn create_mock_provider_with_balance(balance: i64) -> MockStellarProviderTrait {
2246            let mut provider = MockStellarProviderTrait::new();
2247
2248            // Mock get_account for source account
2249            provider.expect_get_account().returning(move |_| {
2250                Box::pin(ready(Ok(AccountEntry {
2251                    account_id: create_account_id(TEST_PK),
2252                    balance,
2253                    seq_num: SequenceNumber(1),
2254                    num_sub_entries: 0,
2255                    inflation_dest: None,
2256                    flags: 0,
2257                    home_domain: Default::default(),
2258                    thresholds: Thresholds([0; 4]),
2259                    signers: Default::default(),
2260                    ext: AccountEntryExt::V0,
2261                })))
2262            });
2263
2264            // Mock get_latest_ledger for fee estimation
2265            provider.expect_get_latest_ledger().returning(|| {
2266                Box::pin(ready(Ok(GetLatestLedgerResponse {
2267                    id: "test".to_string(),
2268                    protocol_version: 20,
2269                    sequence: 1000,
2270                })))
2271            });
2272
2273            // Mock simulate_transaction_envelope for Soroban fee estimation
2274            provider
2275                .expect_simulate_transaction_envelope()
2276                .returning(|_| {
2277                    Box::pin(ready(Ok(SimulateTransactionResponse {
2278                        min_resource_fee: 100,
2279                        transaction_data: String::new(),
2280                        ..Default::default()
2281                    })))
2282                });
2283
2284            // Mock get_ledger_entries for trustline balance check
2285            provider.expect_get_ledger_entries().returning(|_| {
2286                use soroban_rs::stellar_rpc_client::{GetLedgerEntriesResponse, LedgerEntryResult};
2287                use soroban_rs::xdr::{
2288                    LedgerEntry, LedgerEntryData, LedgerEntryExt, TrustLineAsset, TrustLineEntry,
2289                    TrustLineEntryExt,
2290                };
2291
2292                let trustline_entry = TrustLineEntry {
2293                    account_id: create_account_id(TEST_PK),
2294                    asset: TrustLineAsset::CreditAlphanum4(soroban_rs::xdr::AlphaNum4 {
2295                        asset_code: soroban_rs::xdr::AssetCode4(*b"USDC"),
2296                        issuer: create_account_id(TEST_PK_2),
2297                    }),
2298                    balance: 10_000_000, // 10 USDC
2299                    limit: 1_000_000_000,
2300                    flags: 1,
2301                    ext: TrustLineEntryExt::V0,
2302                };
2303
2304                let ledger_entry = LedgerEntry {
2305                    last_modified_ledger_seq: 0,
2306                    data: LedgerEntryData::Trustline(trustline_entry),
2307                    ext: LedgerEntryExt::V0,
2308                };
2309
2310                let xdr_base64 = ledger_entry
2311                    .data
2312                    .to_xdr_base64(soroban_rs::xdr::Limits::none())
2313                    .unwrap();
2314
2315                Box::pin(ready(Ok(GetLedgerEntriesResponse {
2316                    entries: Some(vec![LedgerEntryResult {
2317                        key: String::new(),
2318                        xdr: xdr_base64,
2319                        last_modified_ledger: 0,
2320                        live_until_ledger_seq_ledger_seq: None,
2321                    }]),
2322                    latest_ledger: 0,
2323                })))
2324            });
2325
2326            provider
2327        }
2328
2329        fn create_mock_dex_service() -> MockStellarDexServiceTrait {
2330            let mut dex_service = MockStellarDexServiceTrait::new();
2331            dex_service
2332                .expect_get_xlm_to_token_quote()
2333                .returning(|_, _, _, _| {
2334                    Box::pin(ready(Ok(
2335                        crate::services::stellar_dex::StellarQuoteResponse {
2336                            input_asset: "native".to_string(),
2337                            output_asset: format!("USDC:{USDC_ISSUER}"),
2338                            in_amount: 100,
2339                            out_amount: 1_000_000, // 0.1 USDC
2340                            price_impact_pct: 0.0,
2341                            slippage_bps: 100,
2342                            path: None,
2343                        },
2344                    )))
2345                });
2346            dex_service
2347        }
2348
2349        #[tokio::test]
2350        async fn test_valid_fee_payment() {
2351            let envelope = create_usdc_payment_envelope(TEST_PK, TEST_PK_2, 1_000_000);
2352            let policy = create_usdc_policy();
2353            let provider = create_mock_provider_with_balance(10_000_000_000);
2354            let dex_service = create_mock_dex_service();
2355
2356            let result = StellarTransactionValidator::validate_user_fee_payment_amounts(
2357                &envelope,
2358                TEST_PK_2,
2359                &policy,
2360                &provider,
2361                &dex_service,
2362            )
2363            .await;
2364
2365            assert!(result.is_ok());
2366        }
2367
2368        #[tokio::test]
2369        async fn test_no_fee_payment() {
2370            // Envelope with payment to different address (not the relayer)
2371            let envelope = create_usdc_payment_envelope(TEST_PK, TEST_PK, 1_000_000);
2372            let policy = create_usdc_policy();
2373            let provider = create_mock_provider_with_balance(10_000_000_000);
2374            let dex_service = create_mock_dex_service();
2375
2376            let result = StellarTransactionValidator::validate_user_fee_payment_amounts(
2377                &envelope,
2378                TEST_PK_2, // Different from destination
2379                &policy,
2380                &provider,
2381                &dex_service,
2382            )
2383            .await;
2384
2385            assert!(result.is_err());
2386            assert!(result
2387                .unwrap_err()
2388                .to_string()
2389                .contains("must include a fee payment operation to the relayer"));
2390        }
2391
2392        #[tokio::test]
2393        async fn test_multiple_fee_payments_rejected() {
2394            // Create envelope with two USDC payments to relayer
2395            let payment1 = Operation {
2396                source_account: None,
2397                body: OperationBody::Payment(PaymentOp {
2398                    destination: create_muxed_account(TEST_PK_2),
2399                    asset: soroban_rs::xdr::Asset::CreditAlphanum4(soroban_rs::xdr::AlphaNum4 {
2400                        asset_code: soroban_rs::xdr::AssetCode4(*b"USDC"),
2401                        issuer: create_account_id(USDC_ISSUER),
2402                    }),
2403                    amount: 500_000,
2404                }),
2405            };
2406            let payment2 = Operation {
2407                source_account: None,
2408                body: OperationBody::Payment(PaymentOp {
2409                    destination: create_muxed_account(TEST_PK_2),
2410                    asset: soroban_rs::xdr::Asset::CreditAlphanum4(soroban_rs::xdr::AlphaNum4 {
2411                        asset_code: soroban_rs::xdr::AssetCode4(*b"USDC"),
2412                        issuer: create_account_id(USDC_ISSUER),
2413                    }),
2414                    amount: 500_000,
2415                }),
2416            };
2417
2418            let tx = Transaction {
2419                source_account: create_muxed_account(TEST_PK),
2420                fee: 100,
2421                seq_num: SequenceNumber(1),
2422                cond: soroban_rs::xdr::Preconditions::None,
2423                memo: soroban_rs::xdr::Memo::None,
2424                operations: vec![payment1, payment2].try_into().unwrap(),
2425                ext: TransactionExt::V0,
2426            };
2427
2428            let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
2429                tx,
2430                signatures: vec![].try_into().unwrap(),
2431            });
2432
2433            let policy = create_usdc_policy();
2434            let provider = create_mock_provider_with_balance(10_000_000_000);
2435            let dex_service = create_mock_dex_service();
2436
2437            let result = StellarTransactionValidator::validate_user_fee_payment_amounts(
2438                &envelope,
2439                TEST_PK_2,
2440                &policy,
2441                &provider,
2442                &dex_service,
2443            )
2444            .await;
2445
2446            assert!(result.is_err());
2447            assert!(result
2448                .unwrap_err()
2449                .to_string()
2450                .contains("exactly one fee payment operation"));
2451        }
2452
2453        #[tokio::test]
2454        async fn test_token_not_allowed() {
2455            // Create envelope with EURC payment (not in allowed list)
2456            let payment_op = Operation {
2457                source_account: None,
2458                body: OperationBody::Payment(PaymentOp {
2459                    destination: create_muxed_account(TEST_PK_2),
2460                    asset: soroban_rs::xdr::Asset::CreditAlphanum4(soroban_rs::xdr::AlphaNum4 {
2461                        asset_code: soroban_rs::xdr::AssetCode4(*b"EURC"),
2462                        issuer: create_account_id(TEST_PK),
2463                    }),
2464                    amount: 1_000_000,
2465                }),
2466            };
2467
2468            let tx = Transaction {
2469                source_account: create_muxed_account(TEST_PK),
2470                fee: 100,
2471                seq_num: SequenceNumber(1),
2472                cond: soroban_rs::xdr::Preconditions::None,
2473                memo: soroban_rs::xdr::Memo::None,
2474                operations: vec![payment_op].try_into().unwrap(),
2475                ext: TransactionExt::V0,
2476            };
2477
2478            let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
2479                tx,
2480                signatures: vec![].try_into().unwrap(),
2481            });
2482
2483            let policy = create_usdc_policy(); // Only USDC is allowed
2484
2485            let provider = create_mock_provider_with_balance(10_000_000_000);
2486            let dex_service = create_mock_dex_service();
2487
2488            let result = StellarTransactionValidator::validate_user_fee_payment_amounts(
2489                &envelope,
2490                TEST_PK_2,
2491                &policy,
2492                &provider,
2493                &dex_service,
2494            )
2495            .await;
2496
2497            assert!(result.is_err());
2498            assert!(result
2499                .unwrap_err()
2500                .to_string()
2501                .contains("not in allowed tokens list"));
2502        }
2503
2504        #[tokio::test]
2505        async fn test_fee_exceeds_token_max() {
2506            let envelope = create_usdc_payment_envelope(TEST_PK, TEST_PK_2, 1_000_000);
2507            let usdc_asset = format!("USDC:{USDC_ISSUER}");
2508            let mut policy = RelayerStellarPolicy::default();
2509            policy.allowed_tokens = Some(vec![StellarAllowedTokensPolicy {
2510                asset: usdc_asset,
2511                metadata: None,
2512                swap_config: None,
2513                max_allowed_fee: Some(500_000), // Lower than payment amount
2514            }]);
2515
2516            let provider = create_mock_provider_with_balance(10_000_000_000);
2517            let dex_service = create_mock_dex_service();
2518
2519            let result = StellarTransactionValidator::validate_user_fee_payment_amounts(
2520                &envelope,
2521                TEST_PK_2,
2522                &policy,
2523                &provider,
2524                &dex_service,
2525            )
2526            .await;
2527
2528            assert!(result.is_err());
2529            assert!(result.unwrap_err().to_string().contains("Max fee exceeded"));
2530        }
2531
2532        #[tokio::test]
2533        async fn test_insufficient_payment_amount() {
2534            let envelope = create_usdc_payment_envelope(TEST_PK, TEST_PK_2, 1_000_000);
2535            let policy = create_usdc_policy();
2536            let provider = create_mock_provider_with_balance(10_000_000_000);
2537
2538            // Mock DEX to require more than the payment amount
2539            let mut dex_service = MockStellarDexServiceTrait::new();
2540            dex_service
2541                .expect_get_xlm_to_token_quote()
2542                .returning(|_, _, _, _| {
2543                    Box::pin(ready(Ok(
2544                        crate::services::stellar_dex::StellarQuoteResponse {
2545                            input_asset: "native".to_string(),
2546                            output_asset: "USDC:...".to_string(),
2547                            in_amount: 200,
2548                            out_amount: 2_000_000, // More than the 1M payment
2549                            price_impact_pct: 0.0,
2550                            slippage_bps: 100,
2551                            path: None,
2552                        },
2553                    )))
2554                });
2555
2556            let result = StellarTransactionValidator::validate_user_fee_payment_amounts(
2557                &envelope,
2558                TEST_PK_2,
2559                &policy,
2560                &provider,
2561                &dex_service,
2562            )
2563            .await;
2564
2565            assert!(result.is_err());
2566            assert!(result
2567                .unwrap_err()
2568                .to_string()
2569                .contains("Insufficient token payment"));
2570        }
2571
2572        #[tokio::test]
2573        async fn test_insufficient_user_balance() {
2574            let envelope = create_usdc_payment_envelope(TEST_PK, TEST_PK_2, 1_000_000);
2575            let policy = create_usdc_policy();
2576
2577            // Create provider with low USDC trustline balance
2578            let mut provider = MockStellarProviderTrait::new();
2579
2580            provider.expect_get_account().returning(move |_| {
2581                Box::pin(ready(Ok(AccountEntry {
2582                    account_id: create_account_id(TEST_PK),
2583                    balance: 10_000_000_000,
2584                    seq_num: SequenceNumber(1),
2585                    num_sub_entries: 0,
2586                    inflation_dest: None,
2587                    flags: 0,
2588                    home_domain: Default::default(),
2589                    thresholds: Thresholds([0; 4]),
2590                    signers: Default::default(),
2591                    ext: AccountEntryExt::V0,
2592                })))
2593            });
2594
2595            provider.expect_get_latest_ledger().returning(|| {
2596                Box::pin(ready(Ok(GetLatestLedgerResponse {
2597                    id: "test".to_string(),
2598                    protocol_version: 20,
2599                    sequence: 1000,
2600                })))
2601            });
2602
2603            provider
2604                .expect_simulate_transaction_envelope()
2605                .returning(|_| {
2606                    Box::pin(ready(Ok(SimulateTransactionResponse {
2607                        min_resource_fee: 100,
2608                        transaction_data: String::new(),
2609                        ..Default::default()
2610                    })))
2611                });
2612
2613            // Mock get_ledger_entries with low USDC balance
2614            provider.expect_get_ledger_entries().returning(|_| {
2615                use soroban_rs::stellar_rpc_client::{GetLedgerEntriesResponse, LedgerEntryResult};
2616                use soroban_rs::xdr::{
2617                    LedgerEntry, LedgerEntryData, LedgerEntryExt, TrustLineAsset, TrustLineEntry,
2618                    TrustLineEntryExt,
2619                };
2620
2621                let trustline_entry = TrustLineEntry {
2622                    account_id: create_account_id(TEST_PK),
2623                    asset: TrustLineAsset::CreditAlphanum4(soroban_rs::xdr::AlphaNum4 {
2624                        asset_code: soroban_rs::xdr::AssetCode4(*b"USDC"),
2625                        issuer: create_account_id(USDC_ISSUER),
2626                    }),
2627                    balance: 500_000, // Only 0.05 USDC - insufficient
2628                    limit: 1_000_000_000,
2629                    flags: 1,
2630                    ext: TrustLineEntryExt::V0,
2631                };
2632
2633                let ledger_entry = LedgerEntry {
2634                    last_modified_ledger_seq: 0,
2635                    data: LedgerEntryData::Trustline(trustline_entry),
2636                    ext: LedgerEntryExt::V0,
2637                };
2638
2639                let xdr_base64 = ledger_entry
2640                    .data
2641                    .to_xdr_base64(soroban_rs::xdr::Limits::none())
2642                    .unwrap();
2643
2644                Box::pin(ready(Ok(GetLedgerEntriesResponse {
2645                    entries: Some(vec![LedgerEntryResult {
2646                        key: String::new(),
2647                        xdr: xdr_base64,
2648                        last_modified_ledger: 0,
2649                        live_until_ledger_seq_ledger_seq: None,
2650                    }]),
2651                    latest_ledger: 0,
2652                })))
2653            });
2654
2655            let dex_service = create_mock_dex_service();
2656
2657            let result = StellarTransactionValidator::validate_user_fee_payment_amounts(
2658                &envelope,
2659                TEST_PK_2,
2660                &policy,
2661                &provider,
2662                &dex_service,
2663            )
2664            .await;
2665
2666            assert!(result.is_err());
2667            assert!(result
2668                .unwrap_err()
2669                .to_string()
2670                .contains("Insufficient balance"));
2671        }
2672
2673        #[tokio::test]
2674        async fn test_valid_fee_payment_with_usdc() {
2675            let envelope = create_usdc_payment_envelope(TEST_PK, TEST_PK_2, 1_000_000);
2676            let policy = create_usdc_policy();
2677            let provider = create_mock_provider_with_balance(10_000_000_000);
2678            let dex_service = create_mock_dex_service();
2679
2680            let result = StellarTransactionValidator::validate_user_fee_payment_amounts(
2681                &envelope,
2682                TEST_PK_2,
2683                &policy,
2684                &provider,
2685                &dex_service,
2686            )
2687            .await;
2688
2689            assert!(result.is_ok());
2690        }
2691
2692        #[tokio::test]
2693        async fn test_dex_conversion_failure() {
2694            let envelope = create_usdc_payment_envelope(TEST_PK, TEST_PK_2, 1_000_000);
2695            let policy = create_usdc_policy();
2696            let provider = create_mock_provider_with_balance(10_000_000_000);
2697
2698            let mut dex_service = MockStellarDexServiceTrait::new();
2699            dex_service
2700                .expect_get_xlm_to_token_quote()
2701                .returning(|_, _, _, _| {
2702                    Box::pin(ready(Err(
2703                        crate::services::stellar_dex::StellarDexServiceError::UnknownError(
2704                            "DEX unavailable".to_string(),
2705                        ),
2706                    )))
2707                });
2708
2709            let result = StellarTransactionValidator::validate_user_fee_payment_amounts(
2710                &envelope,
2711                TEST_PK_2,
2712                &policy,
2713                &provider,
2714                &dex_service,
2715            )
2716            .await;
2717
2718            assert!(result.is_err());
2719            assert!(result
2720                .unwrap_err()
2721                .to_string()
2722                .contains("Failed to convert XLM fee to token"));
2723        }
2724    }
2725
2726    mod validate_contract_invocation_tests {
2727        use super::*;
2728
2729        #[test]
2730        fn test_invoke_contract_allowed() {
2731            let invoke_op = InvokeHostFunctionOp {
2732                host_function: HostFunction::InvokeContract(InvokeContractArgs {
2733                    contract_address: ScAddress::Contract(soroban_rs::xdr::ContractId(
2734                        soroban_rs::xdr::Hash([0u8; 32]),
2735                    )),
2736                    function_name: ScSymbol("test".try_into().unwrap()),
2737                    args: Default::default(),
2738                }),
2739                auth: Default::default(),
2740            };
2741
2742            let policy = RelayerStellarPolicy::default();
2743            assert!(StellarTransactionValidator::validate_contract_invocation(
2744                &invoke_op, 0, TEST_PK_2, &policy
2745            )
2746            .is_ok());
2747        }
2748
2749        #[test]
2750        fn test_create_contract_rejected() {
2751            let invoke_op = InvokeHostFunctionOp {
2752                host_function: HostFunction::CreateContract(soroban_rs::xdr::CreateContractArgs {
2753                    contract_id_preimage: soroban_rs::xdr::ContractIdPreimage::Address(
2754                        soroban_rs::xdr::ContractIdPreimageFromAddress {
2755                            address: ScAddress::Account(create_account_id(TEST_PK)),
2756                            salt: soroban_rs::xdr::Uint256([0u8; 32]),
2757                        },
2758                    ),
2759                    executable: soroban_rs::xdr::ContractExecutable::Wasm(soroban_rs::xdr::Hash(
2760                        [0u8; 32],
2761                    )),
2762                }),
2763                auth: Default::default(),
2764            };
2765
2766            let policy = RelayerStellarPolicy::default();
2767            let result = StellarTransactionValidator::validate_contract_invocation(
2768                &invoke_op, 0, TEST_PK_2, &policy,
2769            );
2770            assert!(result.is_err());
2771            assert!(result
2772                .unwrap_err()
2773                .to_string()
2774                .contains("CreateContract not allowed"));
2775        }
2776
2777        #[test]
2778        fn test_upload_wasm_rejected() {
2779            let invoke_op = InvokeHostFunctionOp {
2780                host_function: HostFunction::UploadContractWasm(vec![0u8; 100].try_into().unwrap()),
2781                auth: Default::default(),
2782            };
2783
2784            let policy = RelayerStellarPolicy::default();
2785            let result = StellarTransactionValidator::validate_contract_invocation(
2786                &invoke_op, 0, TEST_PK_2, &policy,
2787            );
2788            assert!(result.is_err());
2789            assert!(result
2790                .unwrap_err()
2791                .to_string()
2792                .contains("UploadContractWasm not allowed"));
2793        }
2794
2795        #[test]
2796        fn test_relayer_in_auth_rejected() {
2797            let auth_entry = SorobanAuthorizationEntry {
2798                credentials: SorobanCredentials::Address(
2799                    soroban_rs::xdr::SorobanAddressCredentials {
2800                        address: ScAddress::Account(create_account_id(TEST_PK_2)),
2801                        nonce: 0,
2802                        signature_expiration_ledger: 0,
2803                        signature: soroban_rs::xdr::ScVal::Void,
2804                    },
2805                ),
2806                root_invocation: soroban_rs::xdr::SorobanAuthorizedInvocation {
2807                    function: SorobanAuthorizedFunction::ContractFn(
2808                        soroban_rs::xdr::InvokeContractArgs {
2809                            contract_address: ScAddress::Contract(soroban_rs::xdr::ContractId(
2810                                soroban_rs::xdr::Hash([0u8; 32]),
2811                            )),
2812                            function_name: ScSymbol("test".try_into().unwrap()),
2813                            args: Default::default(),
2814                        },
2815                    ),
2816                    sub_invocations: Default::default(),
2817                },
2818            };
2819
2820            let invoke_op = InvokeHostFunctionOp {
2821                host_function: HostFunction::InvokeContract(InvokeContractArgs {
2822                    contract_address: ScAddress::Contract(soroban_rs::xdr::ContractId(
2823                        soroban_rs::xdr::Hash([0u8; 32]),
2824                    )),
2825                    function_name: ScSymbol("test".try_into().unwrap()),
2826                    args: Default::default(),
2827                }),
2828                auth: vec![auth_entry].try_into().unwrap(),
2829            };
2830
2831            let policy = RelayerStellarPolicy::default();
2832            let result = StellarTransactionValidator::validate_contract_invocation(
2833                &invoke_op, 0, TEST_PK_2, // Relayer address matches auth entry
2834                &policy,
2835            );
2836            assert!(result.is_err());
2837            assert!(result.unwrap_err().to_string().contains("requires relayer"));
2838        }
2839    }
2840}