openzeppelin_relayer/domain/transaction/stellar/
utils.rs

1//! Utility functions for Stellar transaction domain logic.
2use crate::constants::{
3    DEFAULT_CONVERSION_SLIPPAGE_PERCENTAGE, STELLAR_DEFAULT_TRANSACTION_FEE, STELLAR_MAX_OPERATIONS,
4};
5use crate::domain::relayer::xdr_utils::{extract_operations, xdr_needs_simulation};
6use crate::models::{AssetSpec, OperationSpec, RelayerError, RelayerStellarPolicy};
7use crate::services::provider::StellarProviderTrait;
8use crate::services::stellar_dex::StellarDexServiceTrait;
9use base64::{engine::general_purpose, Engine};
10use chrono::{DateTime, Utc};
11use serde::Serialize;
12use soroban_rs::xdr::{
13    AccountId, AlphaNum12, AlphaNum4, Asset, ChangeTrustAsset, ContractDataEntry, ContractId, Hash,
14    LedgerEntryData, LedgerKey, LedgerKeyContractData, Limits, Operation, Preconditions,
15    PublicKey as XdrPublicKey, ReadXdr, ScAddress, ScSymbol, ScVal, TimeBounds, TimePoint,
16    TransactionEnvelope, TransactionMeta, Uint256, VecM,
17};
18use std::str::FromStr;
19use stellar_strkey::ed25519::PublicKey;
20use thiserror::Error;
21use tracing::{debug, warn};
22
23// ============================================================================
24// Error Types
25// ============================================================================
26
27/// Errors that can occur during Stellar transaction utility operations.
28///
29/// This error type is specific to Stellar transaction utilities and provides
30/// detailed error information. It can be converted to `RelayerError` using
31/// the `From` trait implementation.
32#[derive(Error, Debug, Serialize)]
33pub enum StellarTransactionUtilsError {
34    #[error("Sequence overflow: {0}")]
35    SequenceOverflow(String),
36
37    #[error("Failed to parse XDR: {0}")]
38    XdrParseFailed(String),
39
40    #[error("Failed to extract operations: {0}")]
41    OperationExtractionFailed(String),
42
43    #[error("Failed to check if simulation is needed: {0}")]
44    SimulationCheckFailed(String),
45
46    #[error("Failed to simulate transaction: {0}")]
47    SimulationFailed(String),
48
49    #[error("Transaction simulation returned no results")]
50    SimulationNoResults,
51
52    #[error("Failed to get DEX quote: {0}")]
53    DexQuoteFailed(String),
54
55    #[error("Invalid asset identifier format: {0}")]
56    InvalidAssetFormat(String),
57
58    #[error("Asset code too long (max {0} characters): {1}")]
59    AssetCodeTooLong(usize, String),
60
61    #[error("Too many operations (max {0})")]
62    TooManyOperations(usize),
63
64    #[error("Cannot add operations to fee-bump transactions")]
65    CannotModifyFeeBump,
66
67    #[error("Cannot set time bounds on fee-bump transactions")]
68    CannotSetTimeBoundsOnFeeBump,
69
70    #[error("V0 transactions are not supported")]
71    V0TransactionsNotSupported,
72
73    #[error("Cannot update sequence number on fee bump transaction")]
74    CannotUpdateSequenceOnFeeBump,
75
76    #[error("Invalid transaction format: {0}")]
77    InvalidTransactionFormat(String),
78
79    #[error("Invalid account address '{0}': {1}")]
80    InvalidAccountAddress(String, String),
81
82    #[error("Invalid contract address '{0}': {1}")]
83    InvalidContractAddress(String, String),
84
85    #[error("Failed to create {0} symbol: {1:?}")]
86    SymbolCreationFailed(String, String),
87
88    #[error("Failed to create {0} key vector: {1:?}")]
89    KeyVectorCreationFailed(String, String),
90
91    #[error("Failed to query contract data (Persistent) for {0}: {1}")]
92    ContractDataQueryPersistentFailed(String, String),
93
94    #[error("Failed to query contract data (Temporary) for {0}: {1}")]
95    ContractDataQueryTemporaryFailed(String, String),
96
97    #[error("Failed to parse ledger entry XDR for {0}: {1}")]
98    LedgerEntryParseFailed(String, String),
99
100    #[error("No entries found for {0}")]
101    NoEntriesFound(String),
102
103    #[error("Empty entries for {0}")]
104    EmptyEntries(String),
105
106    #[error("Unexpected ledger entry type for {0} (expected ContractData)")]
107    UnexpectedLedgerEntryType(String),
108
109    // Token-specific errors
110    #[error("Asset code cannot be empty in asset identifier: {0}")]
111    EmptyAssetCode(String),
112
113    #[error("Issuer address cannot be empty in asset identifier: {0}")]
114    EmptyIssuerAddress(String),
115
116    #[error("Invalid issuer address length (expected {0} characters): {1}")]
117    InvalidIssuerLength(usize, String),
118
119    #[error("Invalid issuer address format (must start with '{0}'): {1}")]
120    InvalidIssuerPrefix(char, String),
121
122    #[error("Failed to fetch account for balance: {0}")]
123    AccountFetchFailed(String),
124
125    #[error("Failed to query trustline for asset {0}: {1}")]
126    TrustlineQueryFailed(String, String),
127
128    #[error("No trustline found for asset {0} on account {1}")]
129    NoTrustlineFound(String, String),
130
131    #[error("Unsupported trustline entry version")]
132    UnsupportedTrustlineVersion,
133
134    #[error("Unexpected ledger entry type for trustline query")]
135    UnexpectedTrustlineEntryType,
136
137    #[error("Balance too large (i128 hi={0}, lo={1}) to fit in u64")]
138    BalanceTooLarge(i64, u64),
139
140    #[error("Negative balance not allowed: i128 lo={0}")]
141    NegativeBalanceI128(u64),
142
143    #[error("Negative balance not allowed: i64={0}")]
144    NegativeBalanceI64(i64),
145
146    #[error("Unexpected balance value type in contract data: {0:?}. Expected I128, U64, or I64")]
147    UnexpectedBalanceType(String),
148
149    #[error("Unexpected ledger entry type for contract data query")]
150    UnexpectedContractDataEntryType,
151
152    #[error("Native asset should be handled before trustline query")]
153    NativeAssetInTrustlineQuery,
154
155    #[error("Failed to invoke contract function '{0}': {1}")]
156    ContractInvocationFailed(String, String),
157}
158
159impl From<StellarTransactionUtilsError> for RelayerError {
160    fn from(error: StellarTransactionUtilsError) -> Self {
161        match &error {
162            StellarTransactionUtilsError::SequenceOverflow(msg)
163            | StellarTransactionUtilsError::SimulationCheckFailed(msg)
164            | StellarTransactionUtilsError::SimulationFailed(msg)
165            | StellarTransactionUtilsError::XdrParseFailed(msg)
166            | StellarTransactionUtilsError::OperationExtractionFailed(msg)
167            | StellarTransactionUtilsError::DexQuoteFailed(msg) => {
168                RelayerError::Internal(msg.clone())
169            }
170            StellarTransactionUtilsError::SimulationNoResults => RelayerError::Internal(
171                "Transaction simulation failed: no results returned".to_string(),
172            ),
173            StellarTransactionUtilsError::InvalidAssetFormat(msg)
174            | StellarTransactionUtilsError::InvalidTransactionFormat(msg) => {
175                RelayerError::ValidationError(msg.clone())
176            }
177            StellarTransactionUtilsError::AssetCodeTooLong(max_len, code) => {
178                RelayerError::ValidationError(format!(
179                    "Asset code too long (max {max_len} characters): {code}"
180                ))
181            }
182            StellarTransactionUtilsError::TooManyOperations(max) => {
183                RelayerError::ValidationError(format!("Too many operations (max {max})"))
184            }
185            StellarTransactionUtilsError::CannotModifyFeeBump => RelayerError::ValidationError(
186                "Cannot add operations to fee-bump transactions".to_string(),
187            ),
188            StellarTransactionUtilsError::CannotSetTimeBoundsOnFeeBump => {
189                RelayerError::ValidationError(
190                    "Cannot set time bounds on fee-bump transactions".to_string(),
191                )
192            }
193            StellarTransactionUtilsError::V0TransactionsNotSupported => {
194                RelayerError::ValidationError("V0 transactions are not supported".to_string())
195            }
196            StellarTransactionUtilsError::CannotUpdateSequenceOnFeeBump => {
197                RelayerError::ValidationError(
198                    "Cannot update sequence number on fee bump transaction".to_string(),
199                )
200            }
201            StellarTransactionUtilsError::InvalidAccountAddress(_, msg)
202            | StellarTransactionUtilsError::InvalidContractAddress(_, msg)
203            | StellarTransactionUtilsError::SymbolCreationFailed(_, msg)
204            | StellarTransactionUtilsError::KeyVectorCreationFailed(_, msg)
205            | StellarTransactionUtilsError::ContractDataQueryPersistentFailed(_, msg)
206            | StellarTransactionUtilsError::ContractDataQueryTemporaryFailed(_, msg)
207            | StellarTransactionUtilsError::LedgerEntryParseFailed(_, msg) => {
208                RelayerError::Internal(msg.clone())
209            }
210            StellarTransactionUtilsError::NoEntriesFound(_)
211            | StellarTransactionUtilsError::EmptyEntries(_)
212            | StellarTransactionUtilsError::UnexpectedLedgerEntryType(_)
213            | StellarTransactionUtilsError::EmptyAssetCode(_)
214            | StellarTransactionUtilsError::EmptyIssuerAddress(_)
215            | StellarTransactionUtilsError::NoTrustlineFound(_, _)
216            | StellarTransactionUtilsError::UnsupportedTrustlineVersion
217            | StellarTransactionUtilsError::UnexpectedTrustlineEntryType
218            | StellarTransactionUtilsError::BalanceTooLarge(_, _)
219            | StellarTransactionUtilsError::NegativeBalanceI128(_)
220            | StellarTransactionUtilsError::NegativeBalanceI64(_)
221            | StellarTransactionUtilsError::UnexpectedBalanceType(_)
222            | StellarTransactionUtilsError::UnexpectedContractDataEntryType
223            | StellarTransactionUtilsError::NativeAssetInTrustlineQuery => {
224                RelayerError::ValidationError(error.to_string())
225            }
226            StellarTransactionUtilsError::InvalidIssuerLength(expected, actual) => {
227                RelayerError::ValidationError(format!(
228                    "Invalid issuer address length (expected {expected} characters): {actual}"
229                ))
230            }
231            StellarTransactionUtilsError::InvalidIssuerPrefix(prefix, addr) => {
232                RelayerError::ValidationError(format!(
233                    "Invalid issuer address format (must start with '{prefix}'): {addr}"
234                ))
235            }
236            StellarTransactionUtilsError::AccountFetchFailed(msg)
237            | StellarTransactionUtilsError::TrustlineQueryFailed(_, msg)
238            | StellarTransactionUtilsError::ContractInvocationFailed(_, msg) => {
239                RelayerError::ProviderError(msg.clone())
240            }
241        }
242    }
243}
244
245/// Returns true if any operation needs simulation (contract invocation, creation, or wasm upload).
246pub fn needs_simulation(operations: &[OperationSpec]) -> bool {
247    operations.iter().any(|op| {
248        matches!(
249            op,
250            OperationSpec::InvokeContract { .. }
251                | OperationSpec::CreateContract { .. }
252                | OperationSpec::UploadWasm { .. }
253        )
254    })
255}
256
257pub fn next_sequence_u64(seq_num: i64) -> Result<u64, RelayerError> {
258    let next_i64 = seq_num
259        .checked_add(1)
260        .ok_or_else(|| RelayerError::ProviderError("sequence overflow".into()))?;
261    u64::try_from(next_i64)
262        .map_err(|_| RelayerError::ProviderError("sequence overflows u64".into()))
263}
264
265pub fn i64_from_u64(value: u64) -> Result<i64, RelayerError> {
266    i64::try_from(value).map_err(|_| RelayerError::ProviderError("u64→i64 overflow".into()))
267}
268
269/// Detects if an error is due to a bad sequence number.
270/// Returns true if the error message contains indicators of sequence number mismatch.
271pub fn is_bad_sequence_error(error_msg: &str) -> bool {
272    let error_lower = error_msg.to_lowercase();
273    error_lower.contains("txbadseq")
274}
275
276/// Fetches the current sequence number from the blockchain and calculates the next usable sequence.
277/// This is a shared helper that can be used by both stellar_relayer and stellar_transaction.
278///
279/// # Returns
280/// The next usable sequence number (on-chain sequence + 1)
281pub async fn fetch_next_sequence_from_chain<P>(
282    provider: &P,
283    relayer_address: &str,
284) -> Result<u64, String>
285where
286    P: StellarProviderTrait,
287{
288    debug!(
289        "Fetching sequence from chain for address: {}",
290        relayer_address
291    );
292
293    // Fetch account info from chain
294    let account = provider.get_account(relayer_address).await.map_err(|e| {
295        warn!(
296            address = %relayer_address,
297            error = %e,
298            "get_account failed in fetch_next_sequence_from_chain"
299        );
300        format!("Failed to fetch account from chain: {e}")
301    })?;
302
303    let on_chain_seq = account.seq_num.0; // Extract the i64 value
304    let next_usable = next_sequence_u64(on_chain_seq)
305        .map_err(|e| format!("Failed to calculate next sequence: {e}"))?;
306
307    debug!(
308        "Fetched sequence from chain: on-chain={}, next usable={}",
309        on_chain_seq, next_usable
310    );
311    Ok(next_usable)
312}
313
314/// Convert a V0 transaction to V1 format for signing.
315/// This is needed because the signature payload for V0 transactions uses V1 format internally.
316pub fn convert_v0_to_v1_transaction(
317    v0_tx: &soroban_rs::xdr::TransactionV0,
318) -> soroban_rs::xdr::Transaction {
319    soroban_rs::xdr::Transaction {
320        source_account: soroban_rs::xdr::MuxedAccount::Ed25519(
321            v0_tx.source_account_ed25519.clone(),
322        ),
323        fee: v0_tx.fee,
324        seq_num: v0_tx.seq_num.clone(),
325        cond: match v0_tx.time_bounds.clone() {
326            Some(tb) => soroban_rs::xdr::Preconditions::Time(tb),
327            None => soroban_rs::xdr::Preconditions::None,
328        },
329        memo: v0_tx.memo.clone(),
330        operations: v0_tx.operations.clone(),
331        ext: soroban_rs::xdr::TransactionExt::V0,
332    }
333}
334
335/// Create a signature payload for the given envelope type
336pub fn create_signature_payload(
337    envelope: &soroban_rs::xdr::TransactionEnvelope,
338    network_id: &soroban_rs::xdr::Hash,
339) -> Result<soroban_rs::xdr::TransactionSignaturePayload, RelayerError> {
340    let tagged_transaction = match envelope {
341        soroban_rs::xdr::TransactionEnvelope::TxV0(e) => {
342            // For V0, convert to V1 transaction format for signing
343            let v1_tx = convert_v0_to_v1_transaction(&e.tx);
344            soroban_rs::xdr::TransactionSignaturePayloadTaggedTransaction::Tx(v1_tx)
345        }
346        soroban_rs::xdr::TransactionEnvelope::Tx(e) => {
347            soroban_rs::xdr::TransactionSignaturePayloadTaggedTransaction::Tx(e.tx.clone())
348        }
349        soroban_rs::xdr::TransactionEnvelope::TxFeeBump(e) => {
350            soroban_rs::xdr::TransactionSignaturePayloadTaggedTransaction::TxFeeBump(e.tx.clone())
351        }
352    };
353
354    Ok(soroban_rs::xdr::TransactionSignaturePayload {
355        network_id: network_id.clone(),
356        tagged_transaction,
357    })
358}
359
360/// Create signature payload for a transaction directly (for operations-based signing)
361pub fn create_transaction_signature_payload(
362    transaction: &soroban_rs::xdr::Transaction,
363    network_id: &soroban_rs::xdr::Hash,
364) -> soroban_rs::xdr::TransactionSignaturePayload {
365    soroban_rs::xdr::TransactionSignaturePayload {
366        network_id: network_id.clone(),
367        tagged_transaction: soroban_rs::xdr::TransactionSignaturePayloadTaggedTransaction::Tx(
368            transaction.clone(),
369        ),
370    }
371}
372
373/// Update the sequence number in a transaction envelope.
374///
375/// Only V1 (Tx) envelopes are supported; V0 and fee-bump envelopes return an error.
376pub fn update_envelope_sequence(
377    envelope: &mut TransactionEnvelope,
378    sequence: i64,
379) -> Result<(), StellarTransactionUtilsError> {
380    match envelope {
381        TransactionEnvelope::Tx(v1) => {
382            v1.tx.seq_num = soroban_rs::xdr::SequenceNumber(sequence);
383            Ok(())
384        }
385        TransactionEnvelope::TxV0(_) => {
386            Err(StellarTransactionUtilsError::V0TransactionsNotSupported)
387        }
388        TransactionEnvelope::TxFeeBump(_) => {
389            Err(StellarTransactionUtilsError::CannotUpdateSequenceOnFeeBump)
390        }
391    }
392}
393
394/// Extract the fee (in stroops) from a V1 transaction envelope.
395pub fn envelope_fee_in_stroops(
396    envelope: &TransactionEnvelope,
397) -> Result<u64, StellarTransactionUtilsError> {
398    match envelope {
399        TransactionEnvelope::Tx(env) => Ok(u64::from(env.tx.fee)),
400        _ => Err(StellarTransactionUtilsError::InvalidTransactionFormat(
401            "Expected V1 transaction envelope".to_string(),
402        )),
403    }
404}
405
406// ============================================================================
407// Account and Contract Address Utilities
408// ============================================================================
409
410/// Parse a Stellar account address string into an AccountId XDR type.
411///
412/// # Arguments
413///
414/// * `account_id` - Stellar account address (must be valid PublicKey)
415///
416/// # Returns
417///
418/// AccountId XDR type or error if address is invalid
419pub fn parse_account_id(account_id: &str) -> Result<AccountId, StellarTransactionUtilsError> {
420    let account_pk = PublicKey::from_str(account_id).map_err(|e| {
421        StellarTransactionUtilsError::InvalidAccountAddress(account_id.to_string(), e.to_string())
422    })?;
423    let account_uint256 = Uint256(account_pk.0);
424    let account_xdr_pk = XdrPublicKey::PublicKeyTypeEd25519(account_uint256);
425    Ok(AccountId(account_xdr_pk))
426}
427
428/// Parse a contract address string into a ContractId and extract the hash.
429///
430/// # Arguments
431///
432/// * `contract_address` - Contract address in StrKey format
433///
434/// # Returns
435///
436/// Contract hash (Hash) or error if address is invalid
437pub fn parse_contract_address(
438    contract_address: &str,
439) -> Result<Hash, StellarTransactionUtilsError> {
440    let contract_id = ContractId::from_str(contract_address).map_err(|e| {
441        StellarTransactionUtilsError::InvalidContractAddress(
442            contract_address.to_string(),
443            e.to_string(),
444        )
445    })?;
446    Ok(contract_id.0)
447}
448
449// ============================================================================
450// Contract Data Utilities
451// ============================================================================
452
453/// Create an ScVal key for contract data queries.
454///
455/// Creates a ScVal::Vec containing a symbol and optional address.
456/// Used for SEP-41 token interface keys like "Balance" and "Decimals".
457///
458/// # Arguments
459///
460/// * `symbol` - Symbol name (e.g., "Balance", "Decimals")
461/// * `address` - Optional ScAddress to include in the key
462///
463/// # Returns
464///
465/// ScVal::Vec key or error if creation fails
466pub fn create_contract_data_key(
467    symbol: &str,
468    address: Option<ScAddress>,
469) -> Result<ScVal, StellarTransactionUtilsError> {
470    if address.is_none() {
471        let sym = ScSymbol::try_from(symbol).map_err(|e| {
472            StellarTransactionUtilsError::SymbolCreationFailed(symbol.to_string(), format!("{e:?}"))
473        })?;
474        return Ok(ScVal::Symbol(sym));
475    }
476
477    let mut key_items: Vec<ScVal> =
478        vec![ScVal::Symbol(ScSymbol::try_from(symbol).map_err(|e| {
479            StellarTransactionUtilsError::SymbolCreationFailed(symbol.to_string(), format!("{e:?}"))
480        })?)];
481
482    if let Some(addr) = address {
483        key_items.push(ScVal::Address(addr));
484    }
485
486    let key_vec: VecM<ScVal, { u32::MAX }> = VecM::try_from(key_items).map_err(|e| {
487        StellarTransactionUtilsError::KeyVectorCreationFailed(symbol.to_string(), format!("{e:?}"))
488    })?;
489
490    Ok(ScVal::Vec(Some(soroban_rs::xdr::ScVec(key_vec))))
491}
492
493/// Query contract data with Persistent/Temporary durability fallback.
494///
495/// Queries contract data storage, trying Persistent durability first,
496/// then falling back to Temporary if not found. This handles both
497/// production tokens (Persistent) and test tokens (Temporary).
498///
499/// # Arguments
500///
501/// * `provider` - Stellar provider for querying ledger entries
502/// * `contract_hash` - Contract hash (Hash)
503/// * `key` - ScVal key to query
504/// * `error_context` - Context string for error messages
505///
506/// # Returns
507///
508/// GetLedgerEntriesResponse or error if query fails
509pub async fn query_contract_data_with_fallback<P>(
510    provider: &P,
511    contract_hash: Hash,
512    key: ScVal,
513    error_context: &str,
514) -> Result<soroban_rs::stellar_rpc_client::GetLedgerEntriesResponse, StellarTransactionUtilsError>
515where
516    P: StellarProviderTrait + Send + Sync,
517{
518    let contract_address_sc =
519        soroban_rs::xdr::ScAddress::Contract(soroban_rs::xdr::ContractId(contract_hash));
520
521    let mut ledger_key = LedgerKey::ContractData(LedgerKeyContractData {
522        contract: contract_address_sc.clone(),
523        key: key.clone(),
524        durability: soroban_rs::xdr::ContractDataDurability::Persistent,
525    });
526
527    // Query ledger entry with Persistent durability
528    let mut ledger_entries = provider
529        .get_ledger_entries(&[ledger_key.clone()])
530        .await
531        .map_err(|e| {
532            StellarTransactionUtilsError::ContractDataQueryPersistentFailed(
533                error_context.to_string(),
534                e.to_string(),
535            )
536        })?;
537
538    // If not found, try Temporary durability
539    if ledger_entries
540        .entries
541        .as_ref()
542        .map(|e| e.is_empty())
543        .unwrap_or(true)
544    {
545        ledger_key = LedgerKey::ContractData(LedgerKeyContractData {
546            contract: contract_address_sc,
547            key,
548            durability: soroban_rs::xdr::ContractDataDurability::Temporary,
549        });
550        ledger_entries = provider
551            .get_ledger_entries(&[ledger_key])
552            .await
553            .map_err(|e| {
554                StellarTransactionUtilsError::ContractDataQueryTemporaryFailed(
555                    error_context.to_string(),
556                    e.to_string(),
557                )
558            })?;
559    }
560
561    Ok(ledger_entries)
562}
563
564/// Parse a ledger entry from base64 XDR string.
565///
566/// Handles both LedgerEntry and LedgerEntryChange formats. If the XDR is a
567/// LedgerEntryChange, extracts the LedgerEntry from it.
568///
569/// # Arguments
570///
571/// * `xdr_string` - Base64-encoded XDR string
572/// * `context` - Context string for error messages
573///
574/// # Returns
575///
576/// Parsed LedgerEntry or error if parsing fails
577pub fn parse_ledger_entry_from_xdr(
578    xdr_string: &str,
579    context: &str,
580) -> Result<LedgerEntryData, StellarTransactionUtilsError> {
581    let trimmed_xdr = xdr_string.trim();
582
583    // Ensure valid base64
584    if general_purpose::STANDARD.decode(trimmed_xdr).is_err() {
585        return Err(StellarTransactionUtilsError::LedgerEntryParseFailed(
586            context.to_string(),
587            "Invalid base64".to_string(),
588        ));
589    }
590
591    // Parse as LedgerEntryData (what Soroban RPC actually returns)
592    match LedgerEntryData::from_xdr_base64(trimmed_xdr, Limits::none()) {
593        Ok(data) => Ok(data),
594        Err(e) => Err(StellarTransactionUtilsError::LedgerEntryParseFailed(
595            context.to_string(),
596            format!("Failed to parse LedgerEntryData: {e}"),
597        )),
598    }
599}
600
601/// Extract ScVal from contract data entry.
602///
603/// Parses the first entry from GetLedgerEntriesResponse and extracts
604/// the ScVal from ContractDataEntry.
605///
606/// # Arguments
607///
608/// * `ledger_entries` - Response from get_ledger_entries
609/// * `context` - Context string for error messages and logging
610///
611/// # Returns
612///
613/// ScVal from contract data or error if extraction fails
614pub fn extract_scval_from_contract_data(
615    ledger_entries: &soroban_rs::stellar_rpc_client::GetLedgerEntriesResponse,
616    context: &str,
617) -> Result<ScVal, StellarTransactionUtilsError> {
618    let entries = ledger_entries
619        .entries
620        .as_ref()
621        .ok_or_else(|| StellarTransactionUtilsError::NoEntriesFound(context.into()))?;
622
623    if entries.is_empty() {
624        return Err(StellarTransactionUtilsError::EmptyEntries(context.into()));
625    }
626
627    let entry_xdr = &entries[0].xdr;
628    let entry = parse_ledger_entry_from_xdr(entry_xdr, context)?;
629
630    match entry {
631        LedgerEntryData::ContractData(ContractDataEntry { val, .. }) => Ok(val.clone()),
632
633        _ => Err(StellarTransactionUtilsError::UnexpectedLedgerEntryType(
634            context.into(),
635        )),
636    }
637}
638
639/// Extracts the return value from TransactionMeta if available.
640///
641/// Supports both V3 and V4 TransactionMeta versions for backward compatibility.
642/// - V3: soroban_meta.return_value (ScVal, required)
643/// - V4: soroban_meta.return_value (Option<ScVal>, optional)
644///
645/// # Arguments
646///
647/// * `result_meta` - TransactionMeta to extract return value from
648///
649/// # Returns
650///
651/// Some(&ScVal) if return value is available, None otherwise
652pub fn extract_return_value_from_meta(result_meta: &TransactionMeta) -> Option<&ScVal> {
653    match result_meta {
654        TransactionMeta::V3(meta_v3) => meta_v3.soroban_meta.as_ref().map(|m| &m.return_value),
655        TransactionMeta::V4(meta_v4) => meta_v4
656            .soroban_meta
657            .as_ref()
658            .and_then(|m| m.return_value.as_ref()),
659        _ => None,
660    }
661}
662
663/// Extract a u32 value from an ScVal.
664///
665/// Handles multiple ScVal types that can represent numeric values.
666///
667/// # Arguments
668///
669/// * `val` - ScVal to extract from
670/// * `context` - Context string (for logging)
671///
672/// # Returns
673///
674/// Some(u32) if extraction succeeds, None otherwise
675pub fn extract_u32_from_scval(val: &ScVal, context: &str) -> Option<u32> {
676    let result = match val {
677        ScVal::U32(n) => Ok(*n),
678        ScVal::I32(n) => (*n).try_into().map_err(|_| "Negative I32"),
679        ScVal::U64(n) => (*n).try_into().map_err(|_| "U64 overflow"),
680        ScVal::I64(n) => (*n).try_into().map_err(|_| "I64 overflow/negative"),
681        ScVal::U128(n) => {
682            if n.hi == 0 {
683                n.lo.try_into().map_err(|_| "U128 lo overflow")
684            } else {
685                Err("U128 hi set")
686            }
687        }
688        ScVal::I128(n) => {
689            if n.hi == 0 {
690                n.lo.try_into().map_err(|_| "I128 lo overflow")
691            } else {
692                Err("I128 hi set/negative")
693            }
694        }
695        _ => Err("Unsupported ScVal type"),
696    };
697
698    match result {
699        Ok(v) => Some(v),
700        Err(msg) => {
701            warn!(context = %context, val = ?val, "Failed to extract u32: {}", msg);
702            None
703        }
704    }
705}
706
707// ============================================================================
708// Gas Abstraction Utility Functions
709// ============================================================================
710
711/// Convert raw token amount to UI amount based on decimals
712///
713/// Uses pure integer arithmetic to avoid floating-point precision errors.
714/// This is safer for financial calculations where precision is critical.
715pub fn amount_to_ui_amount(amount: u64, decimals: u8) -> String {
716    if decimals == 0 {
717        return amount.to_string();
718    }
719
720    let amount_str = amount.to_string();
721    let len = amount_str.len();
722    let decimals_usize = decimals as usize;
723
724    let combined = if len > decimals_usize {
725        let split_idx = len - decimals_usize;
726        let whole = &amount_str[..split_idx];
727        let frac = &amount_str[split_idx..];
728        format!("{whole}.{frac}")
729    } else {
730        // Need to pad with leading zeros
731        let zeros = "0".repeat(decimals_usize - len);
732        format!("0.{zeros}{amount_str}")
733    };
734
735    // Trim trailing zeros
736    let mut trimmed = combined.trim_end_matches('0').to_string();
737    if trimmed.ends_with('.') {
738        trimmed.pop();
739    }
740
741    // If we stripped everything (e.g. amount 0), return "0"
742    if trimmed.is_empty() {
743        "0".to_string()
744    } else {
745        trimmed
746    }
747}
748
749/// Count operations in a transaction envelope from XDR base64 string
750///
751/// Parses the XDR string, extracts operations, and returns the count.
752pub fn count_operations_from_xdr(xdr: &str) -> Result<usize, StellarTransactionUtilsError> {
753    let envelope = TransactionEnvelope::from_xdr_base64(xdr, Limits::none()).map_err(|e| {
754        StellarTransactionUtilsError::XdrParseFailed(format!("Failed to parse XDR: {e}"))
755    })?;
756
757    let operations = extract_operations(&envelope).map_err(|e| {
758        StellarTransactionUtilsError::OperationExtractionFailed(format!(
759            "Failed to extract operations: {e}"
760        ))
761    })?;
762
763    Ok(operations.len())
764}
765
766/// Parse transaction and count operations
767///
768/// Supports both XDR (base64 string) and operations array formats
769pub fn parse_transaction_and_count_operations(
770    transaction_json: &serde_json::Value,
771) -> Result<usize, StellarTransactionUtilsError> {
772    // Try to parse as XDR string first
773    if let Some(xdr_str) = transaction_json.as_str() {
774        let envelope =
775            TransactionEnvelope::from_xdr_base64(xdr_str, Limits::none()).map_err(|e| {
776                StellarTransactionUtilsError::XdrParseFailed(format!("Failed to parse XDR: {e}"))
777            })?;
778
779        let operations = extract_operations(&envelope).map_err(|e| {
780            StellarTransactionUtilsError::OperationExtractionFailed(format!(
781                "Failed to extract operations: {e}"
782            ))
783        })?;
784
785        return Ok(operations.len());
786    }
787
788    // Try to parse as operations array
789    if let Some(ops_array) = transaction_json.as_array() {
790        return Ok(ops_array.len());
791    }
792
793    // Try to parse as object with operations field
794    if let Some(obj) = transaction_json.as_object() {
795        if let Some(ops) = obj.get("operations") {
796            if let Some(ops_array) = ops.as_array() {
797                return Ok(ops_array.len());
798            }
799        }
800        if let Some(xdr_str) = obj.get("transaction_xdr").and_then(|v| v.as_str()) {
801            let envelope =
802                TransactionEnvelope::from_xdr_base64(xdr_str, Limits::none()).map_err(|e| {
803                    StellarTransactionUtilsError::XdrParseFailed(format!(
804                        "Failed to parse XDR: {e}"
805                    ))
806                })?;
807
808            let operations = extract_operations(&envelope).map_err(|e| {
809                StellarTransactionUtilsError::OperationExtractionFailed(format!(
810                    "Failed to extract operations: {e}"
811                ))
812            })?;
813
814            return Ok(operations.len());
815        }
816    }
817
818    Err(StellarTransactionUtilsError::InvalidTransactionFormat(
819        "Transaction must be either XDR string or operations array".to_string(),
820    ))
821}
822
823/// Fee quote structure containing fee estimates in both tokens and stroops
824#[derive(Debug)]
825pub struct FeeQuote {
826    pub fee_in_token: u64,
827    pub fee_in_token_ui: String,
828    pub fee_in_stroops: u64,
829    pub conversion_rate: f64,
830}
831
832/// Estimate the base transaction fee in XLM (stroops)
833///
834/// For Stellar, the base fee is typically 100 stroops per operation.
835pub fn estimate_base_fee(num_operations: usize) -> u64 {
836    (num_operations.max(1) as u64) * STELLAR_DEFAULT_TRANSACTION_FEE as u64
837}
838
839/// Estimate transaction fee in XLM (stroops) based on envelope content
840///
841/// This function intelligently estimates fees by:
842/// 1. Checking if the transaction needs simulation (contains Soroban operations)
843/// 2. If simulation is needed, performs simulation and uses `min_resource_fee` from the response
844/// 3. If simulation is not needed, counts operations and uses `estimate_base_fee`
845///
846/// # Arguments
847/// * `envelope` - The transaction envelope to estimate fees for
848/// * `provider` - Stellar provider for simulation (required if simulation is needed)
849/// * `operations_override` - Optional override for operations count (useful when operations will be added, e.g., +1 for fee payment)
850///
851/// # Returns
852/// Estimated fee in stroops (XLM)
853pub async fn estimate_fee<P>(
854    envelope: &TransactionEnvelope,
855    provider: &P,
856    operations_override: Option<usize>,
857) -> Result<u64, StellarTransactionUtilsError>
858where
859    P: StellarProviderTrait + Send + Sync,
860{
861    // Check if simulation is needed
862    let needs_sim = xdr_needs_simulation(envelope).map_err(|e| {
863        StellarTransactionUtilsError::SimulationCheckFailed(format!(
864            "Failed to check if simulation is needed: {e}"
865        ))
866    })?;
867
868    if needs_sim {
869        debug!("Transaction contains Soroban operations, simulating to get accurate fee");
870
871        // For simulation, we simulate the envelope as-is
872        let simulation_result = provider
873            .simulate_transaction_envelope(envelope)
874            .await
875            .map_err(|e| {
876                StellarTransactionUtilsError::SimulationFailed(format!(
877                    "Failed to simulate transaction: {e}"
878                ))
879            })?;
880
881        // Check simulation success
882        if simulation_result.results.is_empty() {
883            return Err(StellarTransactionUtilsError::SimulationNoResults);
884        }
885
886        // Use min_resource_fee from simulation (this includes all fees for Soroban operations)
887        // If operations_override is provided, we add the base fee for additional operations
888        let resource_fee = simulation_result.min_resource_fee as u64;
889        let inclusion_fee = STELLAR_DEFAULT_TRANSACTION_FEE as u64;
890        let required_fee = inclusion_fee + resource_fee;
891
892        debug!("Simulation returned fee: {} stroops", required_fee);
893        Ok(required_fee)
894    } else {
895        // No simulation needed, count operations and estimate base fee
896        let num_operations = if let Some(override_count) = operations_override {
897            override_count
898        } else {
899            let operations = extract_operations(envelope).map_err(|e| {
900                StellarTransactionUtilsError::OperationExtractionFailed(format!(
901                    "Failed to extract operations: {e}"
902                ))
903            })?;
904            operations.len()
905        };
906
907        let fee = estimate_base_fee(num_operations);
908        debug!(
909            "No simulation needed, estimated fee from {} operations: {} stroops",
910            num_operations, fee
911        );
912        Ok(fee)
913    }
914}
915
916/// Convert XLM fee to token amount using DEX service
917///
918/// This function converts an XLM fee (in stroops) to the equivalent amount in the requested token
919/// using the DEX service. For native XLM, no conversion is needed.
920/// Optionally applies a fee margin percentage to the XLM fee before conversion.
921///
922/// # Arguments
923/// * `dex_service` - DEX service for token conversion quotes
924/// * `policy` - Stellar relayer policy for slippage and token decimals
925/// * `xlm_fee` - Fee amount in XLM stroops (already estimated)
926/// * `fee_token` - Token identifier (e.g., "native" or "USDC:GA5Z...")
927///
928/// # Returns
929/// A tuple containing:
930/// * `FeeQuote` - Fee quote with amounts in both token and XLM
931/// * `u64` - Buffered XLM fee (with margin applied if specified)
932pub async fn convert_xlm_fee_to_token<D>(
933    dex_service: &D,
934    policy: &RelayerStellarPolicy,
935    xlm_fee: u64,
936    fee_token: &str,
937) -> Result<FeeQuote, StellarTransactionUtilsError>
938where
939    D: StellarDexServiceTrait + Send + Sync,
940{
941    // Handle native XLM - no conversion needed
942    if fee_token == "native" || fee_token.is_empty() {
943        debug!("Converting XLM fee to native XLM: {}", xlm_fee);
944        let buffered_fee = if let Some(margin) = policy.fee_margin_percentage {
945            (xlm_fee as f64 * (1.0 + margin as f64 / 100.0)) as u64
946        } else {
947            xlm_fee
948        };
949
950        return Ok(FeeQuote {
951            fee_in_token: buffered_fee,
952            fee_in_token_ui: amount_to_ui_amount(buffered_fee, 7),
953            fee_in_stroops: buffered_fee,
954            conversion_rate: 1.0,
955        });
956    }
957
958    debug!("Converting XLM fee to token: {}", fee_token);
959
960    // Apply fee margin if specified in policy
961    let buffered_xlm_fee = if let Some(margin) = policy.fee_margin_percentage {
962        (xlm_fee as f64 * (1.0 + margin as f64 / 100.0)) as u64
963    } else {
964        xlm_fee
965    };
966
967    // Get slippage from policy or use default
968    let slippage = policy
969        .get_allowed_token_entry(fee_token)
970        .and_then(|token| {
971            token
972                .swap_config
973                .as_ref()
974                .and_then(|config| config.slippage_percentage)
975        })
976        .or(policy.slippage_percentage)
977        .unwrap_or(DEFAULT_CONVERSION_SLIPPAGE_PERCENTAGE);
978
979    // Get quote from DEX service
980    // Get token decimals from policy or default to 7
981    let token_decimals = policy.get_allowed_token_decimals(fee_token);
982    let quote = dex_service
983        .get_xlm_to_token_quote(fee_token, buffered_xlm_fee, slippage, token_decimals)
984        .await
985        .map_err(|e| {
986            StellarTransactionUtilsError::DexQuoteFailed(format!("Failed to get quote: {e}"))
987        })?;
988
989    debug!(
990        "Quote from DEX: input={} stroops XLM, output={} stroops token, input_asset={}, output_asset={}",
991        quote.in_amount, quote.out_amount, quote.input_asset, quote.output_asset
992    );
993
994    // Calculate conversion rate
995    let conversion_rate = if buffered_xlm_fee > 0 {
996        quote.out_amount as f64 / buffered_xlm_fee as f64
997    } else {
998        0.0
999    };
1000
1001    let fee_quote = FeeQuote {
1002        fee_in_token: quote.out_amount,
1003        fee_in_token_ui: amount_to_ui_amount(quote.out_amount, token_decimals.unwrap_or(7)),
1004        fee_in_stroops: buffered_xlm_fee,
1005        conversion_rate,
1006    };
1007
1008    debug!(
1009        "Final fee quote: fee_in_token={} stroops ({} {}), fee_in_stroops={} stroops XLM, conversion_rate={}",
1010        fee_quote.fee_in_token, fee_quote.fee_in_token_ui, fee_token, fee_quote.fee_in_stroops, fee_quote.conversion_rate
1011    );
1012
1013    Ok(fee_quote)
1014}
1015
1016/// Parse transaction envelope from JSON value
1017pub fn parse_transaction_envelope(
1018    transaction_json: &serde_json::Value,
1019) -> Result<TransactionEnvelope, StellarTransactionUtilsError> {
1020    // Try to parse as XDR string first
1021    if let Some(xdr_str) = transaction_json.as_str() {
1022        return TransactionEnvelope::from_xdr_base64(xdr_str, Limits::none()).map_err(|e| {
1023            StellarTransactionUtilsError::XdrParseFailed(format!("Failed to parse XDR: {e}"))
1024        });
1025    }
1026
1027    // Try to parse as object with transaction_xdr field
1028    if let Some(obj) = transaction_json.as_object() {
1029        if let Some(xdr_str) = obj.get("transaction_xdr").and_then(|v| v.as_str()) {
1030            return TransactionEnvelope::from_xdr_base64(xdr_str, Limits::none()).map_err(|e| {
1031                StellarTransactionUtilsError::XdrParseFailed(format!("Failed to parse XDR: {e}"))
1032            });
1033        }
1034    }
1035
1036    Err(StellarTransactionUtilsError::InvalidTransactionFormat(
1037        "Transaction must be XDR string or object with transaction_xdr field".to_string(),
1038    ))
1039}
1040
1041/// Create fee payment operation
1042pub fn create_fee_payment_operation(
1043    destination: &str,
1044    asset_id: &str,
1045    amount: i64,
1046) -> Result<OperationSpec, StellarTransactionUtilsError> {
1047    // Parse asset identifier
1048    let asset = if asset_id == "native" || asset_id.is_empty() {
1049        AssetSpec::Native
1050    } else {
1051        // Parse "CODE:ISSUER" format
1052        if let Some(colon_pos) = asset_id.find(':') {
1053            let code = asset_id[..colon_pos].to_string();
1054            let issuer = asset_id[colon_pos + 1..].to_string();
1055
1056            // Determine if it's Credit4 or Credit12 based on code length
1057            if code.len() <= 4 {
1058                AssetSpec::Credit4 { code, issuer }
1059            } else if code.len() <= 12 {
1060                AssetSpec::Credit12 { code, issuer }
1061            } else {
1062                return Err(StellarTransactionUtilsError::AssetCodeTooLong(
1063                    12, // Stellar max asset code length
1064                    code,
1065                ));
1066            }
1067        } else {
1068            return Err(StellarTransactionUtilsError::InvalidAssetFormat(format!(
1069                "Invalid asset identifier format. Expected 'native' or 'CODE:ISSUER', got: {asset_id}"
1070            )));
1071        }
1072    };
1073
1074    Ok(OperationSpec::Payment {
1075        destination: destination.to_string(),
1076        amount,
1077        asset,
1078    })
1079}
1080
1081/// Add operation to transaction envelope
1082pub fn add_operation_to_envelope(
1083    envelope: &mut TransactionEnvelope,
1084    operation: Operation,
1085) -> Result<(), StellarTransactionUtilsError> {
1086    match envelope {
1087        TransactionEnvelope::TxV0(ref mut e) => {
1088            // Extract existing operations
1089            let mut ops: Vec<Operation> = e.tx.operations.iter().cloned().collect();
1090            ops.push(operation);
1091
1092            // Convert back to VecM
1093            let operations: VecM<Operation, 100> = ops.try_into().map_err(|_| {
1094                StellarTransactionUtilsError::TooManyOperations(STELLAR_MAX_OPERATIONS)
1095            })?;
1096
1097            e.tx.operations = operations;
1098
1099            // Update fee to account for new operation
1100            e.tx.fee = (e.tx.operations.len() as u32) * STELLAR_DEFAULT_TRANSACTION_FEE;
1101            // 100 stroops per operation
1102        }
1103        TransactionEnvelope::Tx(ref mut e) => {
1104            // Extract existing operations
1105            let mut ops: Vec<Operation> = e.tx.operations.iter().cloned().collect();
1106            ops.push(operation);
1107
1108            // Convert back to VecM
1109            let operations: VecM<Operation, 100> = ops.try_into().map_err(|_| {
1110                StellarTransactionUtilsError::TooManyOperations(STELLAR_MAX_OPERATIONS)
1111            })?;
1112
1113            e.tx.operations = operations;
1114
1115            // Update fee to account for new operation
1116            e.tx.fee = (e.tx.operations.len() as u32) * STELLAR_DEFAULT_TRANSACTION_FEE;
1117            // 100 stroops per operation
1118        }
1119        TransactionEnvelope::TxFeeBump(_) => {
1120            return Err(StellarTransactionUtilsError::CannotModifyFeeBump);
1121        }
1122    }
1123    Ok(())
1124}
1125
1126/// Extract time bounds from a transaction envelope
1127///
1128/// Handles both regular transactions (TxV0, Tx) and fee-bump transactions
1129/// (extracts from inner transaction).
1130///
1131/// # Arguments
1132/// * `envelope` - The transaction envelope to extract time bounds from
1133///
1134/// # Returns
1135/// Some(TimeBounds) if present, None otherwise
1136pub fn extract_time_bounds(envelope: &TransactionEnvelope) -> Option<&TimeBounds> {
1137    match envelope {
1138        TransactionEnvelope::TxV0(e) => e.tx.time_bounds.as_ref(),
1139        TransactionEnvelope::Tx(e) => match &e.tx.cond {
1140            Preconditions::Time(tb) => Some(tb),
1141            Preconditions::V2(v2) => v2.time_bounds.as_ref(),
1142            Preconditions::None => None,
1143        },
1144        TransactionEnvelope::TxFeeBump(fb) => {
1145            // Extract from inner transaction
1146            match &fb.tx.inner_tx {
1147                soroban_rs::xdr::FeeBumpTransactionInnerTx::Tx(inner_tx) => {
1148                    match &inner_tx.tx.cond {
1149                        Preconditions::Time(tb) => Some(tb),
1150                        Preconditions::V2(v2) => v2.time_bounds.as_ref(),
1151                        Preconditions::None => None,
1152                    }
1153                }
1154            }
1155        }
1156    }
1157}
1158
1159/// Set time bounds on transaction envelope
1160pub fn set_time_bounds(
1161    envelope: &mut TransactionEnvelope,
1162    valid_until: DateTime<Utc>,
1163) -> Result<(), StellarTransactionUtilsError> {
1164    let max_time = valid_until.timestamp() as u64;
1165    let time_bounds = TimeBounds {
1166        min_time: TimePoint(0),
1167        max_time: TimePoint(max_time),
1168    };
1169
1170    match envelope {
1171        TransactionEnvelope::TxV0(ref mut e) => {
1172            e.tx.time_bounds = Some(time_bounds);
1173        }
1174        TransactionEnvelope::Tx(ref mut e) => {
1175            e.tx.cond = Preconditions::Time(time_bounds);
1176        }
1177        TransactionEnvelope::TxFeeBump(_) => {
1178            return Err(StellarTransactionUtilsError::CannotSetTimeBoundsOnFeeBump);
1179        }
1180    }
1181    Ok(())
1182}
1183
1184/// Extract asset identifier from CreditAlphanum4
1185fn credit_alphanum4_to_asset_id(
1186    alpha4: &AlphaNum4,
1187) -> Result<String, StellarTransactionUtilsError> {
1188    // Extract code (trim null bytes)
1189    let code_bytes = alpha4.asset_code.0;
1190    let code_len = code_bytes.iter().position(|&b| b == 0).unwrap_or(4);
1191    let code = String::from_utf8(code_bytes[..code_len].to_vec()).map_err(|e| {
1192        StellarTransactionUtilsError::InvalidAssetFormat(format!("Invalid asset code: {e}"))
1193    })?;
1194
1195    // Extract issuer
1196    let issuer = match &alpha4.issuer.0 {
1197        XdrPublicKey::PublicKeyTypeEd25519(uint256) => {
1198            let bytes: [u8; 32] = uint256.0;
1199            let pk = PublicKey(bytes);
1200            pk.to_string()
1201        }
1202    };
1203
1204    Ok(format!("{code}:{issuer}"))
1205}
1206
1207/// Extract asset identifier from CreditAlphanum12
1208fn credit_alphanum12_to_asset_id(
1209    alpha12: &AlphaNum12,
1210) -> Result<String, StellarTransactionUtilsError> {
1211    // Extract code (trim null bytes)
1212    let code_bytes = alpha12.asset_code.0;
1213    let code_len = code_bytes.iter().position(|&b| b == 0).unwrap_or(12);
1214    let code = String::from_utf8(code_bytes[..code_len].to_vec()).map_err(|e| {
1215        StellarTransactionUtilsError::InvalidAssetFormat(format!("Invalid asset code: {e}"))
1216    })?;
1217
1218    // Extract issuer
1219    let issuer = match &alpha12.issuer.0 {
1220        XdrPublicKey::PublicKeyTypeEd25519(uint256) => {
1221            let bytes: [u8; 32] = uint256.0;
1222            let pk = PublicKey(bytes);
1223            pk.to_string()
1224        }
1225    };
1226
1227    Ok(format!("{code}:{issuer}"))
1228}
1229
1230/// Convert ChangeTrustAsset XDR to asset identifier string
1231///
1232/// Returns `Some(asset_id)` for CreditAlphanum4 and CreditAlphanum12 assets,
1233/// or `None` for Native or PoolShare (which don't have asset identifiers).
1234///
1235/// # Arguments
1236///
1237/// * `change_trust_asset` - The ChangeTrustAsset to convert
1238///
1239/// # Returns
1240///
1241/// Asset identifier string in "CODE:ISSUER" format, or None for Native/PoolShare
1242pub fn change_trust_asset_to_asset_id(
1243    change_trust_asset: &ChangeTrustAsset,
1244) -> Result<Option<String>, StellarTransactionUtilsError> {
1245    match change_trust_asset {
1246        ChangeTrustAsset::Native | ChangeTrustAsset::PoolShare(_) => Ok(None),
1247        ChangeTrustAsset::CreditAlphanum4(alpha4) => {
1248            // Convert to Asset and use the unified function
1249            let asset = Asset::CreditAlphanum4(alpha4.clone());
1250            asset_to_asset_id(&asset).map(Some)
1251        }
1252        ChangeTrustAsset::CreditAlphanum12(alpha12) => {
1253            // Convert to Asset and use the unified function
1254            let asset = Asset::CreditAlphanum12(alpha12.clone());
1255            asset_to_asset_id(&asset).map(Some)
1256        }
1257    }
1258}
1259
1260/// Convert Asset XDR to asset identifier string
1261///
1262/// # Arguments
1263///
1264/// * `asset` - The Asset to convert
1265///
1266/// # Returns
1267///
1268/// Asset identifier string ("native" for Native, or "CODE:ISSUER" for credit assets)
1269pub fn asset_to_asset_id(asset: &Asset) -> Result<String, StellarTransactionUtilsError> {
1270    match asset {
1271        Asset::Native => Ok("native".to_string()),
1272        Asset::CreditAlphanum4(alpha4) => credit_alphanum4_to_asset_id(alpha4),
1273        Asset::CreditAlphanum12(alpha12) => credit_alphanum12_to_asset_id(alpha12),
1274    }
1275}
1276
1277#[cfg(test)]
1278mod tests {
1279    use super::*;
1280    use crate::domain::transaction::stellar::test_helpers::TEST_PK;
1281    use crate::models::AssetSpec;
1282    use crate::models::{AuthSpec, ContractSource, WasmSource};
1283
1284    fn payment_op(destination: &str) -> OperationSpec {
1285        OperationSpec::Payment {
1286            destination: destination.to_string(),
1287            amount: 100,
1288            asset: AssetSpec::Native,
1289        }
1290    }
1291
1292    #[test]
1293    fn returns_false_for_only_payment_ops() {
1294        let ops = vec![payment_op(TEST_PK)];
1295        assert!(!needs_simulation(&ops));
1296    }
1297
1298    #[test]
1299    fn returns_true_for_invoke_contract_ops() {
1300        let ops = vec![OperationSpec::InvokeContract {
1301            contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
1302                .to_string(),
1303            function_name: "transfer".to_string(),
1304            args: vec![],
1305            auth: None,
1306        }];
1307        assert!(needs_simulation(&ops));
1308    }
1309
1310    #[test]
1311    fn returns_true_for_upload_wasm_ops() {
1312        let ops = vec![OperationSpec::UploadWasm {
1313            wasm: WasmSource::Hex {
1314                hex: "deadbeef".to_string(),
1315            },
1316            auth: None,
1317        }];
1318        assert!(needs_simulation(&ops));
1319    }
1320
1321    #[test]
1322    fn returns_true_for_create_contract_ops() {
1323        let ops = vec![OperationSpec::CreateContract {
1324            source: ContractSource::Address {
1325                address: TEST_PK.to_string(),
1326            },
1327            wasm_hash: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
1328                .to_string(),
1329            salt: None,
1330            constructor_args: None,
1331            auth: None,
1332        }];
1333        assert!(needs_simulation(&ops));
1334    }
1335
1336    #[test]
1337    fn returns_true_for_single_invoke_host_function() {
1338        let ops = vec![OperationSpec::InvokeContract {
1339            contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
1340                .to_string(),
1341            function_name: "transfer".to_string(),
1342            args: vec![],
1343            auth: Some(AuthSpec::SourceAccount),
1344        }];
1345        assert!(needs_simulation(&ops));
1346    }
1347
1348    #[test]
1349    fn returns_false_for_multiple_payment_ops() {
1350        let ops = vec![payment_op(TEST_PK), payment_op(TEST_PK)];
1351        assert!(!needs_simulation(&ops));
1352    }
1353
1354    mod next_sequence_u64_tests {
1355        use super::*;
1356
1357        #[test]
1358        fn test_increment() {
1359            assert_eq!(next_sequence_u64(0).unwrap(), 1);
1360
1361            assert_eq!(next_sequence_u64(12345).unwrap(), 12346);
1362        }
1363
1364        #[test]
1365        fn test_error_path_overflow_i64_max() {
1366            let result = next_sequence_u64(i64::MAX);
1367            assert!(result.is_err());
1368            match result.unwrap_err() {
1369                RelayerError::ProviderError(msg) => assert_eq!(msg, "sequence overflow"),
1370                _ => panic!("Unexpected error type"),
1371            }
1372        }
1373    }
1374
1375    mod i64_from_u64_tests {
1376        use super::*;
1377
1378        #[test]
1379        fn test_happy_path_conversion() {
1380            assert_eq!(i64_from_u64(0).unwrap(), 0);
1381            assert_eq!(i64_from_u64(12345).unwrap(), 12345);
1382            assert_eq!(i64_from_u64(i64::MAX as u64).unwrap(), i64::MAX);
1383        }
1384
1385        #[test]
1386        fn test_error_path_overflow_u64_max() {
1387            let result = i64_from_u64(u64::MAX);
1388            assert!(result.is_err());
1389            match result.unwrap_err() {
1390                RelayerError::ProviderError(msg) => assert_eq!(msg, "u64→i64 overflow"),
1391                _ => panic!("Unexpected error type"),
1392            }
1393        }
1394
1395        #[test]
1396        fn test_edge_case_just_above_i64_max() {
1397            // Smallest u64 value that will overflow i64
1398            let value = (i64::MAX as u64) + 1;
1399            let result = i64_from_u64(value);
1400            assert!(result.is_err());
1401            match result.unwrap_err() {
1402                RelayerError::ProviderError(msg) => assert_eq!(msg, "u64→i64 overflow"),
1403                _ => panic!("Unexpected error type"),
1404            }
1405        }
1406    }
1407
1408    mod is_bad_sequence_error_tests {
1409        use super::*;
1410
1411        #[test]
1412        fn test_detects_txbadseq() {
1413            assert!(is_bad_sequence_error(
1414                "Failed to send transaction: transaction submission failed: TxBadSeq"
1415            ));
1416            assert!(is_bad_sequence_error("Error: TxBadSeq"));
1417            assert!(is_bad_sequence_error("txbadseq"));
1418            assert!(is_bad_sequence_error("TXBADSEQ"));
1419        }
1420
1421        #[test]
1422        fn test_returns_false_for_other_errors() {
1423            assert!(!is_bad_sequence_error("network timeout"));
1424            assert!(!is_bad_sequence_error("insufficient balance"));
1425            assert!(!is_bad_sequence_error("tx_insufficient_fee"));
1426            assert!(!is_bad_sequence_error("bad_auth"));
1427            assert!(!is_bad_sequence_error(""));
1428        }
1429    }
1430
1431    mod status_check_utils_tests {
1432        use crate::models::{
1433            NetworkTransactionData, StellarTransactionData, TransactionError, TransactionInput,
1434            TransactionRepoModel,
1435        };
1436        use crate::utils::mocks::mockutils::create_mock_transaction;
1437        use chrono::{Duration, Utc};
1438
1439        /// Helper to create a test transaction with a specific created_at timestamp
1440        fn create_test_tx_with_age(seconds_ago: i64) -> TransactionRepoModel {
1441            let created_at = (Utc::now() - Duration::seconds(seconds_ago)).to_rfc3339();
1442            let mut tx = create_mock_transaction();
1443            tx.id = format!("test-tx-{}", seconds_ago);
1444            tx.created_at = created_at;
1445            tx.network_data = NetworkTransactionData::Stellar(StellarTransactionData {
1446                source_account: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
1447                    .to_string(),
1448                fee: None,
1449                sequence_number: None,
1450                memo: None,
1451                valid_until: None,
1452                network_passphrase: "Test SDF Network ; September 2015".to_string(),
1453                signatures: vec![],
1454                hash: Some("test-hash-12345".to_string()),
1455                simulation_transaction_data: None,
1456                transaction_input: TransactionInput::Operations(vec![]),
1457                signed_envelope_xdr: None,
1458                transaction_result_xdr: None,
1459            });
1460            tx
1461        }
1462
1463        mod get_age_since_created_tests {
1464            use crate::domain::transaction::util::get_age_since_created;
1465
1466            use super::*;
1467
1468            #[test]
1469            fn test_returns_correct_age_for_recent_transaction() {
1470                let tx = create_test_tx_with_age(30); // 30 seconds ago
1471                let age = get_age_since_created(&tx).unwrap();
1472
1473                // Allow for small timing differences (within 1 second)
1474                assert!(age.num_seconds() >= 29 && age.num_seconds() <= 31);
1475            }
1476
1477            #[test]
1478            fn test_returns_correct_age_for_old_transaction() {
1479                let tx = create_test_tx_with_age(3600); // 1 hour ago
1480                let age = get_age_since_created(&tx).unwrap();
1481
1482                // Allow for small timing differences
1483                assert!(age.num_seconds() >= 3599 && age.num_seconds() <= 3601);
1484            }
1485
1486            #[test]
1487            fn test_returns_zero_age_for_just_created_transaction() {
1488                let tx = create_test_tx_with_age(0); // Just now
1489                let age = get_age_since_created(&tx).unwrap();
1490
1491                // Should be very close to 0
1492                assert!(age.num_seconds() >= 0 && age.num_seconds() <= 1);
1493            }
1494
1495            #[test]
1496            fn test_handles_negative_age_gracefully() {
1497                // Create transaction with future timestamp (clock skew scenario)
1498                let created_at = (Utc::now() + Duration::seconds(10)).to_rfc3339();
1499                let mut tx = create_mock_transaction();
1500                tx.created_at = created_at;
1501
1502                let age = get_age_since_created(&tx).unwrap();
1503
1504                // Age should be negative
1505                assert!(age.num_seconds() < 0);
1506            }
1507
1508            #[test]
1509            fn test_returns_error_for_invalid_created_at() {
1510                let mut tx = create_mock_transaction();
1511                tx.created_at = "invalid-timestamp".to_string();
1512
1513                let result = get_age_since_created(&tx);
1514                assert!(result.is_err());
1515
1516                match result.unwrap_err() {
1517                    TransactionError::UnexpectedError(msg) => {
1518                        assert!(msg.contains("Invalid created_at timestamp"));
1519                    }
1520                    _ => panic!("Expected UnexpectedError"),
1521                }
1522            }
1523
1524            #[test]
1525            fn test_returns_error_for_empty_created_at() {
1526                let mut tx = create_mock_transaction();
1527                tx.created_at = "".to_string();
1528
1529                let result = get_age_since_created(&tx);
1530                assert!(result.is_err());
1531            }
1532
1533            #[test]
1534            fn test_handles_various_rfc3339_formats() {
1535                let mut tx = create_mock_transaction();
1536
1537                // Test with UTC timezone
1538                tx.created_at = "2025-01-01T12:00:00Z".to_string();
1539                assert!(get_age_since_created(&tx).is_ok());
1540
1541                // Test with offset timezone
1542                tx.created_at = "2025-01-01T12:00:00+00:00".to_string();
1543                assert!(get_age_since_created(&tx).is_ok());
1544
1545                // Test with milliseconds
1546                tx.created_at = "2025-01-01T12:00:00.123Z".to_string();
1547                assert!(get_age_since_created(&tx).is_ok());
1548            }
1549        }
1550    }
1551
1552    #[test]
1553    fn test_create_signature_payload_functions() {
1554        use soroban_rs::xdr::{
1555            Hash, SequenceNumber, TransactionEnvelope, TransactionV0, TransactionV0Envelope,
1556            Uint256,
1557        };
1558
1559        // Test create_transaction_signature_payload
1560        let transaction = soroban_rs::xdr::Transaction {
1561            source_account: soroban_rs::xdr::MuxedAccount::Ed25519(Uint256([1u8; 32])),
1562            fee: 100,
1563            seq_num: SequenceNumber(123),
1564            cond: soroban_rs::xdr::Preconditions::None,
1565            memo: soroban_rs::xdr::Memo::None,
1566            operations: vec![].try_into().unwrap(),
1567            ext: soroban_rs::xdr::TransactionExt::V0,
1568        };
1569        let network_id = Hash([2u8; 32]);
1570
1571        let payload = create_transaction_signature_payload(&transaction, &network_id);
1572        assert_eq!(payload.network_id, network_id);
1573
1574        // Test create_signature_payload with V0 envelope
1575        let v0_tx = TransactionV0 {
1576            source_account_ed25519: Uint256([1u8; 32]),
1577            fee: 100,
1578            seq_num: SequenceNumber(123),
1579            time_bounds: None,
1580            memo: soroban_rs::xdr::Memo::None,
1581            operations: vec![].try_into().unwrap(),
1582            ext: soroban_rs::xdr::TransactionV0Ext::V0,
1583        };
1584        let v0_envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
1585            tx: v0_tx,
1586            signatures: vec![].try_into().unwrap(),
1587        });
1588
1589        let v0_payload = create_signature_payload(&v0_envelope, &network_id).unwrap();
1590        assert_eq!(v0_payload.network_id, network_id);
1591    }
1592
1593    mod convert_v0_to_v1_transaction_tests {
1594        use super::*;
1595        use soroban_rs::xdr::{SequenceNumber, TransactionV0, Uint256};
1596
1597        #[test]
1598        fn test_convert_v0_to_v1_transaction() {
1599            // Create a simple V0 transaction
1600            let v0_tx = TransactionV0 {
1601                source_account_ed25519: Uint256([1u8; 32]),
1602                fee: 100,
1603                seq_num: SequenceNumber(123),
1604                time_bounds: None,
1605                memo: soroban_rs::xdr::Memo::None,
1606                operations: vec![].try_into().unwrap(),
1607                ext: soroban_rs::xdr::TransactionV0Ext::V0,
1608            };
1609
1610            // Convert to V1
1611            let v1_tx = convert_v0_to_v1_transaction(&v0_tx);
1612
1613            // Check that conversion worked correctly
1614            assert_eq!(v1_tx.fee, v0_tx.fee);
1615            assert_eq!(v1_tx.seq_num, v0_tx.seq_num);
1616            assert_eq!(v1_tx.memo, v0_tx.memo);
1617            assert_eq!(v1_tx.operations, v0_tx.operations);
1618            assert!(matches!(v1_tx.ext, soroban_rs::xdr::TransactionExt::V0));
1619            assert!(matches!(v1_tx.cond, soroban_rs::xdr::Preconditions::None));
1620
1621            // Check source account conversion
1622            match v1_tx.source_account {
1623                soroban_rs::xdr::MuxedAccount::Ed25519(addr) => {
1624                    assert_eq!(addr, v0_tx.source_account_ed25519);
1625                }
1626                _ => panic!("Expected Ed25519 muxed account"),
1627            }
1628        }
1629
1630        #[test]
1631        fn test_convert_v0_to_v1_transaction_with_time_bounds() {
1632            // Create a V0 transaction with time bounds
1633            let time_bounds = soroban_rs::xdr::TimeBounds {
1634                min_time: soroban_rs::xdr::TimePoint(100),
1635                max_time: soroban_rs::xdr::TimePoint(200),
1636            };
1637
1638            let v0_tx = TransactionV0 {
1639                source_account_ed25519: Uint256([2u8; 32]),
1640                fee: 200,
1641                seq_num: SequenceNumber(456),
1642                time_bounds: Some(time_bounds.clone()),
1643                memo: soroban_rs::xdr::Memo::Text("test".try_into().unwrap()),
1644                operations: vec![].try_into().unwrap(),
1645                ext: soroban_rs::xdr::TransactionV0Ext::V0,
1646            };
1647
1648            // Convert to V1
1649            let v1_tx = convert_v0_to_v1_transaction(&v0_tx);
1650
1651            // Check that time bounds were correctly converted to preconditions
1652            match v1_tx.cond {
1653                soroban_rs::xdr::Preconditions::Time(tb) => {
1654                    assert_eq!(tb, time_bounds);
1655                }
1656                _ => panic!("Expected Time preconditions"),
1657            }
1658        }
1659    }
1660}
1661
1662#[cfg(test)]
1663mod parse_contract_address_tests {
1664    use super::*;
1665    use crate::domain::transaction::stellar::test_helpers::{
1666        TEST_CONTRACT, TEST_PK as TEST_ACCOUNT,
1667    };
1668
1669    #[test]
1670    fn test_parse_valid_contract_address() {
1671        let result = parse_contract_address(TEST_CONTRACT);
1672        assert!(result.is_ok());
1673
1674        let hash = result.unwrap();
1675        assert_eq!(hash.0.len(), 32);
1676    }
1677
1678    #[test]
1679    fn test_parse_invalid_contract_address() {
1680        let result = parse_contract_address("INVALID_CONTRACT");
1681        assert!(result.is_err());
1682
1683        match result.unwrap_err() {
1684            StellarTransactionUtilsError::InvalidContractAddress(addr, _) => {
1685                assert_eq!(addr, "INVALID_CONTRACT");
1686            }
1687            _ => panic!("Expected InvalidContractAddress error"),
1688        }
1689    }
1690
1691    #[test]
1692    fn test_parse_contract_address_wrong_prefix() {
1693        // Try with an account address instead of contract
1694        let result = parse_contract_address(TEST_ACCOUNT);
1695        assert!(result.is_err());
1696    }
1697
1698    #[test]
1699    fn test_parse_empty_contract_address() {
1700        let result = parse_contract_address("");
1701        assert!(result.is_err());
1702    }
1703}
1704
1705// ============================================================================
1706// Update Envelope Sequence and Envelope Fee Tests
1707// ============================================================================
1708
1709#[cfg(test)]
1710mod update_envelope_sequence_tests {
1711    use super::*;
1712    use soroban_rs::xdr::{
1713        FeeBumpTransaction, FeeBumpTransactionEnvelope, FeeBumpTransactionExt,
1714        FeeBumpTransactionInnerTx, Memo, MuxedAccount, Preconditions, SequenceNumber, Transaction,
1715        TransactionExt, TransactionV0, TransactionV0Envelope, TransactionV0Ext,
1716        TransactionV1Envelope, Uint256, VecM,
1717    };
1718
1719    fn create_minimal_v1_envelope() -> TransactionEnvelope {
1720        let tx = Transaction {
1721            source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
1722            fee: 100,
1723            seq_num: SequenceNumber(0),
1724            cond: Preconditions::None,
1725            memo: Memo::None,
1726            operations: VecM::default(),
1727            ext: TransactionExt::V0,
1728        };
1729        TransactionEnvelope::Tx(TransactionV1Envelope {
1730            tx,
1731            signatures: VecM::default(),
1732        })
1733    }
1734
1735    fn create_v0_envelope() -> TransactionEnvelope {
1736        let tx = TransactionV0 {
1737            source_account_ed25519: Uint256([0u8; 32]),
1738            fee: 100,
1739            seq_num: SequenceNumber(0),
1740            time_bounds: None,
1741            memo: Memo::None,
1742            operations: VecM::default(),
1743            ext: TransactionV0Ext::V0,
1744        };
1745        TransactionEnvelope::TxV0(TransactionV0Envelope {
1746            tx,
1747            signatures: VecM::default(),
1748        })
1749    }
1750
1751    fn create_fee_bump_envelope() -> TransactionEnvelope {
1752        let inner_tx = Transaction {
1753            source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
1754            fee: 100,
1755            seq_num: SequenceNumber(0),
1756            cond: Preconditions::None,
1757            memo: Memo::None,
1758            operations: VecM::default(),
1759            ext: TransactionExt::V0,
1760        };
1761        let inner_envelope = TransactionV1Envelope {
1762            tx: inner_tx,
1763            signatures: VecM::default(),
1764        };
1765        let fee_bump_tx = FeeBumpTransaction {
1766            fee_source: MuxedAccount::Ed25519(Uint256([1u8; 32])),
1767            fee: 200,
1768            inner_tx: FeeBumpTransactionInnerTx::Tx(inner_envelope),
1769            ext: FeeBumpTransactionExt::V0,
1770        };
1771        TransactionEnvelope::TxFeeBump(FeeBumpTransactionEnvelope {
1772            tx: fee_bump_tx,
1773            signatures: VecM::default(),
1774        })
1775    }
1776
1777    #[test]
1778    fn test_update_envelope_sequence() {
1779        let mut envelope = create_minimal_v1_envelope();
1780        update_envelope_sequence(&mut envelope, 12345).unwrap();
1781        if let TransactionEnvelope::Tx(v1) = &envelope {
1782            assert_eq!(v1.tx.seq_num.0, 12345);
1783        } else {
1784            panic!("Expected Tx envelope");
1785        }
1786    }
1787
1788    #[test]
1789    fn test_update_envelope_sequence_v0_returns_error() {
1790        let mut envelope = create_v0_envelope();
1791        let result = update_envelope_sequence(&mut envelope, 12345);
1792        assert!(result.is_err());
1793        match result.unwrap_err() {
1794            StellarTransactionUtilsError::V0TransactionsNotSupported => {}
1795            _ => panic!("Expected V0TransactionsNotSupported error"),
1796        }
1797    }
1798
1799    #[test]
1800    fn test_update_envelope_sequence_fee_bump_returns_error() {
1801        let mut envelope = create_fee_bump_envelope();
1802        let result = update_envelope_sequence(&mut envelope, 12345);
1803        assert!(result.is_err());
1804        match result.unwrap_err() {
1805            StellarTransactionUtilsError::CannotUpdateSequenceOnFeeBump => {}
1806            _ => panic!("Expected CannotUpdateSequenceOnFeeBump error"),
1807        }
1808    }
1809
1810    #[test]
1811    fn test_update_envelope_sequence_zero() {
1812        let mut envelope = create_minimal_v1_envelope();
1813        update_envelope_sequence(&mut envelope, 0).unwrap();
1814        if let TransactionEnvelope::Tx(v1) = &envelope {
1815            assert_eq!(v1.tx.seq_num.0, 0);
1816        } else {
1817            panic!("Expected Tx envelope");
1818        }
1819    }
1820
1821    #[test]
1822    fn test_update_envelope_sequence_max_value() {
1823        let mut envelope = create_minimal_v1_envelope();
1824        update_envelope_sequence(&mut envelope, i64::MAX).unwrap();
1825        if let TransactionEnvelope::Tx(v1) = &envelope {
1826            assert_eq!(v1.tx.seq_num.0, i64::MAX);
1827        } else {
1828            panic!("Expected Tx envelope");
1829        }
1830    }
1831
1832    #[test]
1833    fn test_envelope_fee_in_stroops_v1() {
1834        let envelope = create_minimal_v1_envelope();
1835        let fee = envelope_fee_in_stroops(&envelope).unwrap();
1836        assert_eq!(fee, 100);
1837    }
1838
1839    #[test]
1840    fn test_envelope_fee_in_stroops_v0_returns_error() {
1841        let envelope = create_v0_envelope();
1842        let result = envelope_fee_in_stroops(&envelope);
1843        assert!(result.is_err());
1844        match result.unwrap_err() {
1845            StellarTransactionUtilsError::InvalidTransactionFormat(msg) => {
1846                assert!(msg.contains("Expected V1"));
1847            }
1848            _ => panic!("Expected InvalidTransactionFormat error"),
1849        }
1850    }
1851
1852    #[test]
1853    fn test_envelope_fee_in_stroops_fee_bump_returns_error() {
1854        let envelope = create_fee_bump_envelope();
1855        let result = envelope_fee_in_stroops(&envelope);
1856        assert!(result.is_err());
1857    }
1858}
1859
1860// ============================================================================
1861// Contract Data Key Tests
1862// ============================================================================
1863
1864#[cfg(test)]
1865mod create_contract_data_key_tests {
1866    use super::*;
1867    use crate::domain::transaction::stellar::test_helpers::TEST_PK as TEST_ACCOUNT;
1868    use stellar_strkey::ed25519::PublicKey;
1869
1870    #[test]
1871    fn test_create_key_without_address() {
1872        let result = create_contract_data_key("Balance", None);
1873        assert!(result.is_ok());
1874
1875        match result.unwrap() {
1876            ScVal::Symbol(sym) => {
1877                assert_eq!(sym.to_string(), "Balance");
1878            }
1879            _ => panic!("Expected Symbol ScVal"),
1880        }
1881    }
1882
1883    #[test]
1884    fn test_create_key_with_address() {
1885        let pk = PublicKey::from_string(TEST_ACCOUNT).unwrap();
1886        let uint256 = Uint256(pk.0);
1887        let account_id = AccountId(soroban_rs::xdr::PublicKey::PublicKeyTypeEd25519(uint256));
1888        let sc_address = ScAddress::Account(account_id);
1889
1890        let result = create_contract_data_key("Balance", Some(sc_address.clone()));
1891        assert!(result.is_ok());
1892
1893        match result.unwrap() {
1894            ScVal::Vec(Some(vec)) => {
1895                assert_eq!(vec.0.len(), 2);
1896                match &vec.0[0] {
1897                    ScVal::Symbol(sym) => assert_eq!(sym.to_string(), "Balance"),
1898                    _ => panic!("Expected Symbol as first element"),
1899                }
1900                match &vec.0[1] {
1901                    ScVal::Address(addr) => assert_eq!(addr, &sc_address),
1902                    _ => panic!("Expected Address as second element"),
1903                }
1904            }
1905            _ => panic!("Expected Vec ScVal"),
1906        }
1907    }
1908
1909    #[test]
1910    fn test_create_key_invalid_symbol() {
1911        // Test with symbol that's too long or has invalid characters
1912        let very_long_symbol = "a".repeat(100);
1913        let result = create_contract_data_key(&very_long_symbol, None);
1914        assert!(result.is_err());
1915
1916        match result.unwrap_err() {
1917            StellarTransactionUtilsError::SymbolCreationFailed(_, _) => {}
1918            _ => panic!("Expected SymbolCreationFailed error"),
1919        }
1920    }
1921
1922    #[test]
1923    fn test_create_key_decimals() {
1924        let result = create_contract_data_key("Decimals", None);
1925        assert!(result.is_ok());
1926    }
1927}
1928
1929// ============================================================================
1930// Extract ScVal from Contract Data Tests
1931// ============================================================================
1932
1933#[cfg(test)]
1934mod extract_scval_from_contract_data_tests {
1935    use super::*;
1936    use soroban_rs::stellar_rpc_client::{GetLedgerEntriesResponse, LedgerEntryResult};
1937    use soroban_rs::xdr::{
1938        ContractDataDurability, ContractDataEntry, ExtensionPoint, Hash, LedgerEntry,
1939        LedgerEntryData, LedgerEntryExt, ScSymbol, ScVal, WriteXdr,
1940    };
1941
1942    #[test]
1943    fn test_extract_scval_success() {
1944        let contract_data = ContractDataEntry {
1945            ext: ExtensionPoint::V0,
1946            contract: ScAddress::Contract(ContractId(Hash([0u8; 32]))),
1947            key: ScVal::Symbol(ScSymbol::try_from("test").unwrap()),
1948            durability: ContractDataDurability::Persistent,
1949            val: ScVal::U32(42),
1950        };
1951
1952        let ledger_entry = LedgerEntry {
1953            last_modified_ledger_seq: 100,
1954            data: LedgerEntryData::ContractData(contract_data),
1955            ext: LedgerEntryExt::V0,
1956        };
1957
1958        let xdr = ledger_entry
1959            .data
1960            .to_xdr_base64(soroban_rs::xdr::Limits::none())
1961            .unwrap();
1962
1963        let response = GetLedgerEntriesResponse {
1964            entries: Some(vec![LedgerEntryResult {
1965                key: "test_key".to_string(),
1966                xdr,
1967                last_modified_ledger: 100,
1968                live_until_ledger_seq_ledger_seq: None,
1969            }]),
1970            latest_ledger: 100,
1971        };
1972
1973        let result = extract_scval_from_contract_data(&response, "test");
1974        assert!(result.is_ok());
1975
1976        match result.unwrap() {
1977            ScVal::U32(val) => assert_eq!(val, 42),
1978            _ => panic!("Expected U32 ScVal"),
1979        }
1980    }
1981
1982    #[test]
1983    fn test_extract_scval_no_entries() {
1984        let response = GetLedgerEntriesResponse {
1985            entries: None,
1986            latest_ledger: 100,
1987        };
1988
1989        let result = extract_scval_from_contract_data(&response, "test");
1990        assert!(result.is_err());
1991
1992        match result.unwrap_err() {
1993            StellarTransactionUtilsError::NoEntriesFound(_) => {}
1994            _ => panic!("Expected NoEntriesFound error"),
1995        }
1996    }
1997
1998    #[test]
1999    fn test_extract_scval_empty_entries() {
2000        let response = GetLedgerEntriesResponse {
2001            entries: Some(vec![]),
2002            latest_ledger: 100,
2003        };
2004
2005        let result = extract_scval_from_contract_data(&response, "test");
2006        assert!(result.is_err());
2007
2008        match result.unwrap_err() {
2009            StellarTransactionUtilsError::EmptyEntries(_) => {}
2010            _ => panic!("Expected EmptyEntries error"),
2011        }
2012    }
2013}
2014
2015// ============================================================================
2016// Extract u32 from ScVal Tests
2017// ============================================================================
2018
2019#[cfg(test)]
2020mod extract_u32_from_scval_tests {
2021    use super::*;
2022    use soroban_rs::xdr::{Int128Parts, ScVal, UInt128Parts};
2023
2024    #[test]
2025    fn test_extract_from_u32() {
2026        let val = ScVal::U32(42);
2027        assert_eq!(extract_u32_from_scval(&val, "test"), Some(42));
2028    }
2029
2030    #[test]
2031    fn test_extract_from_i32_positive() {
2032        let val = ScVal::I32(100);
2033        assert_eq!(extract_u32_from_scval(&val, "test"), Some(100));
2034    }
2035
2036    #[test]
2037    fn test_extract_from_i32_negative() {
2038        let val = ScVal::I32(-1);
2039        assert_eq!(extract_u32_from_scval(&val, "test"), None);
2040    }
2041
2042    #[test]
2043    fn test_extract_from_u64() {
2044        let val = ScVal::U64(1000);
2045        assert_eq!(extract_u32_from_scval(&val, "test"), Some(1000));
2046    }
2047
2048    #[test]
2049    fn test_extract_from_u64_overflow() {
2050        let val = ScVal::U64(u64::MAX);
2051        assert_eq!(extract_u32_from_scval(&val, "test"), None);
2052    }
2053
2054    #[test]
2055    fn test_extract_from_i64_positive() {
2056        let val = ScVal::I64(500);
2057        assert_eq!(extract_u32_from_scval(&val, "test"), Some(500));
2058    }
2059
2060    #[test]
2061    fn test_extract_from_i64_negative() {
2062        let val = ScVal::I64(-500);
2063        assert_eq!(extract_u32_from_scval(&val, "test"), None);
2064    }
2065
2066    #[test]
2067    fn test_extract_from_u128_small() {
2068        let val = ScVal::U128(UInt128Parts { hi: 0, lo: 255 });
2069        assert_eq!(extract_u32_from_scval(&val, "test"), Some(255));
2070    }
2071
2072    #[test]
2073    fn test_extract_from_u128_hi_set() {
2074        let val = ScVal::U128(UInt128Parts { hi: 1, lo: 0 });
2075        assert_eq!(extract_u32_from_scval(&val, "test"), None);
2076    }
2077
2078    #[test]
2079    fn test_extract_from_i128_small() {
2080        let val = ScVal::I128(Int128Parts { hi: 0, lo: 123 });
2081        assert_eq!(extract_u32_from_scval(&val, "test"), Some(123));
2082    }
2083
2084    #[test]
2085    fn test_extract_from_unsupported_type() {
2086        let val = ScVal::Bool(true);
2087        assert_eq!(extract_u32_from_scval(&val, "test"), None);
2088    }
2089}
2090
2091// ============================================================================
2092// Amount to UI Amount Tests
2093// ============================================================================
2094
2095#[cfg(test)]
2096mod amount_to_ui_amount_tests {
2097    use super::*;
2098
2099    #[test]
2100    fn test_zero_decimals() {
2101        assert_eq!(amount_to_ui_amount(100, 0), "100");
2102        assert_eq!(amount_to_ui_amount(0, 0), "0");
2103    }
2104
2105    #[test]
2106    fn test_with_decimals_no_padding() {
2107        assert_eq!(amount_to_ui_amount(1000000, 6), "1");
2108        assert_eq!(amount_to_ui_amount(1500000, 6), "1.5");
2109        assert_eq!(amount_to_ui_amount(1234567, 6), "1.234567");
2110    }
2111
2112    #[test]
2113    fn test_with_decimals_needs_padding() {
2114        assert_eq!(amount_to_ui_amount(1, 6), "0.000001");
2115        assert_eq!(amount_to_ui_amount(100, 6), "0.0001");
2116        assert_eq!(amount_to_ui_amount(1000, 3), "1");
2117    }
2118
2119    #[test]
2120    fn test_trailing_zeros_removed() {
2121        assert_eq!(amount_to_ui_amount(1000000, 6), "1");
2122        assert_eq!(amount_to_ui_amount(1500000, 7), "0.15");
2123        assert_eq!(amount_to_ui_amount(10000000, 7), "1");
2124    }
2125
2126    #[test]
2127    fn test_zero_amount() {
2128        assert_eq!(amount_to_ui_amount(0, 6), "0");
2129        assert_eq!(amount_to_ui_amount(0, 0), "0");
2130    }
2131
2132    #[test]
2133    fn test_xlm_7_decimals() {
2134        assert_eq!(amount_to_ui_amount(10000000, 7), "1");
2135        assert_eq!(amount_to_ui_amount(15000000, 7), "1.5");
2136        assert_eq!(amount_to_ui_amount(100, 7), "0.00001");
2137    }
2138}
2139
2140// // ============================================================================
2141// // Count Operations Tests
2142// // ============================================================================
2143
2144// #[cfg(test)]
2145#[cfg(test)]
2146mod count_operations_tests {
2147    use super::*;
2148    use soroban_rs::xdr::{
2149        Limits, MuxedAccount, Operation, OperationBody, PaymentOp, TransactionV1Envelope, Uint256,
2150        WriteXdr,
2151    };
2152
2153    #[test]
2154    fn test_count_operations_from_xdr() {
2155        use soroban_rs::xdr::{Memo, Preconditions, SequenceNumber, Transaction, TransactionExt};
2156
2157        // Create two payment operations
2158        let payment_op = Operation {
2159            source_account: None,
2160            body: OperationBody::Payment(PaymentOp {
2161                destination: MuxedAccount::Ed25519(Uint256([1u8; 32])),
2162                asset: Asset::Native,
2163                amount: 100,
2164            }),
2165        };
2166
2167        let operations = vec![payment_op.clone(), payment_op].try_into().unwrap();
2168
2169        let tx = Transaction {
2170            source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
2171            fee: 100,
2172            seq_num: SequenceNumber(1),
2173            cond: Preconditions::None,
2174            memo: Memo::None,
2175            operations,
2176            ext: TransactionExt::V0,
2177        };
2178
2179        let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
2180            tx,
2181            signatures: vec![].try_into().unwrap(),
2182        });
2183
2184        let xdr = envelope.to_xdr_base64(Limits::none()).unwrap();
2185        let count = count_operations_from_xdr(&xdr).unwrap();
2186
2187        assert_eq!(count, 2);
2188    }
2189
2190    #[test]
2191    fn test_count_operations_invalid_xdr() {
2192        let result = count_operations_from_xdr("invalid_xdr");
2193        assert!(result.is_err());
2194
2195        match result.unwrap_err() {
2196            StellarTransactionUtilsError::XdrParseFailed(_) => {}
2197            _ => panic!("Expected XdrParseFailed error"),
2198        }
2199    }
2200}
2201
2202// ============================================================================
2203// Estimate Base Fee Tests
2204// ============================================================================
2205
2206#[cfg(test)]
2207mod estimate_base_fee_tests {
2208    use super::*;
2209
2210    #[test]
2211    fn test_single_operation() {
2212        assert_eq!(estimate_base_fee(1), 100);
2213    }
2214
2215    #[test]
2216    fn test_multiple_operations() {
2217        assert_eq!(estimate_base_fee(5), 500);
2218        assert_eq!(estimate_base_fee(10), 1000);
2219    }
2220
2221    #[test]
2222    fn test_zero_operations() {
2223        // Should return fee for at least 1 operation
2224        assert_eq!(estimate_base_fee(0), 100);
2225    }
2226
2227    #[test]
2228    fn test_large_number_of_operations() {
2229        assert_eq!(estimate_base_fee(100), 10000);
2230    }
2231}
2232
2233// ============================================================================
2234// Create Fee Payment Operation Tests
2235// ============================================================================
2236
2237#[cfg(test)]
2238mod create_fee_payment_operation_tests {
2239    use super::*;
2240    use crate::domain::transaction::stellar::test_helpers::TEST_PK as TEST_ACCOUNT;
2241
2242    #[test]
2243    fn test_create_native_payment() {
2244        let result = create_fee_payment_operation(TEST_ACCOUNT, "native", 1000);
2245        assert!(result.is_ok());
2246
2247        match result.unwrap() {
2248            OperationSpec::Payment {
2249                destination,
2250                amount,
2251                asset,
2252            } => {
2253                assert_eq!(destination, TEST_ACCOUNT);
2254                assert_eq!(amount, 1000);
2255                assert!(matches!(asset, AssetSpec::Native));
2256            }
2257            _ => panic!("Expected Payment operation"),
2258        }
2259    }
2260
2261    #[test]
2262    fn test_create_credit4_payment() {
2263        let result = create_fee_payment_operation(
2264            TEST_ACCOUNT,
2265            "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
2266            5000,
2267        );
2268        assert!(result.is_ok());
2269
2270        match result.unwrap() {
2271            OperationSpec::Payment {
2272                destination,
2273                amount,
2274                asset,
2275            } => {
2276                assert_eq!(destination, TEST_ACCOUNT);
2277                assert_eq!(amount, 5000);
2278                match asset {
2279                    AssetSpec::Credit4 { code, issuer } => {
2280                        assert_eq!(code, "USDC");
2281                        assert_eq!(
2282                            issuer,
2283                            "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
2284                        );
2285                    }
2286                    _ => panic!("Expected Credit4 asset"),
2287                }
2288            }
2289            _ => panic!("Expected Payment operation"),
2290        }
2291    }
2292
2293    #[test]
2294    fn test_create_credit12_payment() {
2295        let result = create_fee_payment_operation(
2296            TEST_ACCOUNT,
2297            "LONGASSETNAM:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
2298            2000,
2299        );
2300        assert!(result.is_ok());
2301
2302        match result.unwrap() {
2303            OperationSpec::Payment {
2304                destination,
2305                amount,
2306                asset,
2307            } => {
2308                assert_eq!(destination, TEST_ACCOUNT);
2309                assert_eq!(amount, 2000);
2310                match asset {
2311                    AssetSpec::Credit12 { code, issuer } => {
2312                        assert_eq!(code, "LONGASSETNAM");
2313                        assert_eq!(
2314                            issuer,
2315                            "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
2316                        );
2317                    }
2318                    _ => panic!("Expected Credit12 asset"),
2319                }
2320            }
2321            _ => panic!("Expected Payment operation"),
2322        }
2323    }
2324
2325    #[test]
2326    fn test_create_payment_empty_asset() {
2327        let result = create_fee_payment_operation(TEST_ACCOUNT, "", 1000);
2328        assert!(result.is_ok());
2329
2330        match result.unwrap() {
2331            OperationSpec::Payment { asset, .. } => {
2332                assert!(matches!(asset, AssetSpec::Native));
2333            }
2334            _ => panic!("Expected Payment operation"),
2335        }
2336    }
2337
2338    #[test]
2339    fn test_create_payment_invalid_format() {
2340        let result = create_fee_payment_operation(TEST_ACCOUNT, "INVALID_FORMAT", 1000);
2341        assert!(result.is_err());
2342
2343        match result.unwrap_err() {
2344            StellarTransactionUtilsError::InvalidAssetFormat(_) => {}
2345            _ => panic!("Expected InvalidAssetFormat error"),
2346        }
2347    }
2348
2349    #[test]
2350    fn test_create_payment_asset_code_too_long() {
2351        let result = create_fee_payment_operation(
2352            TEST_ACCOUNT,
2353            "VERYLONGASSETCODE:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
2354            1000,
2355        );
2356        assert!(result.is_err());
2357
2358        match result.unwrap_err() {
2359            StellarTransactionUtilsError::AssetCodeTooLong(max_len, _) => {
2360                assert_eq!(max_len, 12);
2361            }
2362            _ => panic!("Expected AssetCodeTooLong error"),
2363        }
2364    }
2365}
2366
2367#[cfg(test)]
2368mod parse_account_id_tests {
2369    use super::*;
2370    use crate::domain::transaction::stellar::test_helpers::TEST_PK;
2371
2372    #[test]
2373    fn test_parse_account_id_valid() {
2374        let result = parse_account_id(TEST_PK);
2375        assert!(result.is_ok());
2376
2377        let account_id = result.unwrap();
2378        match account_id.0 {
2379            soroban_rs::xdr::PublicKey::PublicKeyTypeEd25519(_) => {}
2380        }
2381    }
2382
2383    #[test]
2384    fn test_parse_account_id_invalid() {
2385        let result = parse_account_id("INVALID_ADDRESS");
2386        assert!(result.is_err());
2387
2388        match result.unwrap_err() {
2389            StellarTransactionUtilsError::InvalidAccountAddress(addr, _) => {
2390                assert_eq!(addr, "INVALID_ADDRESS");
2391            }
2392            _ => panic!("Expected InvalidAccountAddress error"),
2393        }
2394    }
2395
2396    #[test]
2397    fn test_parse_account_id_empty() {
2398        let result = parse_account_id("");
2399        assert!(result.is_err());
2400    }
2401
2402    #[test]
2403    fn test_parse_account_id_wrong_prefix() {
2404        // Contract address instead of account
2405        let result = parse_account_id("CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM");
2406        assert!(result.is_err());
2407    }
2408}
2409
2410#[cfg(test)]
2411mod parse_transaction_and_count_operations_tests {
2412    use super::*;
2413    use crate::domain::transaction::stellar::test_helpers::{
2414        create_native_payment_operation, create_xdr_with_operations, TEST_PK, TEST_PK_2,
2415    };
2416    use serde_json::json;
2417
2418    fn create_test_xdr_with_operations(num_ops: usize) -> String {
2419        let payment_op = create_native_payment_operation(TEST_PK_2, 100);
2420        let operations = vec![payment_op; num_ops];
2421        create_xdr_with_operations(TEST_PK, operations, false)
2422    }
2423
2424    #[test]
2425    fn test_parse_xdr_string() {
2426        let xdr = create_test_xdr_with_operations(2);
2427        let json_value = json!(xdr);
2428
2429        let result = parse_transaction_and_count_operations(&json_value);
2430        assert!(result.is_ok());
2431        assert_eq!(result.unwrap(), 2);
2432    }
2433
2434    #[test]
2435    fn test_parse_operations_array() {
2436        let json_value = json!([
2437            {"type": "payment"},
2438            {"type": "payment"},
2439            {"type": "payment"}
2440        ]);
2441
2442        let result = parse_transaction_and_count_operations(&json_value);
2443        assert!(result.is_ok());
2444        assert_eq!(result.unwrap(), 3);
2445    }
2446
2447    #[test]
2448    fn test_parse_object_with_operations() {
2449        let json_value = json!({
2450            "operations": [
2451                {"type": "payment"},
2452                {"type": "payment"}
2453            ]
2454        });
2455
2456        let result = parse_transaction_and_count_operations(&json_value);
2457        assert!(result.is_ok());
2458        assert_eq!(result.unwrap(), 2);
2459    }
2460
2461    #[test]
2462    fn test_parse_object_with_transaction_xdr() {
2463        let xdr = create_test_xdr_with_operations(3);
2464        let json_value = json!({
2465            "transaction_xdr": xdr
2466        });
2467
2468        let result = parse_transaction_and_count_operations(&json_value);
2469        assert!(result.is_ok());
2470        assert_eq!(result.unwrap(), 3);
2471    }
2472
2473    #[test]
2474    fn test_parse_invalid_xdr() {
2475        let json_value = json!("INVALID_XDR");
2476
2477        let result = parse_transaction_and_count_operations(&json_value);
2478        assert!(result.is_err());
2479
2480        match result.unwrap_err() {
2481            StellarTransactionUtilsError::XdrParseFailed(_) => {}
2482            _ => panic!("Expected XdrParseFailed error"),
2483        }
2484    }
2485
2486    #[test]
2487    fn test_parse_invalid_format() {
2488        let json_value = json!(123);
2489
2490        let result = parse_transaction_and_count_operations(&json_value);
2491        assert!(result.is_err());
2492
2493        match result.unwrap_err() {
2494            StellarTransactionUtilsError::InvalidTransactionFormat(_) => {}
2495            _ => panic!("Expected InvalidTransactionFormat error"),
2496        }
2497    }
2498
2499    #[test]
2500    fn test_parse_empty_operations() {
2501        let json_value = json!([]);
2502
2503        let result = parse_transaction_and_count_operations(&json_value);
2504        assert!(result.is_ok());
2505        assert_eq!(result.unwrap(), 0);
2506    }
2507}
2508
2509#[cfg(test)]
2510mod parse_transaction_envelope_tests {
2511    use super::*;
2512    use crate::domain::transaction::stellar::test_helpers::{
2513        create_unsigned_xdr, TEST_PK, TEST_PK_2,
2514    };
2515    use serde_json::json;
2516
2517    fn create_test_xdr() -> String {
2518        create_unsigned_xdr(TEST_PK, TEST_PK_2)
2519    }
2520
2521    #[test]
2522    fn test_parse_xdr_string() {
2523        let xdr = create_test_xdr();
2524        let json_value = json!(xdr);
2525
2526        let result = parse_transaction_envelope(&json_value);
2527        assert!(result.is_ok());
2528
2529        match result.unwrap() {
2530            TransactionEnvelope::Tx(_) => {}
2531            _ => panic!("Expected Tx envelope"),
2532        }
2533    }
2534
2535    #[test]
2536    fn test_parse_object_with_transaction_xdr() {
2537        let xdr = create_test_xdr();
2538        let json_value = json!({
2539            "transaction_xdr": xdr
2540        });
2541
2542        let result = parse_transaction_envelope(&json_value);
2543        assert!(result.is_ok());
2544
2545        match result.unwrap() {
2546            TransactionEnvelope::Tx(_) => {}
2547            _ => panic!("Expected Tx envelope"),
2548        }
2549    }
2550
2551    #[test]
2552    fn test_parse_invalid_xdr() {
2553        let json_value = json!("INVALID_XDR");
2554
2555        let result = parse_transaction_envelope(&json_value);
2556        assert!(result.is_err());
2557
2558        match result.unwrap_err() {
2559            StellarTransactionUtilsError::XdrParseFailed(_) => {}
2560            _ => panic!("Expected XdrParseFailed error"),
2561        }
2562    }
2563
2564    #[test]
2565    fn test_parse_invalid_format() {
2566        let json_value = json!(123);
2567
2568        let result = parse_transaction_envelope(&json_value);
2569        assert!(result.is_err());
2570
2571        match result.unwrap_err() {
2572            StellarTransactionUtilsError::InvalidTransactionFormat(_) => {}
2573            _ => panic!("Expected InvalidTransactionFormat error"),
2574        }
2575    }
2576
2577    #[test]
2578    fn test_parse_object_without_xdr() {
2579        let json_value = json!({
2580            "some_field": "value"
2581        });
2582
2583        let result = parse_transaction_envelope(&json_value);
2584        assert!(result.is_err());
2585
2586        match result.unwrap_err() {
2587            StellarTransactionUtilsError::InvalidTransactionFormat(_) => {}
2588            _ => panic!("Expected InvalidTransactionFormat error"),
2589        }
2590    }
2591}
2592
2593#[cfg(test)]
2594mod add_operation_to_envelope_tests {
2595    use super::*;
2596    use soroban_rs::xdr::{
2597        Memo, MuxedAccount, Operation, OperationBody, PaymentOp, Preconditions, SequenceNumber,
2598        Transaction, TransactionExt, TransactionV0, TransactionV0Envelope, TransactionV1Envelope,
2599        Uint256,
2600    };
2601
2602    fn create_payment_op() -> Operation {
2603        Operation {
2604            source_account: None,
2605            body: OperationBody::Payment(PaymentOp {
2606                destination: MuxedAccount::Ed25519(Uint256([1u8; 32])),
2607                asset: Asset::Native,
2608                amount: 100,
2609            }),
2610        }
2611    }
2612
2613    #[test]
2614    fn test_add_operation_to_tx_v0() {
2615        let payment_op = create_payment_op();
2616        let operations = vec![payment_op.clone()].try_into().unwrap();
2617
2618        let tx = TransactionV0 {
2619            source_account_ed25519: Uint256([0u8; 32]),
2620            fee: 100,
2621            seq_num: SequenceNumber(1),
2622            time_bounds: None,
2623            memo: Memo::None,
2624            operations,
2625            ext: soroban_rs::xdr::TransactionV0Ext::V0,
2626        };
2627
2628        let mut envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
2629            tx,
2630            signatures: vec![].try_into().unwrap(),
2631        });
2632
2633        let new_op = create_payment_op();
2634        let result = add_operation_to_envelope(&mut envelope, new_op);
2635
2636        assert!(result.is_ok());
2637
2638        match envelope {
2639            TransactionEnvelope::TxV0(e) => {
2640                assert_eq!(e.tx.operations.len(), 2);
2641                assert_eq!(e.tx.fee, 200); // 100 stroops per operation
2642            }
2643            _ => panic!("Expected TxV0 envelope"),
2644        }
2645    }
2646
2647    #[test]
2648    fn test_add_operation_to_tx_v1() {
2649        let payment_op = create_payment_op();
2650        let operations = vec![payment_op.clone()].try_into().unwrap();
2651
2652        let tx = Transaction {
2653            source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
2654            fee: 100,
2655            seq_num: SequenceNumber(1),
2656            cond: Preconditions::None,
2657            memo: Memo::None,
2658            operations,
2659            ext: TransactionExt::V0,
2660        };
2661
2662        let mut envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
2663            tx,
2664            signatures: vec![].try_into().unwrap(),
2665        });
2666
2667        let new_op = create_payment_op();
2668        let result = add_operation_to_envelope(&mut envelope, new_op);
2669
2670        assert!(result.is_ok());
2671
2672        match envelope {
2673            TransactionEnvelope::Tx(e) => {
2674                assert_eq!(e.tx.operations.len(), 2);
2675                assert_eq!(e.tx.fee, 200); // 100 stroops per operation
2676            }
2677            _ => panic!("Expected Tx envelope"),
2678        }
2679    }
2680
2681    #[test]
2682    fn test_add_operation_to_fee_bump_fails() {
2683        // Create a simple inner transaction
2684        let payment_op = create_payment_op();
2685        let operations = vec![payment_op].try_into().unwrap();
2686
2687        let tx = Transaction {
2688            source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
2689            fee: 100,
2690            seq_num: SequenceNumber(1),
2691            cond: Preconditions::None,
2692            memo: Memo::None,
2693            operations,
2694            ext: TransactionExt::V0,
2695        };
2696
2697        let inner_envelope = TransactionV1Envelope {
2698            tx,
2699            signatures: vec![].try_into().unwrap(),
2700        };
2701
2702        let inner_tx = soroban_rs::xdr::FeeBumpTransactionInnerTx::Tx(inner_envelope);
2703
2704        let fee_bump_tx = soroban_rs::xdr::FeeBumpTransaction {
2705            fee_source: MuxedAccount::Ed25519(Uint256([2u8; 32])),
2706            fee: 200,
2707            inner_tx,
2708            ext: soroban_rs::xdr::FeeBumpTransactionExt::V0,
2709        };
2710
2711        let mut envelope =
2712            TransactionEnvelope::TxFeeBump(soroban_rs::xdr::FeeBumpTransactionEnvelope {
2713                tx: fee_bump_tx,
2714                signatures: vec![].try_into().unwrap(),
2715            });
2716
2717        let new_op = create_payment_op();
2718        let result = add_operation_to_envelope(&mut envelope, new_op);
2719
2720        assert!(result.is_err());
2721
2722        match result.unwrap_err() {
2723            StellarTransactionUtilsError::CannotModifyFeeBump => {}
2724            _ => panic!("Expected CannotModifyFeeBump error"),
2725        }
2726    }
2727}
2728
2729#[cfg(test)]
2730mod extract_time_bounds_tests {
2731    use super::*;
2732    use soroban_rs::xdr::{
2733        Memo, MuxedAccount, Operation, OperationBody, PaymentOp, Preconditions, SequenceNumber,
2734        TimeBounds, TimePoint, Transaction, TransactionExt, TransactionV0, TransactionV0Envelope,
2735        TransactionV1Envelope, Uint256,
2736    };
2737
2738    fn create_payment_op() -> Operation {
2739        Operation {
2740            source_account: None,
2741            body: OperationBody::Payment(PaymentOp {
2742                destination: MuxedAccount::Ed25519(Uint256([1u8; 32])),
2743                asset: Asset::Native,
2744                amount: 100,
2745            }),
2746        }
2747    }
2748
2749    #[test]
2750    fn test_extract_time_bounds_from_tx_v0_with_bounds() {
2751        let payment_op = create_payment_op();
2752        let operations = vec![payment_op].try_into().unwrap();
2753
2754        let time_bounds = TimeBounds {
2755            min_time: TimePoint(0),
2756            max_time: TimePoint(1000),
2757        };
2758
2759        let tx = TransactionV0 {
2760            source_account_ed25519: Uint256([0u8; 32]),
2761            fee: 100,
2762            seq_num: SequenceNumber(1),
2763            time_bounds: Some(time_bounds.clone()),
2764            memo: Memo::None,
2765            operations,
2766            ext: soroban_rs::xdr::TransactionV0Ext::V0,
2767        };
2768
2769        let envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
2770            tx,
2771            signatures: vec![].try_into().unwrap(),
2772        });
2773
2774        let result = extract_time_bounds(&envelope);
2775        assert!(result.is_some());
2776
2777        let bounds = result.unwrap();
2778        assert_eq!(bounds.min_time.0, 0);
2779        assert_eq!(bounds.max_time.0, 1000);
2780    }
2781
2782    #[test]
2783    fn test_extract_time_bounds_from_tx_v0_without_bounds() {
2784        let payment_op = create_payment_op();
2785        let operations = vec![payment_op].try_into().unwrap();
2786
2787        let tx = TransactionV0 {
2788            source_account_ed25519: Uint256([0u8; 32]),
2789            fee: 100,
2790            seq_num: SequenceNumber(1),
2791            time_bounds: None,
2792            memo: Memo::None,
2793            operations,
2794            ext: soroban_rs::xdr::TransactionV0Ext::V0,
2795        };
2796
2797        let envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
2798            tx,
2799            signatures: vec![].try_into().unwrap(),
2800        });
2801
2802        let result = extract_time_bounds(&envelope);
2803        assert!(result.is_none());
2804    }
2805
2806    #[test]
2807    fn test_extract_time_bounds_from_tx_v1_with_time_precondition() {
2808        let payment_op = create_payment_op();
2809        let operations = vec![payment_op].try_into().unwrap();
2810
2811        let time_bounds = TimeBounds {
2812            min_time: TimePoint(0),
2813            max_time: TimePoint(2000),
2814        };
2815
2816        let tx = Transaction {
2817            source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
2818            fee: 100,
2819            seq_num: SequenceNumber(1),
2820            cond: Preconditions::Time(time_bounds.clone()),
2821            memo: Memo::None,
2822            operations,
2823            ext: TransactionExt::V0,
2824        };
2825
2826        let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
2827            tx,
2828            signatures: vec![].try_into().unwrap(),
2829        });
2830
2831        let result = extract_time_bounds(&envelope);
2832        assert!(result.is_some());
2833
2834        let bounds = result.unwrap();
2835        assert_eq!(bounds.min_time.0, 0);
2836        assert_eq!(bounds.max_time.0, 2000);
2837    }
2838
2839    #[test]
2840    fn test_extract_time_bounds_from_tx_v1_without_time_precondition() {
2841        let payment_op = create_payment_op();
2842        let operations = vec![payment_op].try_into().unwrap();
2843
2844        let tx = Transaction {
2845            source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
2846            fee: 100,
2847            seq_num: SequenceNumber(1),
2848            cond: Preconditions::None,
2849            memo: Memo::None,
2850            operations,
2851            ext: TransactionExt::V0,
2852        };
2853
2854        let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
2855            tx,
2856            signatures: vec![].try_into().unwrap(),
2857        });
2858
2859        let result = extract_time_bounds(&envelope);
2860        assert!(result.is_none());
2861    }
2862
2863    #[test]
2864    fn test_extract_time_bounds_from_fee_bump() {
2865        // Create inner transaction with time bounds
2866        let payment_op = create_payment_op();
2867        let operations = vec![payment_op].try_into().unwrap();
2868
2869        let time_bounds = TimeBounds {
2870            min_time: TimePoint(0),
2871            max_time: TimePoint(3000),
2872        };
2873
2874        let tx = Transaction {
2875            source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
2876            fee: 100,
2877            seq_num: SequenceNumber(1),
2878            cond: Preconditions::Time(time_bounds.clone()),
2879            memo: Memo::None,
2880            operations,
2881            ext: TransactionExt::V0,
2882        };
2883
2884        let inner_envelope = TransactionV1Envelope {
2885            tx,
2886            signatures: vec![].try_into().unwrap(),
2887        };
2888
2889        let inner_tx = soroban_rs::xdr::FeeBumpTransactionInnerTx::Tx(inner_envelope);
2890
2891        let fee_bump_tx = soroban_rs::xdr::FeeBumpTransaction {
2892            fee_source: MuxedAccount::Ed25519(Uint256([2u8; 32])),
2893            fee: 200,
2894            inner_tx,
2895            ext: soroban_rs::xdr::FeeBumpTransactionExt::V0,
2896        };
2897
2898        let envelope =
2899            TransactionEnvelope::TxFeeBump(soroban_rs::xdr::FeeBumpTransactionEnvelope {
2900                tx: fee_bump_tx,
2901                signatures: vec![].try_into().unwrap(),
2902            });
2903
2904        let result = extract_time_bounds(&envelope);
2905        assert!(result.is_some());
2906
2907        let bounds = result.unwrap();
2908        assert_eq!(bounds.min_time.0, 0);
2909        assert_eq!(bounds.max_time.0, 3000);
2910    }
2911}
2912
2913#[cfg(test)]
2914mod set_time_bounds_tests {
2915    use super::*;
2916    use chrono::Utc;
2917    use soroban_rs::xdr::{
2918        Memo, MuxedAccount, Operation, OperationBody, PaymentOp, Preconditions, SequenceNumber,
2919        TimeBounds, TimePoint, Transaction, TransactionExt, TransactionV0, TransactionV0Envelope,
2920        TransactionV1Envelope, Uint256,
2921    };
2922
2923    fn create_payment_op() -> Operation {
2924        Operation {
2925            source_account: None,
2926            body: OperationBody::Payment(PaymentOp {
2927                destination: MuxedAccount::Ed25519(Uint256([1u8; 32])),
2928                asset: Asset::Native,
2929                amount: 100,
2930            }),
2931        }
2932    }
2933
2934    #[test]
2935    fn test_set_time_bounds_on_tx_v0() {
2936        let payment_op = create_payment_op();
2937        let operations = vec![payment_op].try_into().unwrap();
2938
2939        let tx = TransactionV0 {
2940            source_account_ed25519: Uint256([0u8; 32]),
2941            fee: 100,
2942            seq_num: SequenceNumber(1),
2943            time_bounds: None,
2944            memo: Memo::None,
2945            operations,
2946            ext: soroban_rs::xdr::TransactionV0Ext::V0,
2947        };
2948
2949        let mut envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
2950            tx,
2951            signatures: vec![].try_into().unwrap(),
2952        });
2953
2954        let valid_until = Utc::now() + chrono::Duration::seconds(300);
2955        let result = set_time_bounds(&mut envelope, valid_until);
2956
2957        assert!(result.is_ok());
2958
2959        match envelope {
2960            TransactionEnvelope::TxV0(e) => {
2961                assert!(e.tx.time_bounds.is_some());
2962                let bounds = e.tx.time_bounds.unwrap();
2963                assert_eq!(bounds.min_time.0, 0);
2964                assert_eq!(bounds.max_time.0, valid_until.timestamp() as u64);
2965            }
2966            _ => panic!("Expected TxV0 envelope"),
2967        }
2968    }
2969
2970    #[test]
2971    fn test_set_time_bounds_on_tx_v1() {
2972        let payment_op = create_payment_op();
2973        let operations = vec![payment_op].try_into().unwrap();
2974
2975        let tx = Transaction {
2976            source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
2977            fee: 100,
2978            seq_num: SequenceNumber(1),
2979            cond: Preconditions::None,
2980            memo: Memo::None,
2981            operations,
2982            ext: TransactionExt::V0,
2983        };
2984
2985        let mut envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
2986            tx,
2987            signatures: vec![].try_into().unwrap(),
2988        });
2989
2990        let valid_until = Utc::now() + chrono::Duration::seconds(300);
2991        let result = set_time_bounds(&mut envelope, valid_until);
2992
2993        assert!(result.is_ok());
2994
2995        match envelope {
2996            TransactionEnvelope::Tx(e) => match e.tx.cond {
2997                Preconditions::Time(bounds) => {
2998                    assert_eq!(bounds.min_time.0, 0);
2999                    assert_eq!(bounds.max_time.0, valid_until.timestamp() as u64);
3000                }
3001                _ => panic!("Expected Time precondition"),
3002            },
3003            _ => panic!("Expected Tx envelope"),
3004        }
3005    }
3006
3007    #[test]
3008    fn test_set_time_bounds_on_fee_bump_fails() {
3009        // Create a simple inner transaction
3010        let payment_op = create_payment_op();
3011        let operations = vec![payment_op].try_into().unwrap();
3012
3013        let tx = Transaction {
3014            source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
3015            fee: 100,
3016            seq_num: SequenceNumber(1),
3017            cond: Preconditions::None,
3018            memo: Memo::None,
3019            operations,
3020            ext: TransactionExt::V0,
3021        };
3022
3023        let inner_envelope = TransactionV1Envelope {
3024            tx,
3025            signatures: vec![].try_into().unwrap(),
3026        };
3027
3028        let inner_tx = soroban_rs::xdr::FeeBumpTransactionInnerTx::Tx(inner_envelope);
3029
3030        let fee_bump_tx = soroban_rs::xdr::FeeBumpTransaction {
3031            fee_source: MuxedAccount::Ed25519(Uint256([2u8; 32])),
3032            fee: 200,
3033            inner_tx,
3034            ext: soroban_rs::xdr::FeeBumpTransactionExt::V0,
3035        };
3036
3037        let mut envelope =
3038            TransactionEnvelope::TxFeeBump(soroban_rs::xdr::FeeBumpTransactionEnvelope {
3039                tx: fee_bump_tx,
3040                signatures: vec![].try_into().unwrap(),
3041            });
3042
3043        let valid_until = Utc::now() + chrono::Duration::seconds(300);
3044        let result = set_time_bounds(&mut envelope, valid_until);
3045
3046        assert!(result.is_err());
3047
3048        match result.unwrap_err() {
3049            StellarTransactionUtilsError::CannotSetTimeBoundsOnFeeBump => {}
3050            _ => panic!("Expected CannotSetTimeBoundsOnFeeBump error"),
3051        }
3052    }
3053
3054    #[test]
3055    fn test_set_time_bounds_replaces_existing() {
3056        let payment_op = create_payment_op();
3057        let operations = vec![payment_op].try_into().unwrap();
3058
3059        let old_time_bounds = TimeBounds {
3060            min_time: TimePoint(100),
3061            max_time: TimePoint(1000),
3062        };
3063
3064        let tx = TransactionV0 {
3065            source_account_ed25519: Uint256([0u8; 32]),
3066            fee: 100,
3067            seq_num: SequenceNumber(1),
3068            time_bounds: Some(old_time_bounds),
3069            memo: Memo::None,
3070            operations,
3071            ext: soroban_rs::xdr::TransactionV0Ext::V0,
3072        };
3073
3074        let mut envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
3075            tx,
3076            signatures: vec![].try_into().unwrap(),
3077        });
3078
3079        let valid_until = Utc::now() + chrono::Duration::seconds(300);
3080        let result = set_time_bounds(&mut envelope, valid_until);
3081
3082        assert!(result.is_ok());
3083
3084        match envelope {
3085            TransactionEnvelope::TxV0(e) => {
3086                assert!(e.tx.time_bounds.is_some());
3087                let bounds = e.tx.time_bounds.unwrap();
3088                // Should replace with new bounds (min_time = 0, not 100)
3089                assert_eq!(bounds.min_time.0, 0);
3090                assert_eq!(bounds.max_time.0, valid_until.timestamp() as u64);
3091            }
3092            _ => panic!("Expected TxV0 envelope"),
3093        }
3094    }
3095}
3096
3097// ============================================================================
3098// From<StellarTransactionUtilsError> for RelayerError Tests
3099// ============================================================================
3100
3101#[cfg(test)]
3102mod stellar_transaction_utils_error_conversion_tests {
3103    use super::*;
3104
3105    #[test]
3106    fn test_v0_transactions_not_supported_converts_to_validation_error() {
3107        let err = StellarTransactionUtilsError::V0TransactionsNotSupported;
3108        let relayer_err: RelayerError = err.into();
3109        match relayer_err {
3110            RelayerError::ValidationError(msg) => {
3111                assert_eq!(msg, "V0 transactions are not supported");
3112            }
3113            _ => panic!("Expected ValidationError"),
3114        }
3115    }
3116
3117    #[test]
3118    fn test_cannot_update_sequence_on_fee_bump_converts_to_validation_error() {
3119        let err = StellarTransactionUtilsError::CannotUpdateSequenceOnFeeBump;
3120        let relayer_err: RelayerError = err.into();
3121        match relayer_err {
3122            RelayerError::ValidationError(msg) => {
3123                assert_eq!(msg, "Cannot update sequence number on fee bump transaction");
3124            }
3125            _ => panic!("Expected ValidationError"),
3126        }
3127    }
3128
3129    #[test]
3130    fn test_cannot_set_time_bounds_on_fee_bump_converts_to_validation_error() {
3131        let err = StellarTransactionUtilsError::CannotSetTimeBoundsOnFeeBump;
3132        let relayer_err: RelayerError = err.into();
3133        match relayer_err {
3134            RelayerError::ValidationError(msg) => {
3135                assert_eq!(msg, "Cannot set time bounds on fee-bump transactions");
3136            }
3137            _ => panic!("Expected ValidationError"),
3138        }
3139    }
3140
3141    #[test]
3142    fn test_invalid_transaction_format_converts_to_validation_error() {
3143        let err = StellarTransactionUtilsError::InvalidTransactionFormat("bad format".to_string());
3144        let relayer_err: RelayerError = err.into();
3145        match relayer_err {
3146            RelayerError::ValidationError(msg) => {
3147                assert_eq!(msg, "bad format");
3148            }
3149            _ => panic!("Expected ValidationError"),
3150        }
3151    }
3152
3153    #[test]
3154    fn test_cannot_modify_fee_bump_converts_to_validation_error() {
3155        let err = StellarTransactionUtilsError::CannotModifyFeeBump;
3156        let relayer_err: RelayerError = err.into();
3157        match relayer_err {
3158            RelayerError::ValidationError(msg) => {
3159                assert_eq!(msg, "Cannot add operations to fee-bump transactions");
3160            }
3161            _ => panic!("Expected ValidationError"),
3162        }
3163    }
3164
3165    #[test]
3166    fn test_too_many_operations_converts_to_validation_error() {
3167        let err = StellarTransactionUtilsError::TooManyOperations(100);
3168        let relayer_err: RelayerError = err.into();
3169        match relayer_err {
3170            RelayerError::ValidationError(msg) => {
3171                assert!(msg.contains("Too many operations"));
3172                assert!(msg.contains("100"));
3173            }
3174            _ => panic!("Expected ValidationError"),
3175        }
3176    }
3177
3178    #[test]
3179    fn test_sequence_overflow_converts_to_internal_error() {
3180        let err = StellarTransactionUtilsError::SequenceOverflow("overflow msg".to_string());
3181        let relayer_err: RelayerError = err.into();
3182        match relayer_err {
3183            RelayerError::Internal(msg) => {
3184                assert_eq!(msg, "overflow msg");
3185            }
3186            _ => panic!("Expected Internal error"),
3187        }
3188    }
3189
3190    #[test]
3191    fn test_simulation_no_results_converts_to_internal_error() {
3192        let err = StellarTransactionUtilsError::SimulationNoResults;
3193        let relayer_err: RelayerError = err.into();
3194        match relayer_err {
3195            RelayerError::Internal(msg) => {
3196                assert!(msg.contains("no results"));
3197            }
3198            _ => panic!("Expected Internal error"),
3199        }
3200    }
3201
3202    #[test]
3203    fn test_asset_code_too_long_converts_to_validation_error() {
3204        let err =
3205            StellarTransactionUtilsError::AssetCodeTooLong(12, "VERYLONGASSETCODE".to_string());
3206        let relayer_err: RelayerError = err.into();
3207        match relayer_err {
3208            RelayerError::ValidationError(msg) => {
3209                assert!(msg.contains("Asset code too long"));
3210                assert!(msg.contains("12"));
3211            }
3212            _ => panic!("Expected ValidationError"),
3213        }
3214    }
3215
3216    #[test]
3217    fn test_invalid_asset_format_converts_to_validation_error() {
3218        let err = StellarTransactionUtilsError::InvalidAssetFormat("bad asset".to_string());
3219        let relayer_err: RelayerError = err.into();
3220        match relayer_err {
3221            RelayerError::ValidationError(msg) => {
3222                assert_eq!(msg, "bad asset");
3223            }
3224            _ => panic!("Expected ValidationError"),
3225        }
3226    }
3227
3228    #[test]
3229    fn test_invalid_account_address_converts_to_internal_error() {
3230        let err = StellarTransactionUtilsError::InvalidAccountAddress(
3231            "GABC".to_string(),
3232            "parse error".to_string(),
3233        );
3234        let relayer_err: RelayerError = err.into();
3235        match relayer_err {
3236            RelayerError::Internal(msg) => {
3237                assert_eq!(msg, "parse error");
3238            }
3239            _ => panic!("Expected Internal error"),
3240        }
3241    }
3242
3243    #[test]
3244    fn test_invalid_contract_address_converts_to_internal_error() {
3245        let err = StellarTransactionUtilsError::InvalidContractAddress(
3246            "CABC".to_string(),
3247            "contract parse error".to_string(),
3248        );
3249        let relayer_err: RelayerError = err.into();
3250        match relayer_err {
3251            RelayerError::Internal(msg) => {
3252                assert_eq!(msg, "contract parse error");
3253            }
3254            _ => panic!("Expected Internal error"),
3255        }
3256    }
3257
3258    #[test]
3259    fn test_symbol_creation_failed_converts_to_internal_error() {
3260        let err = StellarTransactionUtilsError::SymbolCreationFailed(
3261            "Balance".to_string(),
3262            "too long".to_string(),
3263        );
3264        let relayer_err: RelayerError = err.into();
3265        match relayer_err {
3266            RelayerError::Internal(msg) => {
3267                assert_eq!(msg, "too long");
3268            }
3269            _ => panic!("Expected Internal error"),
3270        }
3271    }
3272
3273    #[test]
3274    fn test_key_vector_creation_failed_converts_to_internal_error() {
3275        let err = StellarTransactionUtilsError::KeyVectorCreationFailed(
3276            "Balance".to_string(),
3277            "vec error".to_string(),
3278        );
3279        let relayer_err: RelayerError = err.into();
3280        match relayer_err {
3281            RelayerError::Internal(msg) => {
3282                assert_eq!(msg, "vec error");
3283            }
3284            _ => panic!("Expected Internal error"),
3285        }
3286    }
3287
3288    #[test]
3289    fn test_contract_data_query_persistent_failed_converts_to_internal_error() {
3290        let err = StellarTransactionUtilsError::ContractDataQueryPersistentFailed(
3291            "balance".to_string(),
3292            "rpc error".to_string(),
3293        );
3294        let relayer_err: RelayerError = err.into();
3295        match relayer_err {
3296            RelayerError::Internal(msg) => {
3297                assert_eq!(msg, "rpc error");
3298            }
3299            _ => panic!("Expected Internal error"),
3300        }
3301    }
3302
3303    #[test]
3304    fn test_contract_data_query_temporary_failed_converts_to_internal_error() {
3305        let err = StellarTransactionUtilsError::ContractDataQueryTemporaryFailed(
3306            "balance".to_string(),
3307            "temp error".to_string(),
3308        );
3309        let relayer_err: RelayerError = err.into();
3310        match relayer_err {
3311            RelayerError::Internal(msg) => {
3312                assert_eq!(msg, "temp error");
3313            }
3314            _ => panic!("Expected Internal error"),
3315        }
3316    }
3317
3318    #[test]
3319    fn test_ledger_entry_parse_failed_converts_to_internal_error() {
3320        let err = StellarTransactionUtilsError::LedgerEntryParseFailed(
3321            "entry".to_string(),
3322            "xdr error".to_string(),
3323        );
3324        let relayer_err: RelayerError = err.into();
3325        match relayer_err {
3326            RelayerError::Internal(msg) => {
3327                assert_eq!(msg, "xdr error");
3328            }
3329            _ => panic!("Expected Internal error"),
3330        }
3331    }
3332
3333    #[test]
3334    fn test_no_entries_found_converts_to_validation_error() {
3335        let err = StellarTransactionUtilsError::NoEntriesFound("balance".to_string());
3336        let relayer_err: RelayerError = err.into();
3337        match relayer_err {
3338            RelayerError::ValidationError(msg) => {
3339                assert!(msg.contains("No entries found"));
3340            }
3341            _ => panic!("Expected ValidationError"),
3342        }
3343    }
3344
3345    #[test]
3346    fn test_empty_entries_converts_to_validation_error() {
3347        let err = StellarTransactionUtilsError::EmptyEntries("balance".to_string());
3348        let relayer_err: RelayerError = err.into();
3349        match relayer_err {
3350            RelayerError::ValidationError(msg) => {
3351                assert!(msg.contains("Empty entries"));
3352            }
3353            _ => panic!("Expected ValidationError"),
3354        }
3355    }
3356
3357    #[test]
3358    fn test_unexpected_ledger_entry_type_converts_to_validation_error() {
3359        let err = StellarTransactionUtilsError::UnexpectedLedgerEntryType("balance".to_string());
3360        let relayer_err: RelayerError = err.into();
3361        match relayer_err {
3362            RelayerError::ValidationError(msg) => {
3363                assert!(msg.contains("Unexpected ledger entry type"));
3364            }
3365            _ => panic!("Expected ValidationError"),
3366        }
3367    }
3368
3369    #[test]
3370    fn test_invalid_issuer_length_converts_to_validation_error() {
3371        let err = StellarTransactionUtilsError::InvalidIssuerLength(56, "SHORT".to_string());
3372        let relayer_err: RelayerError = err.into();
3373        match relayer_err {
3374            RelayerError::ValidationError(msg) => {
3375                assert!(msg.contains("56"));
3376                assert!(msg.contains("SHORT"));
3377            }
3378            _ => panic!("Expected ValidationError"),
3379        }
3380    }
3381
3382    #[test]
3383    fn test_invalid_issuer_prefix_converts_to_validation_error() {
3384        let err = StellarTransactionUtilsError::InvalidIssuerPrefix('G', "CABC123".to_string());
3385        let relayer_err: RelayerError = err.into();
3386        match relayer_err {
3387            RelayerError::ValidationError(msg) => {
3388                assert!(msg.contains("'G'"));
3389                assert!(msg.contains("CABC123"));
3390            }
3391            _ => panic!("Expected ValidationError"),
3392        }
3393    }
3394
3395    #[test]
3396    fn test_account_fetch_failed_converts_to_provider_error() {
3397        let err = StellarTransactionUtilsError::AccountFetchFailed("fetch error".to_string());
3398        let relayer_err: RelayerError = err.into();
3399        match relayer_err {
3400            RelayerError::ProviderError(msg) => {
3401                assert_eq!(msg, "fetch error");
3402            }
3403            _ => panic!("Expected ProviderError"),
3404        }
3405    }
3406
3407    #[test]
3408    fn test_trustline_query_failed_converts_to_provider_error() {
3409        let err = StellarTransactionUtilsError::TrustlineQueryFailed(
3410            "USDC".to_string(),
3411            "rpc fail".to_string(),
3412        );
3413        let relayer_err: RelayerError = err.into();
3414        match relayer_err {
3415            RelayerError::ProviderError(msg) => {
3416                assert_eq!(msg, "rpc fail");
3417            }
3418            _ => panic!("Expected ProviderError"),
3419        }
3420    }
3421
3422    #[test]
3423    fn test_contract_invocation_failed_converts_to_provider_error() {
3424        let err = StellarTransactionUtilsError::ContractInvocationFailed(
3425            "transfer".to_string(),
3426            "invoke error".to_string(),
3427        );
3428        let relayer_err: RelayerError = err.into();
3429        match relayer_err {
3430            RelayerError::ProviderError(msg) => {
3431                assert_eq!(msg, "invoke error");
3432            }
3433            _ => panic!("Expected ProviderError"),
3434        }
3435    }
3436
3437    #[test]
3438    fn test_xdr_parse_failed_converts_to_internal_error() {
3439        let err = StellarTransactionUtilsError::XdrParseFailed("xdr parse fail".to_string());
3440        let relayer_err: RelayerError = err.into();
3441        match relayer_err {
3442            RelayerError::Internal(msg) => {
3443                assert_eq!(msg, "xdr parse fail");
3444            }
3445            _ => panic!("Expected Internal error"),
3446        }
3447    }
3448
3449    #[test]
3450    fn test_operation_extraction_failed_converts_to_internal_error() {
3451        let err =
3452            StellarTransactionUtilsError::OperationExtractionFailed("extract fail".to_string());
3453        let relayer_err: RelayerError = err.into();
3454        match relayer_err {
3455            RelayerError::Internal(msg) => {
3456                assert_eq!(msg, "extract fail");
3457            }
3458            _ => panic!("Expected Internal error"),
3459        }
3460    }
3461
3462    #[test]
3463    fn test_simulation_failed_converts_to_internal_error() {
3464        let err = StellarTransactionUtilsError::SimulationFailed("sim error".to_string());
3465        let relayer_err: RelayerError = err.into();
3466        match relayer_err {
3467            RelayerError::Internal(msg) => {
3468                assert_eq!(msg, "sim error");
3469            }
3470            _ => panic!("Expected Internal error"),
3471        }
3472    }
3473
3474    #[test]
3475    fn test_simulation_check_failed_converts_to_internal_error() {
3476        let err = StellarTransactionUtilsError::SimulationCheckFailed("check fail".to_string());
3477        let relayer_err: RelayerError = err.into();
3478        match relayer_err {
3479            RelayerError::Internal(msg) => {
3480                assert_eq!(msg, "check fail");
3481            }
3482            _ => panic!("Expected Internal error"),
3483        }
3484    }
3485
3486    #[test]
3487    fn test_dex_quote_failed_converts_to_internal_error() {
3488        let err = StellarTransactionUtilsError::DexQuoteFailed("dex error".to_string());
3489        let relayer_err: RelayerError = err.into();
3490        match relayer_err {
3491            RelayerError::Internal(msg) => {
3492                assert_eq!(msg, "dex error");
3493            }
3494            _ => panic!("Expected Internal error"),
3495        }
3496    }
3497
3498    #[test]
3499    fn test_empty_asset_code_converts_to_validation_error() {
3500        let err = StellarTransactionUtilsError::EmptyAssetCode("CODE:ISSUER".to_string());
3501        let relayer_err: RelayerError = err.into();
3502        match relayer_err {
3503            RelayerError::ValidationError(msg) => {
3504                assert!(msg.contains("Asset code cannot be empty"));
3505            }
3506            _ => panic!("Expected ValidationError"),
3507        }
3508    }
3509
3510    #[test]
3511    fn test_empty_issuer_address_converts_to_validation_error() {
3512        let err = StellarTransactionUtilsError::EmptyIssuerAddress("USDC:".to_string());
3513        let relayer_err: RelayerError = err.into();
3514        match relayer_err {
3515            RelayerError::ValidationError(msg) => {
3516                assert!(msg.contains("Issuer address cannot be empty"));
3517            }
3518            _ => panic!("Expected ValidationError"),
3519        }
3520    }
3521
3522    #[test]
3523    fn test_no_trustline_found_converts_to_validation_error() {
3524        let err =
3525            StellarTransactionUtilsError::NoTrustlineFound("USDC".to_string(), "GABC".to_string());
3526        let relayer_err: RelayerError = err.into();
3527        match relayer_err {
3528            RelayerError::ValidationError(msg) => {
3529                assert!(msg.contains("No trustline found"));
3530            }
3531            _ => panic!("Expected ValidationError"),
3532        }
3533    }
3534
3535    #[test]
3536    fn test_unsupported_trustline_version_converts_to_validation_error() {
3537        let err = StellarTransactionUtilsError::UnsupportedTrustlineVersion;
3538        let relayer_err: RelayerError = err.into();
3539        match relayer_err {
3540            RelayerError::ValidationError(msg) => {
3541                assert!(msg.contains("Unsupported trustline"));
3542            }
3543            _ => panic!("Expected ValidationError"),
3544        }
3545    }
3546
3547    #[test]
3548    fn test_unexpected_trustline_entry_type_converts_to_validation_error() {
3549        let err = StellarTransactionUtilsError::UnexpectedTrustlineEntryType;
3550        let relayer_err: RelayerError = err.into();
3551        match relayer_err {
3552            RelayerError::ValidationError(msg) => {
3553                assert!(msg.contains("Unexpected ledger entry type"));
3554            }
3555            _ => panic!("Expected ValidationError"),
3556        }
3557    }
3558
3559    #[test]
3560    fn test_balance_too_large_converts_to_validation_error() {
3561        let err = StellarTransactionUtilsError::BalanceTooLarge(1, 999);
3562        let relayer_err: RelayerError = err.into();
3563        match relayer_err {
3564            RelayerError::ValidationError(msg) => {
3565                assert!(msg.contains("Balance too large"));
3566            }
3567            _ => panic!("Expected ValidationError"),
3568        }
3569    }
3570
3571    #[test]
3572    fn test_negative_balance_i128_converts_to_validation_error() {
3573        let err = StellarTransactionUtilsError::NegativeBalanceI128(42);
3574        let relayer_err: RelayerError = err.into();
3575        match relayer_err {
3576            RelayerError::ValidationError(msg) => {
3577                assert!(msg.contains("Negative balance"));
3578            }
3579            _ => panic!("Expected ValidationError"),
3580        }
3581    }
3582
3583    #[test]
3584    fn test_negative_balance_i64_converts_to_validation_error() {
3585        let err = StellarTransactionUtilsError::NegativeBalanceI64(-5);
3586        let relayer_err: RelayerError = err.into();
3587        match relayer_err {
3588            RelayerError::ValidationError(msg) => {
3589                assert!(msg.contains("Negative balance"));
3590            }
3591            _ => panic!("Expected ValidationError"),
3592        }
3593    }
3594
3595    #[test]
3596    fn test_unexpected_balance_type_converts_to_validation_error() {
3597        let err = StellarTransactionUtilsError::UnexpectedBalanceType("Bool(true)".to_string());
3598        let relayer_err: RelayerError = err.into();
3599        match relayer_err {
3600            RelayerError::ValidationError(msg) => {
3601                assert!(msg.contains("Unexpected balance value type"));
3602            }
3603            _ => panic!("Expected ValidationError"),
3604        }
3605    }
3606
3607    #[test]
3608    fn test_unexpected_contract_data_entry_type_converts_to_validation_error() {
3609        let err = StellarTransactionUtilsError::UnexpectedContractDataEntryType;
3610        let relayer_err: RelayerError = err.into();
3611        match relayer_err {
3612            RelayerError::ValidationError(msg) => {
3613                assert!(msg.contains("Unexpected ledger entry type"));
3614            }
3615            _ => panic!("Expected ValidationError"),
3616        }
3617    }
3618
3619    #[test]
3620    fn test_native_asset_in_trustline_query_converts_to_validation_error() {
3621        let err = StellarTransactionUtilsError::NativeAssetInTrustlineQuery;
3622        let relayer_err: RelayerError = err.into();
3623        match relayer_err {
3624            RelayerError::ValidationError(msg) => {
3625                assert!(msg.contains("Native asset"));
3626            }
3627            _ => panic!("Expected ValidationError"),
3628        }
3629    }
3630}