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/// Computes the resubmit interval with exponential backoff based on total transaction age.
1278///
1279/// The interval doubles each time the total age doubles:
1280///   - age < base  → `None` (too early to resubmit)
1281///   - age 1-2x base → interval = base  (10s)
1282///   - age 2-4x base → interval = 2*base (20s)
1283///   - age 4-8x base → interval = 4*base (40s)
1284///   - ...capped at `max_interval`
1285///
1286/// Returns the backoff interval to compare against time since last submission (`sent_at`).
1287pub fn compute_resubmit_backoff_interval(
1288    total_age: chrono::Duration,
1289    base_interval_secs: i64,
1290    max_interval_secs: i64,
1291) -> Option<chrono::Duration> {
1292    let age_secs = total_age.num_seconds();
1293
1294    if age_secs < base_interval_secs {
1295        return None;
1296    }
1297
1298    // n = floor(log2(age / base)), so interval = base * 2^n
1299    let ratio = age_secs / base_interval_secs; // >= 1
1300    let n = (ratio as u64).ilog2(); // floor(log2(ratio))
1301    let interval = base_interval_secs.saturating_mul(1_i64.wrapping_shl(n));
1302    let capped = interval.min(max_interval_secs);
1303
1304    Some(chrono::Duration::seconds(capped))
1305}
1306
1307#[cfg(test)]
1308mod tests {
1309    use super::*;
1310    use crate::domain::transaction::stellar::test_helpers::TEST_PK;
1311    use crate::models::AssetSpec;
1312    use crate::models::{AuthSpec, ContractSource, WasmSource};
1313
1314    fn payment_op(destination: &str) -> OperationSpec {
1315        OperationSpec::Payment {
1316            destination: destination.to_string(),
1317            amount: 100,
1318            asset: AssetSpec::Native,
1319        }
1320    }
1321
1322    #[test]
1323    fn returns_false_for_only_payment_ops() {
1324        let ops = vec![payment_op(TEST_PK)];
1325        assert!(!needs_simulation(&ops));
1326    }
1327
1328    #[test]
1329    fn returns_true_for_invoke_contract_ops() {
1330        let ops = vec![OperationSpec::InvokeContract {
1331            contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
1332                .to_string(),
1333            function_name: "transfer".to_string(),
1334            args: vec![],
1335            auth: None,
1336        }];
1337        assert!(needs_simulation(&ops));
1338    }
1339
1340    #[test]
1341    fn returns_true_for_upload_wasm_ops() {
1342        let ops = vec![OperationSpec::UploadWasm {
1343            wasm: WasmSource::Hex {
1344                hex: "deadbeef".to_string(),
1345            },
1346            auth: None,
1347        }];
1348        assert!(needs_simulation(&ops));
1349    }
1350
1351    #[test]
1352    fn returns_true_for_create_contract_ops() {
1353        let ops = vec![OperationSpec::CreateContract {
1354            source: ContractSource::Address {
1355                address: TEST_PK.to_string(),
1356            },
1357            wasm_hash: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
1358                .to_string(),
1359            salt: None,
1360            constructor_args: None,
1361            auth: None,
1362        }];
1363        assert!(needs_simulation(&ops));
1364    }
1365
1366    #[test]
1367    fn returns_true_for_single_invoke_host_function() {
1368        let ops = vec![OperationSpec::InvokeContract {
1369            contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
1370                .to_string(),
1371            function_name: "transfer".to_string(),
1372            args: vec![],
1373            auth: Some(AuthSpec::SourceAccount),
1374        }];
1375        assert!(needs_simulation(&ops));
1376    }
1377
1378    #[test]
1379    fn returns_false_for_multiple_payment_ops() {
1380        let ops = vec![payment_op(TEST_PK), payment_op(TEST_PK)];
1381        assert!(!needs_simulation(&ops));
1382    }
1383
1384    mod next_sequence_u64_tests {
1385        use super::*;
1386
1387        #[test]
1388        fn test_increment() {
1389            assert_eq!(next_sequence_u64(0).unwrap(), 1);
1390
1391            assert_eq!(next_sequence_u64(12345).unwrap(), 12346);
1392        }
1393
1394        #[test]
1395        fn test_error_path_overflow_i64_max() {
1396            let result = next_sequence_u64(i64::MAX);
1397            assert!(result.is_err());
1398            match result.unwrap_err() {
1399                RelayerError::ProviderError(msg) => assert_eq!(msg, "sequence overflow"),
1400                _ => panic!("Unexpected error type"),
1401            }
1402        }
1403    }
1404
1405    mod i64_from_u64_tests {
1406        use super::*;
1407
1408        #[test]
1409        fn test_happy_path_conversion() {
1410            assert_eq!(i64_from_u64(0).unwrap(), 0);
1411            assert_eq!(i64_from_u64(12345).unwrap(), 12345);
1412            assert_eq!(i64_from_u64(i64::MAX as u64).unwrap(), i64::MAX);
1413        }
1414
1415        #[test]
1416        fn test_error_path_overflow_u64_max() {
1417            let result = i64_from_u64(u64::MAX);
1418            assert!(result.is_err());
1419            match result.unwrap_err() {
1420                RelayerError::ProviderError(msg) => assert_eq!(msg, "u64→i64 overflow"),
1421                _ => panic!("Unexpected error type"),
1422            }
1423        }
1424
1425        #[test]
1426        fn test_edge_case_just_above_i64_max() {
1427            // Smallest u64 value that will overflow i64
1428            let value = (i64::MAX as u64) + 1;
1429            let result = i64_from_u64(value);
1430            assert!(result.is_err());
1431            match result.unwrap_err() {
1432                RelayerError::ProviderError(msg) => assert_eq!(msg, "u64→i64 overflow"),
1433                _ => panic!("Unexpected error type"),
1434            }
1435        }
1436    }
1437
1438    mod is_bad_sequence_error_tests {
1439        use super::*;
1440
1441        #[test]
1442        fn test_detects_txbadseq() {
1443            assert!(is_bad_sequence_error(
1444                "Failed to send transaction: transaction submission failed: TxBadSeq"
1445            ));
1446            assert!(is_bad_sequence_error("Error: TxBadSeq"));
1447            assert!(is_bad_sequence_error("txbadseq"));
1448            assert!(is_bad_sequence_error("TXBADSEQ"));
1449        }
1450
1451        #[test]
1452        fn test_returns_false_for_other_errors() {
1453            assert!(!is_bad_sequence_error("network timeout"));
1454            assert!(!is_bad_sequence_error("insufficient balance"));
1455            assert!(!is_bad_sequence_error("tx_insufficient_fee"));
1456            assert!(!is_bad_sequence_error("bad_auth"));
1457            assert!(!is_bad_sequence_error(""));
1458        }
1459    }
1460
1461    mod status_check_utils_tests {
1462        use crate::models::{
1463            NetworkTransactionData, StellarTransactionData, TransactionError, TransactionInput,
1464            TransactionRepoModel,
1465        };
1466        use crate::utils::mocks::mockutils::create_mock_transaction;
1467        use chrono::{Duration, Utc};
1468
1469        /// Helper to create a test transaction with a specific created_at timestamp
1470        fn create_test_tx_with_age(seconds_ago: i64) -> TransactionRepoModel {
1471            let created_at = (Utc::now() - Duration::seconds(seconds_ago)).to_rfc3339();
1472            let mut tx = create_mock_transaction();
1473            tx.id = format!("test-tx-{seconds_ago}");
1474            tx.created_at = created_at;
1475            tx.network_data = NetworkTransactionData::Stellar(StellarTransactionData {
1476                source_account: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
1477                    .to_string(),
1478                fee: None,
1479                sequence_number: None,
1480                memo: None,
1481                valid_until: None,
1482                network_passphrase: "Test SDF Network ; September 2015".to_string(),
1483                signatures: vec![],
1484                hash: Some("test-hash-12345".to_string()),
1485                simulation_transaction_data: None,
1486                transaction_input: TransactionInput::Operations(vec![]),
1487                signed_envelope_xdr: None,
1488                transaction_result_xdr: None,
1489            });
1490            tx
1491        }
1492
1493        mod get_age_since_created_tests {
1494            use crate::domain::transaction::util::get_age_since_created;
1495
1496            use super::*;
1497
1498            #[test]
1499            fn test_returns_correct_age_for_recent_transaction() {
1500                let tx = create_test_tx_with_age(30); // 30 seconds ago
1501                let age = get_age_since_created(&tx).unwrap();
1502
1503                // Allow for small timing differences (within 1 second)
1504                assert!(age.num_seconds() >= 29 && age.num_seconds() <= 31);
1505            }
1506
1507            #[test]
1508            fn test_returns_correct_age_for_old_transaction() {
1509                let tx = create_test_tx_with_age(3600); // 1 hour ago
1510                let age = get_age_since_created(&tx).unwrap();
1511
1512                // Allow for small timing differences
1513                assert!(age.num_seconds() >= 3599 && age.num_seconds() <= 3601);
1514            }
1515
1516            #[test]
1517            fn test_returns_zero_age_for_just_created_transaction() {
1518                let tx = create_test_tx_with_age(0); // Just now
1519                let age = get_age_since_created(&tx).unwrap();
1520
1521                // Should be very close to 0
1522                assert!(age.num_seconds() >= 0 && age.num_seconds() <= 1);
1523            }
1524
1525            #[test]
1526            fn test_handles_negative_age_gracefully() {
1527                // Create transaction with future timestamp (clock skew scenario)
1528                let created_at = (Utc::now() + Duration::seconds(10)).to_rfc3339();
1529                let mut tx = create_mock_transaction();
1530                tx.created_at = created_at;
1531
1532                let age = get_age_since_created(&tx).unwrap();
1533
1534                // Age should be negative
1535                assert!(age.num_seconds() < 0);
1536            }
1537
1538            #[test]
1539            fn test_returns_error_for_invalid_created_at() {
1540                let mut tx = create_mock_transaction();
1541                tx.created_at = "invalid-timestamp".to_string();
1542
1543                let result = get_age_since_created(&tx);
1544                assert!(result.is_err());
1545
1546                match result.unwrap_err() {
1547                    TransactionError::UnexpectedError(msg) => {
1548                        assert!(msg.contains("Invalid created_at timestamp"));
1549                    }
1550                    _ => panic!("Expected UnexpectedError"),
1551                }
1552            }
1553
1554            #[test]
1555            fn test_returns_error_for_empty_created_at() {
1556                let mut tx = create_mock_transaction();
1557                tx.created_at = "".to_string();
1558
1559                let result = get_age_since_created(&tx);
1560                assert!(result.is_err());
1561            }
1562
1563            #[test]
1564            fn test_handles_various_rfc3339_formats() {
1565                let mut tx = create_mock_transaction();
1566
1567                // Test with UTC timezone
1568                tx.created_at = "2025-01-01T12:00:00Z".to_string();
1569                assert!(get_age_since_created(&tx).is_ok());
1570
1571                // Test with offset timezone
1572                tx.created_at = "2025-01-01T12:00:00+00:00".to_string();
1573                assert!(get_age_since_created(&tx).is_ok());
1574
1575                // Test with milliseconds
1576                tx.created_at = "2025-01-01T12:00:00.123Z".to_string();
1577                assert!(get_age_since_created(&tx).is_ok());
1578            }
1579        }
1580    }
1581
1582    #[test]
1583    fn test_create_signature_payload_functions() {
1584        use soroban_rs::xdr::{
1585            Hash, SequenceNumber, TransactionEnvelope, TransactionV0, TransactionV0Envelope,
1586            Uint256,
1587        };
1588
1589        // Test create_transaction_signature_payload
1590        let transaction = soroban_rs::xdr::Transaction {
1591            source_account: soroban_rs::xdr::MuxedAccount::Ed25519(Uint256([1u8; 32])),
1592            fee: 100,
1593            seq_num: SequenceNumber(123),
1594            cond: soroban_rs::xdr::Preconditions::None,
1595            memo: soroban_rs::xdr::Memo::None,
1596            operations: vec![].try_into().unwrap(),
1597            ext: soroban_rs::xdr::TransactionExt::V0,
1598        };
1599        let network_id = Hash([2u8; 32]);
1600
1601        let payload = create_transaction_signature_payload(&transaction, &network_id);
1602        assert_eq!(payload.network_id, network_id);
1603
1604        // Test create_signature_payload with V0 envelope
1605        let v0_tx = TransactionV0 {
1606            source_account_ed25519: Uint256([1u8; 32]),
1607            fee: 100,
1608            seq_num: SequenceNumber(123),
1609            time_bounds: None,
1610            memo: soroban_rs::xdr::Memo::None,
1611            operations: vec![].try_into().unwrap(),
1612            ext: soroban_rs::xdr::TransactionV0Ext::V0,
1613        };
1614        let v0_envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
1615            tx: v0_tx,
1616            signatures: vec![].try_into().unwrap(),
1617        });
1618
1619        let v0_payload = create_signature_payload(&v0_envelope, &network_id).unwrap();
1620        assert_eq!(v0_payload.network_id, network_id);
1621    }
1622
1623    mod convert_v0_to_v1_transaction_tests {
1624        use super::*;
1625        use soroban_rs::xdr::{SequenceNumber, TransactionV0, Uint256};
1626
1627        #[test]
1628        fn test_convert_v0_to_v1_transaction() {
1629            // Create a simple V0 transaction
1630            let v0_tx = TransactionV0 {
1631                source_account_ed25519: Uint256([1u8; 32]),
1632                fee: 100,
1633                seq_num: SequenceNumber(123),
1634                time_bounds: None,
1635                memo: soroban_rs::xdr::Memo::None,
1636                operations: vec![].try_into().unwrap(),
1637                ext: soroban_rs::xdr::TransactionV0Ext::V0,
1638            };
1639
1640            // Convert to V1
1641            let v1_tx = convert_v0_to_v1_transaction(&v0_tx);
1642
1643            // Check that conversion worked correctly
1644            assert_eq!(v1_tx.fee, v0_tx.fee);
1645            assert_eq!(v1_tx.seq_num, v0_tx.seq_num);
1646            assert_eq!(v1_tx.memo, v0_tx.memo);
1647            assert_eq!(v1_tx.operations, v0_tx.operations);
1648            assert!(matches!(v1_tx.ext, soroban_rs::xdr::TransactionExt::V0));
1649            assert!(matches!(v1_tx.cond, soroban_rs::xdr::Preconditions::None));
1650
1651            // Check source account conversion
1652            match v1_tx.source_account {
1653                soroban_rs::xdr::MuxedAccount::Ed25519(addr) => {
1654                    assert_eq!(addr, v0_tx.source_account_ed25519);
1655                }
1656                _ => panic!("Expected Ed25519 muxed account"),
1657            }
1658        }
1659
1660        #[test]
1661        fn test_convert_v0_to_v1_transaction_with_time_bounds() {
1662            // Create a V0 transaction with time bounds
1663            let time_bounds = soroban_rs::xdr::TimeBounds {
1664                min_time: soroban_rs::xdr::TimePoint(100),
1665                max_time: soroban_rs::xdr::TimePoint(200),
1666            };
1667
1668            let v0_tx = TransactionV0 {
1669                source_account_ed25519: Uint256([2u8; 32]),
1670                fee: 200,
1671                seq_num: SequenceNumber(456),
1672                time_bounds: Some(time_bounds.clone()),
1673                memo: soroban_rs::xdr::Memo::Text("test".try_into().unwrap()),
1674                operations: vec![].try_into().unwrap(),
1675                ext: soroban_rs::xdr::TransactionV0Ext::V0,
1676            };
1677
1678            // Convert to V1
1679            let v1_tx = convert_v0_to_v1_transaction(&v0_tx);
1680
1681            // Check that time bounds were correctly converted to preconditions
1682            match v1_tx.cond {
1683                soroban_rs::xdr::Preconditions::Time(tb) => {
1684                    assert_eq!(tb, time_bounds);
1685                }
1686                _ => panic!("Expected Time preconditions"),
1687            }
1688        }
1689    }
1690}
1691
1692#[cfg(test)]
1693mod parse_contract_address_tests {
1694    use super::*;
1695    use crate::domain::transaction::stellar::test_helpers::{
1696        TEST_CONTRACT, TEST_PK as TEST_ACCOUNT,
1697    };
1698
1699    #[test]
1700    fn test_parse_valid_contract_address() {
1701        let result = parse_contract_address(TEST_CONTRACT);
1702        assert!(result.is_ok());
1703
1704        let hash = result.unwrap();
1705        assert_eq!(hash.0.len(), 32);
1706    }
1707
1708    #[test]
1709    fn test_parse_invalid_contract_address() {
1710        let result = parse_contract_address("INVALID_CONTRACT");
1711        assert!(result.is_err());
1712
1713        match result.unwrap_err() {
1714            StellarTransactionUtilsError::InvalidContractAddress(addr, _) => {
1715                assert_eq!(addr, "INVALID_CONTRACT");
1716            }
1717            _ => panic!("Expected InvalidContractAddress error"),
1718        }
1719    }
1720
1721    #[test]
1722    fn test_parse_contract_address_wrong_prefix() {
1723        // Try with an account address instead of contract
1724        let result = parse_contract_address(TEST_ACCOUNT);
1725        assert!(result.is_err());
1726    }
1727
1728    #[test]
1729    fn test_parse_empty_contract_address() {
1730        let result = parse_contract_address("");
1731        assert!(result.is_err());
1732    }
1733}
1734
1735// ============================================================================
1736// Update Envelope Sequence and Envelope Fee Tests
1737// ============================================================================
1738
1739#[cfg(test)]
1740mod update_envelope_sequence_tests {
1741    use super::*;
1742    use soroban_rs::xdr::{
1743        FeeBumpTransaction, FeeBumpTransactionEnvelope, FeeBumpTransactionExt,
1744        FeeBumpTransactionInnerTx, Memo, MuxedAccount, Preconditions, SequenceNumber, Transaction,
1745        TransactionExt, TransactionV0, TransactionV0Envelope, TransactionV0Ext,
1746        TransactionV1Envelope, Uint256, VecM,
1747    };
1748
1749    fn create_minimal_v1_envelope() -> TransactionEnvelope {
1750        let tx = Transaction {
1751            source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
1752            fee: 100,
1753            seq_num: SequenceNumber(0),
1754            cond: Preconditions::None,
1755            memo: Memo::None,
1756            operations: VecM::default(),
1757            ext: TransactionExt::V0,
1758        };
1759        TransactionEnvelope::Tx(TransactionV1Envelope {
1760            tx,
1761            signatures: VecM::default(),
1762        })
1763    }
1764
1765    fn create_v0_envelope() -> TransactionEnvelope {
1766        let tx = TransactionV0 {
1767            source_account_ed25519: Uint256([0u8; 32]),
1768            fee: 100,
1769            seq_num: SequenceNumber(0),
1770            time_bounds: None,
1771            memo: Memo::None,
1772            operations: VecM::default(),
1773            ext: TransactionV0Ext::V0,
1774        };
1775        TransactionEnvelope::TxV0(TransactionV0Envelope {
1776            tx,
1777            signatures: VecM::default(),
1778        })
1779    }
1780
1781    fn create_fee_bump_envelope() -> TransactionEnvelope {
1782        let inner_tx = Transaction {
1783            source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
1784            fee: 100,
1785            seq_num: SequenceNumber(0),
1786            cond: Preconditions::None,
1787            memo: Memo::None,
1788            operations: VecM::default(),
1789            ext: TransactionExt::V0,
1790        };
1791        let inner_envelope = TransactionV1Envelope {
1792            tx: inner_tx,
1793            signatures: VecM::default(),
1794        };
1795        let fee_bump_tx = FeeBumpTransaction {
1796            fee_source: MuxedAccount::Ed25519(Uint256([1u8; 32])),
1797            fee: 200,
1798            inner_tx: FeeBumpTransactionInnerTx::Tx(inner_envelope),
1799            ext: FeeBumpTransactionExt::V0,
1800        };
1801        TransactionEnvelope::TxFeeBump(FeeBumpTransactionEnvelope {
1802            tx: fee_bump_tx,
1803            signatures: VecM::default(),
1804        })
1805    }
1806
1807    #[test]
1808    fn test_update_envelope_sequence() {
1809        let mut envelope = create_minimal_v1_envelope();
1810        update_envelope_sequence(&mut envelope, 12345).unwrap();
1811        if let TransactionEnvelope::Tx(v1) = &envelope {
1812            assert_eq!(v1.tx.seq_num.0, 12345);
1813        } else {
1814            panic!("Expected Tx envelope");
1815        }
1816    }
1817
1818    #[test]
1819    fn test_update_envelope_sequence_v0_returns_error() {
1820        let mut envelope = create_v0_envelope();
1821        let result = update_envelope_sequence(&mut envelope, 12345);
1822        assert!(result.is_err());
1823        match result.unwrap_err() {
1824            StellarTransactionUtilsError::V0TransactionsNotSupported => {}
1825            _ => panic!("Expected V0TransactionsNotSupported error"),
1826        }
1827    }
1828
1829    #[test]
1830    fn test_update_envelope_sequence_fee_bump_returns_error() {
1831        let mut envelope = create_fee_bump_envelope();
1832        let result = update_envelope_sequence(&mut envelope, 12345);
1833        assert!(result.is_err());
1834        match result.unwrap_err() {
1835            StellarTransactionUtilsError::CannotUpdateSequenceOnFeeBump => {}
1836            _ => panic!("Expected CannotUpdateSequenceOnFeeBump error"),
1837        }
1838    }
1839
1840    #[test]
1841    fn test_update_envelope_sequence_zero() {
1842        let mut envelope = create_minimal_v1_envelope();
1843        update_envelope_sequence(&mut envelope, 0).unwrap();
1844        if let TransactionEnvelope::Tx(v1) = &envelope {
1845            assert_eq!(v1.tx.seq_num.0, 0);
1846        } else {
1847            panic!("Expected Tx envelope");
1848        }
1849    }
1850
1851    #[test]
1852    fn test_update_envelope_sequence_max_value() {
1853        let mut envelope = create_minimal_v1_envelope();
1854        update_envelope_sequence(&mut envelope, i64::MAX).unwrap();
1855        if let TransactionEnvelope::Tx(v1) = &envelope {
1856            assert_eq!(v1.tx.seq_num.0, i64::MAX);
1857        } else {
1858            panic!("Expected Tx envelope");
1859        }
1860    }
1861
1862    #[test]
1863    fn test_envelope_fee_in_stroops_v1() {
1864        let envelope = create_minimal_v1_envelope();
1865        let fee = envelope_fee_in_stroops(&envelope).unwrap();
1866        assert_eq!(fee, 100);
1867    }
1868
1869    #[test]
1870    fn test_envelope_fee_in_stroops_v0_returns_error() {
1871        let envelope = create_v0_envelope();
1872        let result = envelope_fee_in_stroops(&envelope);
1873        assert!(result.is_err());
1874        match result.unwrap_err() {
1875            StellarTransactionUtilsError::InvalidTransactionFormat(msg) => {
1876                assert!(msg.contains("Expected V1"));
1877            }
1878            _ => panic!("Expected InvalidTransactionFormat error"),
1879        }
1880    }
1881
1882    #[test]
1883    fn test_envelope_fee_in_stroops_fee_bump_returns_error() {
1884        let envelope = create_fee_bump_envelope();
1885        let result = envelope_fee_in_stroops(&envelope);
1886        assert!(result.is_err());
1887    }
1888}
1889
1890// ============================================================================
1891// Contract Data Key Tests
1892// ============================================================================
1893
1894#[cfg(test)]
1895mod create_contract_data_key_tests {
1896    use super::*;
1897    use crate::domain::transaction::stellar::test_helpers::TEST_PK as TEST_ACCOUNT;
1898    use stellar_strkey::ed25519::PublicKey;
1899
1900    #[test]
1901    fn test_create_key_without_address() {
1902        let result = create_contract_data_key("Balance", None);
1903        assert!(result.is_ok());
1904
1905        match result.unwrap() {
1906            ScVal::Symbol(sym) => {
1907                assert_eq!(sym.to_string(), "Balance");
1908            }
1909            _ => panic!("Expected Symbol ScVal"),
1910        }
1911    }
1912
1913    #[test]
1914    fn test_create_key_with_address() {
1915        let pk = PublicKey::from_string(TEST_ACCOUNT).unwrap();
1916        let uint256 = Uint256(pk.0);
1917        let account_id = AccountId(soroban_rs::xdr::PublicKey::PublicKeyTypeEd25519(uint256));
1918        let sc_address = ScAddress::Account(account_id);
1919
1920        let result = create_contract_data_key("Balance", Some(sc_address.clone()));
1921        assert!(result.is_ok());
1922
1923        match result.unwrap() {
1924            ScVal::Vec(Some(vec)) => {
1925                assert_eq!(vec.0.len(), 2);
1926                match &vec.0[0] {
1927                    ScVal::Symbol(sym) => assert_eq!(sym.to_string(), "Balance"),
1928                    _ => panic!("Expected Symbol as first element"),
1929                }
1930                match &vec.0[1] {
1931                    ScVal::Address(addr) => assert_eq!(addr, &sc_address),
1932                    _ => panic!("Expected Address as second element"),
1933                }
1934            }
1935            _ => panic!("Expected Vec ScVal"),
1936        }
1937    }
1938
1939    #[test]
1940    fn test_create_key_invalid_symbol() {
1941        // Test with symbol that's too long or has invalid characters
1942        let very_long_symbol = "a".repeat(100);
1943        let result = create_contract_data_key(&very_long_symbol, None);
1944        assert!(result.is_err());
1945
1946        match result.unwrap_err() {
1947            StellarTransactionUtilsError::SymbolCreationFailed(_, _) => {}
1948            _ => panic!("Expected SymbolCreationFailed error"),
1949        }
1950    }
1951
1952    #[test]
1953    fn test_create_key_decimals() {
1954        let result = create_contract_data_key("Decimals", None);
1955        assert!(result.is_ok());
1956    }
1957}
1958
1959// ============================================================================
1960// Extract ScVal from Contract Data Tests
1961// ============================================================================
1962
1963#[cfg(test)]
1964mod extract_scval_from_contract_data_tests {
1965    use super::*;
1966    use soroban_rs::stellar_rpc_client::{GetLedgerEntriesResponse, LedgerEntryResult};
1967    use soroban_rs::xdr::{
1968        ContractDataDurability, ContractDataEntry, ExtensionPoint, Hash, LedgerEntry,
1969        LedgerEntryData, LedgerEntryExt, ScSymbol, ScVal, WriteXdr,
1970    };
1971
1972    #[test]
1973    fn test_extract_scval_success() {
1974        let contract_data = ContractDataEntry {
1975            ext: ExtensionPoint::V0,
1976            contract: ScAddress::Contract(ContractId(Hash([0u8; 32]))),
1977            key: ScVal::Symbol(ScSymbol::try_from("test").unwrap()),
1978            durability: ContractDataDurability::Persistent,
1979            val: ScVal::U32(42),
1980        };
1981
1982        let ledger_entry = LedgerEntry {
1983            last_modified_ledger_seq: 100,
1984            data: LedgerEntryData::ContractData(contract_data),
1985            ext: LedgerEntryExt::V0,
1986        };
1987
1988        let xdr = ledger_entry
1989            .data
1990            .to_xdr_base64(soroban_rs::xdr::Limits::none())
1991            .unwrap();
1992
1993        let response = GetLedgerEntriesResponse {
1994            entries: Some(vec![LedgerEntryResult {
1995                key: "test_key".to_string(),
1996                xdr,
1997                last_modified_ledger: 100,
1998                live_until_ledger_seq_ledger_seq: None,
1999            }]),
2000            latest_ledger: 100,
2001        };
2002
2003        let result = extract_scval_from_contract_data(&response, "test");
2004        assert!(result.is_ok());
2005
2006        match result.unwrap() {
2007            ScVal::U32(val) => assert_eq!(val, 42),
2008            _ => panic!("Expected U32 ScVal"),
2009        }
2010    }
2011
2012    #[test]
2013    fn test_extract_scval_no_entries() {
2014        let response = GetLedgerEntriesResponse {
2015            entries: None,
2016            latest_ledger: 100,
2017        };
2018
2019        let result = extract_scval_from_contract_data(&response, "test");
2020        assert!(result.is_err());
2021
2022        match result.unwrap_err() {
2023            StellarTransactionUtilsError::NoEntriesFound(_) => {}
2024            _ => panic!("Expected NoEntriesFound error"),
2025        }
2026    }
2027
2028    #[test]
2029    fn test_extract_scval_empty_entries() {
2030        let response = GetLedgerEntriesResponse {
2031            entries: Some(vec![]),
2032            latest_ledger: 100,
2033        };
2034
2035        let result = extract_scval_from_contract_data(&response, "test");
2036        assert!(result.is_err());
2037
2038        match result.unwrap_err() {
2039            StellarTransactionUtilsError::EmptyEntries(_) => {}
2040            _ => panic!("Expected EmptyEntries error"),
2041        }
2042    }
2043}
2044
2045// ============================================================================
2046// Extract u32 from ScVal Tests
2047// ============================================================================
2048
2049#[cfg(test)]
2050mod extract_u32_from_scval_tests {
2051    use super::*;
2052    use soroban_rs::xdr::{Int128Parts, ScVal, UInt128Parts};
2053
2054    #[test]
2055    fn test_extract_from_u32() {
2056        let val = ScVal::U32(42);
2057        assert_eq!(extract_u32_from_scval(&val, "test"), Some(42));
2058    }
2059
2060    #[test]
2061    fn test_extract_from_i32_positive() {
2062        let val = ScVal::I32(100);
2063        assert_eq!(extract_u32_from_scval(&val, "test"), Some(100));
2064    }
2065
2066    #[test]
2067    fn test_extract_from_i32_negative() {
2068        let val = ScVal::I32(-1);
2069        assert_eq!(extract_u32_from_scval(&val, "test"), None);
2070    }
2071
2072    #[test]
2073    fn test_extract_from_u64() {
2074        let val = ScVal::U64(1000);
2075        assert_eq!(extract_u32_from_scval(&val, "test"), Some(1000));
2076    }
2077
2078    #[test]
2079    fn test_extract_from_u64_overflow() {
2080        let val = ScVal::U64(u64::MAX);
2081        assert_eq!(extract_u32_from_scval(&val, "test"), None);
2082    }
2083
2084    #[test]
2085    fn test_extract_from_i64_positive() {
2086        let val = ScVal::I64(500);
2087        assert_eq!(extract_u32_from_scval(&val, "test"), Some(500));
2088    }
2089
2090    #[test]
2091    fn test_extract_from_i64_negative() {
2092        let val = ScVal::I64(-500);
2093        assert_eq!(extract_u32_from_scval(&val, "test"), None);
2094    }
2095
2096    #[test]
2097    fn test_extract_from_u128_small() {
2098        let val = ScVal::U128(UInt128Parts { hi: 0, lo: 255 });
2099        assert_eq!(extract_u32_from_scval(&val, "test"), Some(255));
2100    }
2101
2102    #[test]
2103    fn test_extract_from_u128_hi_set() {
2104        let val = ScVal::U128(UInt128Parts { hi: 1, lo: 0 });
2105        assert_eq!(extract_u32_from_scval(&val, "test"), None);
2106    }
2107
2108    #[test]
2109    fn test_extract_from_i128_small() {
2110        let val = ScVal::I128(Int128Parts { hi: 0, lo: 123 });
2111        assert_eq!(extract_u32_from_scval(&val, "test"), Some(123));
2112    }
2113
2114    #[test]
2115    fn test_extract_from_unsupported_type() {
2116        let val = ScVal::Bool(true);
2117        assert_eq!(extract_u32_from_scval(&val, "test"), None);
2118    }
2119}
2120
2121// ============================================================================
2122// Amount to UI Amount Tests
2123// ============================================================================
2124
2125#[cfg(test)]
2126mod amount_to_ui_amount_tests {
2127    use super::*;
2128
2129    #[test]
2130    fn test_zero_decimals() {
2131        assert_eq!(amount_to_ui_amount(100, 0), "100");
2132        assert_eq!(amount_to_ui_amount(0, 0), "0");
2133    }
2134
2135    #[test]
2136    fn test_with_decimals_no_padding() {
2137        assert_eq!(amount_to_ui_amount(1000000, 6), "1");
2138        assert_eq!(amount_to_ui_amount(1500000, 6), "1.5");
2139        assert_eq!(amount_to_ui_amount(1234567, 6), "1.234567");
2140    }
2141
2142    #[test]
2143    fn test_with_decimals_needs_padding() {
2144        assert_eq!(amount_to_ui_amount(1, 6), "0.000001");
2145        assert_eq!(amount_to_ui_amount(100, 6), "0.0001");
2146        assert_eq!(amount_to_ui_amount(1000, 3), "1");
2147    }
2148
2149    #[test]
2150    fn test_trailing_zeros_removed() {
2151        assert_eq!(amount_to_ui_amount(1000000, 6), "1");
2152        assert_eq!(amount_to_ui_amount(1500000, 7), "0.15");
2153        assert_eq!(amount_to_ui_amount(10000000, 7), "1");
2154    }
2155
2156    #[test]
2157    fn test_zero_amount() {
2158        assert_eq!(amount_to_ui_amount(0, 6), "0");
2159        assert_eq!(amount_to_ui_amount(0, 0), "0");
2160    }
2161
2162    #[test]
2163    fn test_xlm_7_decimals() {
2164        assert_eq!(amount_to_ui_amount(10000000, 7), "1");
2165        assert_eq!(amount_to_ui_amount(15000000, 7), "1.5");
2166        assert_eq!(amount_to_ui_amount(100, 7), "0.00001");
2167    }
2168}
2169
2170// // ============================================================================
2171// // Count Operations Tests
2172// // ============================================================================
2173
2174// #[cfg(test)]
2175#[cfg(test)]
2176mod count_operations_tests {
2177    use super::*;
2178    use soroban_rs::xdr::{
2179        Limits, MuxedAccount, Operation, OperationBody, PaymentOp, TransactionV1Envelope, Uint256,
2180        WriteXdr,
2181    };
2182
2183    #[test]
2184    fn test_count_operations_from_xdr() {
2185        use soroban_rs::xdr::{Memo, Preconditions, SequenceNumber, Transaction, TransactionExt};
2186
2187        // Create two payment operations
2188        let payment_op = Operation {
2189            source_account: None,
2190            body: OperationBody::Payment(PaymentOp {
2191                destination: MuxedAccount::Ed25519(Uint256([1u8; 32])),
2192                asset: Asset::Native,
2193                amount: 100,
2194            }),
2195        };
2196
2197        let operations = vec![payment_op.clone(), payment_op].try_into().unwrap();
2198
2199        let tx = Transaction {
2200            source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
2201            fee: 100,
2202            seq_num: SequenceNumber(1),
2203            cond: Preconditions::None,
2204            memo: Memo::None,
2205            operations,
2206            ext: TransactionExt::V0,
2207        };
2208
2209        let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
2210            tx,
2211            signatures: vec![].try_into().unwrap(),
2212        });
2213
2214        let xdr = envelope.to_xdr_base64(Limits::none()).unwrap();
2215        let count = count_operations_from_xdr(&xdr).unwrap();
2216
2217        assert_eq!(count, 2);
2218    }
2219
2220    #[test]
2221    fn test_count_operations_invalid_xdr() {
2222        let result = count_operations_from_xdr("invalid_xdr");
2223        assert!(result.is_err());
2224
2225        match result.unwrap_err() {
2226            StellarTransactionUtilsError::XdrParseFailed(_) => {}
2227            _ => panic!("Expected XdrParseFailed error"),
2228        }
2229    }
2230}
2231
2232// ============================================================================
2233// Estimate Base Fee Tests
2234// ============================================================================
2235
2236#[cfg(test)]
2237mod estimate_base_fee_tests {
2238    use super::*;
2239
2240    #[test]
2241    fn test_single_operation() {
2242        assert_eq!(estimate_base_fee(1), 100);
2243    }
2244
2245    #[test]
2246    fn test_multiple_operations() {
2247        assert_eq!(estimate_base_fee(5), 500);
2248        assert_eq!(estimate_base_fee(10), 1000);
2249    }
2250
2251    #[test]
2252    fn test_zero_operations() {
2253        // Should return fee for at least 1 operation
2254        assert_eq!(estimate_base_fee(0), 100);
2255    }
2256
2257    #[test]
2258    fn test_large_number_of_operations() {
2259        assert_eq!(estimate_base_fee(100), 10000);
2260    }
2261}
2262
2263// ============================================================================
2264// Create Fee Payment Operation Tests
2265// ============================================================================
2266
2267#[cfg(test)]
2268mod create_fee_payment_operation_tests {
2269    use super::*;
2270    use crate::domain::transaction::stellar::test_helpers::TEST_PK as TEST_ACCOUNT;
2271
2272    #[test]
2273    fn test_create_native_payment() {
2274        let result = create_fee_payment_operation(TEST_ACCOUNT, "native", 1000);
2275        assert!(result.is_ok());
2276
2277        match result.unwrap() {
2278            OperationSpec::Payment {
2279                destination,
2280                amount,
2281                asset,
2282            } => {
2283                assert_eq!(destination, TEST_ACCOUNT);
2284                assert_eq!(amount, 1000);
2285                assert!(matches!(asset, AssetSpec::Native));
2286            }
2287            _ => panic!("Expected Payment operation"),
2288        }
2289    }
2290
2291    #[test]
2292    fn test_create_credit4_payment() {
2293        let result = create_fee_payment_operation(
2294            TEST_ACCOUNT,
2295            "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
2296            5000,
2297        );
2298        assert!(result.is_ok());
2299
2300        match result.unwrap() {
2301            OperationSpec::Payment {
2302                destination,
2303                amount,
2304                asset,
2305            } => {
2306                assert_eq!(destination, TEST_ACCOUNT);
2307                assert_eq!(amount, 5000);
2308                match asset {
2309                    AssetSpec::Credit4 { code, issuer } => {
2310                        assert_eq!(code, "USDC");
2311                        assert_eq!(
2312                            issuer,
2313                            "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
2314                        );
2315                    }
2316                    _ => panic!("Expected Credit4 asset"),
2317                }
2318            }
2319            _ => panic!("Expected Payment operation"),
2320        }
2321    }
2322
2323    #[test]
2324    fn test_create_credit12_payment() {
2325        let result = create_fee_payment_operation(
2326            TEST_ACCOUNT,
2327            "LONGASSETNAM:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
2328            2000,
2329        );
2330        assert!(result.is_ok());
2331
2332        match result.unwrap() {
2333            OperationSpec::Payment {
2334                destination,
2335                amount,
2336                asset,
2337            } => {
2338                assert_eq!(destination, TEST_ACCOUNT);
2339                assert_eq!(amount, 2000);
2340                match asset {
2341                    AssetSpec::Credit12 { code, issuer } => {
2342                        assert_eq!(code, "LONGASSETNAM");
2343                        assert_eq!(
2344                            issuer,
2345                            "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
2346                        );
2347                    }
2348                    _ => panic!("Expected Credit12 asset"),
2349                }
2350            }
2351            _ => panic!("Expected Payment operation"),
2352        }
2353    }
2354
2355    #[test]
2356    fn test_create_payment_empty_asset() {
2357        let result = create_fee_payment_operation(TEST_ACCOUNT, "", 1000);
2358        assert!(result.is_ok());
2359
2360        match result.unwrap() {
2361            OperationSpec::Payment { asset, .. } => {
2362                assert!(matches!(asset, AssetSpec::Native));
2363            }
2364            _ => panic!("Expected Payment operation"),
2365        }
2366    }
2367
2368    #[test]
2369    fn test_create_payment_invalid_format() {
2370        let result = create_fee_payment_operation(TEST_ACCOUNT, "INVALID_FORMAT", 1000);
2371        assert!(result.is_err());
2372
2373        match result.unwrap_err() {
2374            StellarTransactionUtilsError::InvalidAssetFormat(_) => {}
2375            _ => panic!("Expected InvalidAssetFormat error"),
2376        }
2377    }
2378
2379    #[test]
2380    fn test_create_payment_asset_code_too_long() {
2381        let result = create_fee_payment_operation(
2382            TEST_ACCOUNT,
2383            "VERYLONGASSETCODE:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
2384            1000,
2385        );
2386        assert!(result.is_err());
2387
2388        match result.unwrap_err() {
2389            StellarTransactionUtilsError::AssetCodeTooLong(max_len, _) => {
2390                assert_eq!(max_len, 12);
2391            }
2392            _ => panic!("Expected AssetCodeTooLong error"),
2393        }
2394    }
2395}
2396
2397#[cfg(test)]
2398mod parse_account_id_tests {
2399    use super::*;
2400    use crate::domain::transaction::stellar::test_helpers::TEST_PK;
2401
2402    #[test]
2403    fn test_parse_account_id_valid() {
2404        let result = parse_account_id(TEST_PK);
2405        assert!(result.is_ok());
2406
2407        let account_id = result.unwrap();
2408        match account_id.0 {
2409            soroban_rs::xdr::PublicKey::PublicKeyTypeEd25519(_) => {}
2410        }
2411    }
2412
2413    #[test]
2414    fn test_parse_account_id_invalid() {
2415        let result = parse_account_id("INVALID_ADDRESS");
2416        assert!(result.is_err());
2417
2418        match result.unwrap_err() {
2419            StellarTransactionUtilsError::InvalidAccountAddress(addr, _) => {
2420                assert_eq!(addr, "INVALID_ADDRESS");
2421            }
2422            _ => panic!("Expected InvalidAccountAddress error"),
2423        }
2424    }
2425
2426    #[test]
2427    fn test_parse_account_id_empty() {
2428        let result = parse_account_id("");
2429        assert!(result.is_err());
2430    }
2431
2432    #[test]
2433    fn test_parse_account_id_wrong_prefix() {
2434        // Contract address instead of account
2435        let result = parse_account_id("CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM");
2436        assert!(result.is_err());
2437    }
2438}
2439
2440#[cfg(test)]
2441mod parse_transaction_and_count_operations_tests {
2442    use super::*;
2443    use crate::domain::transaction::stellar::test_helpers::{
2444        create_native_payment_operation, create_xdr_with_operations, TEST_PK, TEST_PK_2,
2445    };
2446    use serde_json::json;
2447
2448    fn create_test_xdr_with_operations(num_ops: usize) -> String {
2449        let payment_op = create_native_payment_operation(TEST_PK_2, 100);
2450        let operations = vec![payment_op; num_ops];
2451        create_xdr_with_operations(TEST_PK, operations, false)
2452    }
2453
2454    #[test]
2455    fn test_parse_xdr_string() {
2456        let xdr = create_test_xdr_with_operations(2);
2457        let json_value = json!(xdr);
2458
2459        let result = parse_transaction_and_count_operations(&json_value);
2460        assert!(result.is_ok());
2461        assert_eq!(result.unwrap(), 2);
2462    }
2463
2464    #[test]
2465    fn test_parse_operations_array() {
2466        let json_value = json!([
2467            {"type": "payment"},
2468            {"type": "payment"},
2469            {"type": "payment"}
2470        ]);
2471
2472        let result = parse_transaction_and_count_operations(&json_value);
2473        assert!(result.is_ok());
2474        assert_eq!(result.unwrap(), 3);
2475    }
2476
2477    #[test]
2478    fn test_parse_object_with_operations() {
2479        let json_value = json!({
2480            "operations": [
2481                {"type": "payment"},
2482                {"type": "payment"}
2483            ]
2484        });
2485
2486        let result = parse_transaction_and_count_operations(&json_value);
2487        assert!(result.is_ok());
2488        assert_eq!(result.unwrap(), 2);
2489    }
2490
2491    #[test]
2492    fn test_parse_object_with_transaction_xdr() {
2493        let xdr = create_test_xdr_with_operations(3);
2494        let json_value = json!({
2495            "transaction_xdr": xdr
2496        });
2497
2498        let result = parse_transaction_and_count_operations(&json_value);
2499        assert!(result.is_ok());
2500        assert_eq!(result.unwrap(), 3);
2501    }
2502
2503    #[test]
2504    fn test_parse_invalid_xdr() {
2505        let json_value = json!("INVALID_XDR");
2506
2507        let result = parse_transaction_and_count_operations(&json_value);
2508        assert!(result.is_err());
2509
2510        match result.unwrap_err() {
2511            StellarTransactionUtilsError::XdrParseFailed(_) => {}
2512            _ => panic!("Expected XdrParseFailed error"),
2513        }
2514    }
2515
2516    #[test]
2517    fn test_parse_invalid_format() {
2518        let json_value = json!(123);
2519
2520        let result = parse_transaction_and_count_operations(&json_value);
2521        assert!(result.is_err());
2522
2523        match result.unwrap_err() {
2524            StellarTransactionUtilsError::InvalidTransactionFormat(_) => {}
2525            _ => panic!("Expected InvalidTransactionFormat error"),
2526        }
2527    }
2528
2529    #[test]
2530    fn test_parse_empty_operations() {
2531        let json_value = json!([]);
2532
2533        let result = parse_transaction_and_count_operations(&json_value);
2534        assert!(result.is_ok());
2535        assert_eq!(result.unwrap(), 0);
2536    }
2537}
2538
2539#[cfg(test)]
2540mod parse_transaction_envelope_tests {
2541    use super::*;
2542    use crate::domain::transaction::stellar::test_helpers::{
2543        create_unsigned_xdr, TEST_PK, TEST_PK_2,
2544    };
2545    use serde_json::json;
2546
2547    fn create_test_xdr() -> String {
2548        create_unsigned_xdr(TEST_PK, TEST_PK_2)
2549    }
2550
2551    #[test]
2552    fn test_parse_xdr_string() {
2553        let xdr = create_test_xdr();
2554        let json_value = json!(xdr);
2555
2556        let result = parse_transaction_envelope(&json_value);
2557        assert!(result.is_ok());
2558
2559        match result.unwrap() {
2560            TransactionEnvelope::Tx(_) => {}
2561            _ => panic!("Expected Tx envelope"),
2562        }
2563    }
2564
2565    #[test]
2566    fn test_parse_object_with_transaction_xdr() {
2567        let xdr = create_test_xdr();
2568        let json_value = json!({
2569            "transaction_xdr": xdr
2570        });
2571
2572        let result = parse_transaction_envelope(&json_value);
2573        assert!(result.is_ok());
2574
2575        match result.unwrap() {
2576            TransactionEnvelope::Tx(_) => {}
2577            _ => panic!("Expected Tx envelope"),
2578        }
2579    }
2580
2581    #[test]
2582    fn test_parse_invalid_xdr() {
2583        let json_value = json!("INVALID_XDR");
2584
2585        let result = parse_transaction_envelope(&json_value);
2586        assert!(result.is_err());
2587
2588        match result.unwrap_err() {
2589            StellarTransactionUtilsError::XdrParseFailed(_) => {}
2590            _ => panic!("Expected XdrParseFailed error"),
2591        }
2592    }
2593
2594    #[test]
2595    fn test_parse_invalid_format() {
2596        let json_value = json!(123);
2597
2598        let result = parse_transaction_envelope(&json_value);
2599        assert!(result.is_err());
2600
2601        match result.unwrap_err() {
2602            StellarTransactionUtilsError::InvalidTransactionFormat(_) => {}
2603            _ => panic!("Expected InvalidTransactionFormat error"),
2604        }
2605    }
2606
2607    #[test]
2608    fn test_parse_object_without_xdr() {
2609        let json_value = json!({
2610            "some_field": "value"
2611        });
2612
2613        let result = parse_transaction_envelope(&json_value);
2614        assert!(result.is_err());
2615
2616        match result.unwrap_err() {
2617            StellarTransactionUtilsError::InvalidTransactionFormat(_) => {}
2618            _ => panic!("Expected InvalidTransactionFormat error"),
2619        }
2620    }
2621}
2622
2623#[cfg(test)]
2624mod add_operation_to_envelope_tests {
2625    use super::*;
2626    use soroban_rs::xdr::{
2627        Memo, MuxedAccount, Operation, OperationBody, PaymentOp, Preconditions, SequenceNumber,
2628        Transaction, TransactionExt, TransactionV0, TransactionV0Envelope, TransactionV1Envelope,
2629        Uint256,
2630    };
2631
2632    fn create_payment_op() -> Operation {
2633        Operation {
2634            source_account: None,
2635            body: OperationBody::Payment(PaymentOp {
2636                destination: MuxedAccount::Ed25519(Uint256([1u8; 32])),
2637                asset: Asset::Native,
2638                amount: 100,
2639            }),
2640        }
2641    }
2642
2643    #[test]
2644    fn test_add_operation_to_tx_v0() {
2645        let payment_op = create_payment_op();
2646        let operations = vec![payment_op.clone()].try_into().unwrap();
2647
2648        let tx = TransactionV0 {
2649            source_account_ed25519: Uint256([0u8; 32]),
2650            fee: 100,
2651            seq_num: SequenceNumber(1),
2652            time_bounds: None,
2653            memo: Memo::None,
2654            operations,
2655            ext: soroban_rs::xdr::TransactionV0Ext::V0,
2656        };
2657
2658        let mut envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
2659            tx,
2660            signatures: vec![].try_into().unwrap(),
2661        });
2662
2663        let new_op = create_payment_op();
2664        let result = add_operation_to_envelope(&mut envelope, new_op);
2665
2666        assert!(result.is_ok());
2667
2668        match envelope {
2669            TransactionEnvelope::TxV0(e) => {
2670                assert_eq!(e.tx.operations.len(), 2);
2671                assert_eq!(e.tx.fee, 200); // 100 stroops per operation
2672            }
2673            _ => panic!("Expected TxV0 envelope"),
2674        }
2675    }
2676
2677    #[test]
2678    fn test_add_operation_to_tx_v1() {
2679        let payment_op = create_payment_op();
2680        let operations = vec![payment_op.clone()].try_into().unwrap();
2681
2682        let tx = Transaction {
2683            source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
2684            fee: 100,
2685            seq_num: SequenceNumber(1),
2686            cond: Preconditions::None,
2687            memo: Memo::None,
2688            operations,
2689            ext: TransactionExt::V0,
2690        };
2691
2692        let mut envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
2693            tx,
2694            signatures: vec![].try_into().unwrap(),
2695        });
2696
2697        let new_op = create_payment_op();
2698        let result = add_operation_to_envelope(&mut envelope, new_op);
2699
2700        assert!(result.is_ok());
2701
2702        match envelope {
2703            TransactionEnvelope::Tx(e) => {
2704                assert_eq!(e.tx.operations.len(), 2);
2705                assert_eq!(e.tx.fee, 200); // 100 stroops per operation
2706            }
2707            _ => panic!("Expected Tx envelope"),
2708        }
2709    }
2710
2711    #[test]
2712    fn test_add_operation_to_fee_bump_fails() {
2713        // Create a simple inner transaction
2714        let payment_op = create_payment_op();
2715        let operations = vec![payment_op].try_into().unwrap();
2716
2717        let tx = Transaction {
2718            source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
2719            fee: 100,
2720            seq_num: SequenceNumber(1),
2721            cond: Preconditions::None,
2722            memo: Memo::None,
2723            operations,
2724            ext: TransactionExt::V0,
2725        };
2726
2727        let inner_envelope = TransactionV1Envelope {
2728            tx,
2729            signatures: vec![].try_into().unwrap(),
2730        };
2731
2732        let inner_tx = soroban_rs::xdr::FeeBumpTransactionInnerTx::Tx(inner_envelope);
2733
2734        let fee_bump_tx = soroban_rs::xdr::FeeBumpTransaction {
2735            fee_source: MuxedAccount::Ed25519(Uint256([2u8; 32])),
2736            fee: 200,
2737            inner_tx,
2738            ext: soroban_rs::xdr::FeeBumpTransactionExt::V0,
2739        };
2740
2741        let mut envelope =
2742            TransactionEnvelope::TxFeeBump(soroban_rs::xdr::FeeBumpTransactionEnvelope {
2743                tx: fee_bump_tx,
2744                signatures: vec![].try_into().unwrap(),
2745            });
2746
2747        let new_op = create_payment_op();
2748        let result = add_operation_to_envelope(&mut envelope, new_op);
2749
2750        assert!(result.is_err());
2751
2752        match result.unwrap_err() {
2753            StellarTransactionUtilsError::CannotModifyFeeBump => {}
2754            _ => panic!("Expected CannotModifyFeeBump error"),
2755        }
2756    }
2757}
2758
2759#[cfg(test)]
2760mod extract_time_bounds_tests {
2761    use super::*;
2762    use soroban_rs::xdr::{
2763        Memo, MuxedAccount, Operation, OperationBody, PaymentOp, Preconditions, SequenceNumber,
2764        TimeBounds, TimePoint, Transaction, TransactionExt, TransactionV0, TransactionV0Envelope,
2765        TransactionV1Envelope, Uint256,
2766    };
2767
2768    fn create_payment_op() -> Operation {
2769        Operation {
2770            source_account: None,
2771            body: OperationBody::Payment(PaymentOp {
2772                destination: MuxedAccount::Ed25519(Uint256([1u8; 32])),
2773                asset: Asset::Native,
2774                amount: 100,
2775            }),
2776        }
2777    }
2778
2779    #[test]
2780    fn test_extract_time_bounds_from_tx_v0_with_bounds() {
2781        let payment_op = create_payment_op();
2782        let operations = vec![payment_op].try_into().unwrap();
2783
2784        let time_bounds = TimeBounds {
2785            min_time: TimePoint(0),
2786            max_time: TimePoint(1000),
2787        };
2788
2789        let tx = TransactionV0 {
2790            source_account_ed25519: Uint256([0u8; 32]),
2791            fee: 100,
2792            seq_num: SequenceNumber(1),
2793            time_bounds: Some(time_bounds.clone()),
2794            memo: Memo::None,
2795            operations,
2796            ext: soroban_rs::xdr::TransactionV0Ext::V0,
2797        };
2798
2799        let envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
2800            tx,
2801            signatures: vec![].try_into().unwrap(),
2802        });
2803
2804        let result = extract_time_bounds(&envelope);
2805        assert!(result.is_some());
2806
2807        let bounds = result.unwrap();
2808        assert_eq!(bounds.min_time.0, 0);
2809        assert_eq!(bounds.max_time.0, 1000);
2810    }
2811
2812    #[test]
2813    fn test_extract_time_bounds_from_tx_v0_without_bounds() {
2814        let payment_op = create_payment_op();
2815        let operations = vec![payment_op].try_into().unwrap();
2816
2817        let tx = TransactionV0 {
2818            source_account_ed25519: Uint256([0u8; 32]),
2819            fee: 100,
2820            seq_num: SequenceNumber(1),
2821            time_bounds: None,
2822            memo: Memo::None,
2823            operations,
2824            ext: soroban_rs::xdr::TransactionV0Ext::V0,
2825        };
2826
2827        let envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
2828            tx,
2829            signatures: vec![].try_into().unwrap(),
2830        });
2831
2832        let result = extract_time_bounds(&envelope);
2833        assert!(result.is_none());
2834    }
2835
2836    #[test]
2837    fn test_extract_time_bounds_from_tx_v1_with_time_precondition() {
2838        let payment_op = create_payment_op();
2839        let operations = vec![payment_op].try_into().unwrap();
2840
2841        let time_bounds = TimeBounds {
2842            min_time: TimePoint(0),
2843            max_time: TimePoint(2000),
2844        };
2845
2846        let tx = Transaction {
2847            source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
2848            fee: 100,
2849            seq_num: SequenceNumber(1),
2850            cond: Preconditions::Time(time_bounds.clone()),
2851            memo: Memo::None,
2852            operations,
2853            ext: TransactionExt::V0,
2854        };
2855
2856        let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
2857            tx,
2858            signatures: vec![].try_into().unwrap(),
2859        });
2860
2861        let result = extract_time_bounds(&envelope);
2862        assert!(result.is_some());
2863
2864        let bounds = result.unwrap();
2865        assert_eq!(bounds.min_time.0, 0);
2866        assert_eq!(bounds.max_time.0, 2000);
2867    }
2868
2869    #[test]
2870    fn test_extract_time_bounds_from_tx_v1_without_time_precondition() {
2871        let payment_op = create_payment_op();
2872        let operations = vec![payment_op].try_into().unwrap();
2873
2874        let tx = Transaction {
2875            source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
2876            fee: 100,
2877            seq_num: SequenceNumber(1),
2878            cond: Preconditions::None,
2879            memo: Memo::None,
2880            operations,
2881            ext: TransactionExt::V0,
2882        };
2883
2884        let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
2885            tx,
2886            signatures: vec![].try_into().unwrap(),
2887        });
2888
2889        let result = extract_time_bounds(&envelope);
2890        assert!(result.is_none());
2891    }
2892
2893    #[test]
2894    fn test_extract_time_bounds_from_fee_bump() {
2895        // Create inner transaction with time bounds
2896        let payment_op = create_payment_op();
2897        let operations = vec![payment_op].try_into().unwrap();
2898
2899        let time_bounds = TimeBounds {
2900            min_time: TimePoint(0),
2901            max_time: TimePoint(3000),
2902        };
2903
2904        let tx = Transaction {
2905            source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
2906            fee: 100,
2907            seq_num: SequenceNumber(1),
2908            cond: Preconditions::Time(time_bounds.clone()),
2909            memo: Memo::None,
2910            operations,
2911            ext: TransactionExt::V0,
2912        };
2913
2914        let inner_envelope = TransactionV1Envelope {
2915            tx,
2916            signatures: vec![].try_into().unwrap(),
2917        };
2918
2919        let inner_tx = soroban_rs::xdr::FeeBumpTransactionInnerTx::Tx(inner_envelope);
2920
2921        let fee_bump_tx = soroban_rs::xdr::FeeBumpTransaction {
2922            fee_source: MuxedAccount::Ed25519(Uint256([2u8; 32])),
2923            fee: 200,
2924            inner_tx,
2925            ext: soroban_rs::xdr::FeeBumpTransactionExt::V0,
2926        };
2927
2928        let envelope =
2929            TransactionEnvelope::TxFeeBump(soroban_rs::xdr::FeeBumpTransactionEnvelope {
2930                tx: fee_bump_tx,
2931                signatures: vec![].try_into().unwrap(),
2932            });
2933
2934        let result = extract_time_bounds(&envelope);
2935        assert!(result.is_some());
2936
2937        let bounds = result.unwrap();
2938        assert_eq!(bounds.min_time.0, 0);
2939        assert_eq!(bounds.max_time.0, 3000);
2940    }
2941}
2942
2943#[cfg(test)]
2944mod set_time_bounds_tests {
2945    use super::*;
2946    use chrono::Utc;
2947    use soroban_rs::xdr::{
2948        Memo, MuxedAccount, Operation, OperationBody, PaymentOp, Preconditions, SequenceNumber,
2949        TimeBounds, TimePoint, Transaction, TransactionExt, TransactionV0, TransactionV0Envelope,
2950        TransactionV1Envelope, Uint256,
2951    };
2952
2953    fn create_payment_op() -> Operation {
2954        Operation {
2955            source_account: None,
2956            body: OperationBody::Payment(PaymentOp {
2957                destination: MuxedAccount::Ed25519(Uint256([1u8; 32])),
2958                asset: Asset::Native,
2959                amount: 100,
2960            }),
2961        }
2962    }
2963
2964    #[test]
2965    fn test_set_time_bounds_on_tx_v0() {
2966        let payment_op = create_payment_op();
2967        let operations = vec![payment_op].try_into().unwrap();
2968
2969        let tx = TransactionV0 {
2970            source_account_ed25519: Uint256([0u8; 32]),
2971            fee: 100,
2972            seq_num: SequenceNumber(1),
2973            time_bounds: None,
2974            memo: Memo::None,
2975            operations,
2976            ext: soroban_rs::xdr::TransactionV0Ext::V0,
2977        };
2978
2979        let mut envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
2980            tx,
2981            signatures: vec![].try_into().unwrap(),
2982        });
2983
2984        let valid_until = Utc::now() + chrono::Duration::seconds(300);
2985        let result = set_time_bounds(&mut envelope, valid_until);
2986
2987        assert!(result.is_ok());
2988
2989        match envelope {
2990            TransactionEnvelope::TxV0(e) => {
2991                assert!(e.tx.time_bounds.is_some());
2992                let bounds = e.tx.time_bounds.unwrap();
2993                assert_eq!(bounds.min_time.0, 0);
2994                assert_eq!(bounds.max_time.0, valid_until.timestamp() as u64);
2995            }
2996            _ => panic!("Expected TxV0 envelope"),
2997        }
2998    }
2999
3000    #[test]
3001    fn test_set_time_bounds_on_tx_v1() {
3002        let payment_op = create_payment_op();
3003        let operations = vec![payment_op].try_into().unwrap();
3004
3005        let tx = Transaction {
3006            source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
3007            fee: 100,
3008            seq_num: SequenceNumber(1),
3009            cond: Preconditions::None,
3010            memo: Memo::None,
3011            operations,
3012            ext: TransactionExt::V0,
3013        };
3014
3015        let mut envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
3016            tx,
3017            signatures: vec![].try_into().unwrap(),
3018        });
3019
3020        let valid_until = Utc::now() + chrono::Duration::seconds(300);
3021        let result = set_time_bounds(&mut envelope, valid_until);
3022
3023        assert!(result.is_ok());
3024
3025        match envelope {
3026            TransactionEnvelope::Tx(e) => match e.tx.cond {
3027                Preconditions::Time(bounds) => {
3028                    assert_eq!(bounds.min_time.0, 0);
3029                    assert_eq!(bounds.max_time.0, valid_until.timestamp() as u64);
3030                }
3031                _ => panic!("Expected Time precondition"),
3032            },
3033            _ => panic!("Expected Tx envelope"),
3034        }
3035    }
3036
3037    #[test]
3038    fn test_set_time_bounds_on_fee_bump_fails() {
3039        // Create a simple inner transaction
3040        let payment_op = create_payment_op();
3041        let operations = vec![payment_op].try_into().unwrap();
3042
3043        let tx = Transaction {
3044            source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
3045            fee: 100,
3046            seq_num: SequenceNumber(1),
3047            cond: Preconditions::None,
3048            memo: Memo::None,
3049            operations,
3050            ext: TransactionExt::V0,
3051        };
3052
3053        let inner_envelope = TransactionV1Envelope {
3054            tx,
3055            signatures: vec![].try_into().unwrap(),
3056        };
3057
3058        let inner_tx = soroban_rs::xdr::FeeBumpTransactionInnerTx::Tx(inner_envelope);
3059
3060        let fee_bump_tx = soroban_rs::xdr::FeeBumpTransaction {
3061            fee_source: MuxedAccount::Ed25519(Uint256([2u8; 32])),
3062            fee: 200,
3063            inner_tx,
3064            ext: soroban_rs::xdr::FeeBumpTransactionExt::V0,
3065        };
3066
3067        let mut envelope =
3068            TransactionEnvelope::TxFeeBump(soroban_rs::xdr::FeeBumpTransactionEnvelope {
3069                tx: fee_bump_tx,
3070                signatures: vec![].try_into().unwrap(),
3071            });
3072
3073        let valid_until = Utc::now() + chrono::Duration::seconds(300);
3074        let result = set_time_bounds(&mut envelope, valid_until);
3075
3076        assert!(result.is_err());
3077
3078        match result.unwrap_err() {
3079            StellarTransactionUtilsError::CannotSetTimeBoundsOnFeeBump => {}
3080            _ => panic!("Expected CannotSetTimeBoundsOnFeeBump error"),
3081        }
3082    }
3083
3084    #[test]
3085    fn test_set_time_bounds_replaces_existing() {
3086        let payment_op = create_payment_op();
3087        let operations = vec![payment_op].try_into().unwrap();
3088
3089        let old_time_bounds = TimeBounds {
3090            min_time: TimePoint(100),
3091            max_time: TimePoint(1000),
3092        };
3093
3094        let tx = TransactionV0 {
3095            source_account_ed25519: Uint256([0u8; 32]),
3096            fee: 100,
3097            seq_num: SequenceNumber(1),
3098            time_bounds: Some(old_time_bounds),
3099            memo: Memo::None,
3100            operations,
3101            ext: soroban_rs::xdr::TransactionV0Ext::V0,
3102        };
3103
3104        let mut envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
3105            tx,
3106            signatures: vec![].try_into().unwrap(),
3107        });
3108
3109        let valid_until = Utc::now() + chrono::Duration::seconds(300);
3110        let result = set_time_bounds(&mut envelope, valid_until);
3111
3112        assert!(result.is_ok());
3113
3114        match envelope {
3115            TransactionEnvelope::TxV0(e) => {
3116                assert!(e.tx.time_bounds.is_some());
3117                let bounds = e.tx.time_bounds.unwrap();
3118                // Should replace with new bounds (min_time = 0, not 100)
3119                assert_eq!(bounds.min_time.0, 0);
3120                assert_eq!(bounds.max_time.0, valid_until.timestamp() as u64);
3121            }
3122            _ => panic!("Expected TxV0 envelope"),
3123        }
3124    }
3125}
3126
3127// ============================================================================
3128// From<StellarTransactionUtilsError> for RelayerError Tests
3129// ============================================================================
3130
3131#[cfg(test)]
3132mod stellar_transaction_utils_error_conversion_tests {
3133    use super::*;
3134
3135    #[test]
3136    fn test_v0_transactions_not_supported_converts_to_validation_error() {
3137        let err = StellarTransactionUtilsError::V0TransactionsNotSupported;
3138        let relayer_err: RelayerError = err.into();
3139        match relayer_err {
3140            RelayerError::ValidationError(msg) => {
3141                assert_eq!(msg, "V0 transactions are not supported");
3142            }
3143            _ => panic!("Expected ValidationError"),
3144        }
3145    }
3146
3147    #[test]
3148    fn test_cannot_update_sequence_on_fee_bump_converts_to_validation_error() {
3149        let err = StellarTransactionUtilsError::CannotUpdateSequenceOnFeeBump;
3150        let relayer_err: RelayerError = err.into();
3151        match relayer_err {
3152            RelayerError::ValidationError(msg) => {
3153                assert_eq!(msg, "Cannot update sequence number on fee bump transaction");
3154            }
3155            _ => panic!("Expected ValidationError"),
3156        }
3157    }
3158
3159    #[test]
3160    fn test_cannot_set_time_bounds_on_fee_bump_converts_to_validation_error() {
3161        let err = StellarTransactionUtilsError::CannotSetTimeBoundsOnFeeBump;
3162        let relayer_err: RelayerError = err.into();
3163        match relayer_err {
3164            RelayerError::ValidationError(msg) => {
3165                assert_eq!(msg, "Cannot set time bounds on fee-bump transactions");
3166            }
3167            _ => panic!("Expected ValidationError"),
3168        }
3169    }
3170
3171    #[test]
3172    fn test_invalid_transaction_format_converts_to_validation_error() {
3173        let err = StellarTransactionUtilsError::InvalidTransactionFormat("bad format".to_string());
3174        let relayer_err: RelayerError = err.into();
3175        match relayer_err {
3176            RelayerError::ValidationError(msg) => {
3177                assert_eq!(msg, "bad format");
3178            }
3179            _ => panic!("Expected ValidationError"),
3180        }
3181    }
3182
3183    #[test]
3184    fn test_cannot_modify_fee_bump_converts_to_validation_error() {
3185        let err = StellarTransactionUtilsError::CannotModifyFeeBump;
3186        let relayer_err: RelayerError = err.into();
3187        match relayer_err {
3188            RelayerError::ValidationError(msg) => {
3189                assert_eq!(msg, "Cannot add operations to fee-bump transactions");
3190            }
3191            _ => panic!("Expected ValidationError"),
3192        }
3193    }
3194
3195    #[test]
3196    fn test_too_many_operations_converts_to_validation_error() {
3197        let err = StellarTransactionUtilsError::TooManyOperations(100);
3198        let relayer_err: RelayerError = err.into();
3199        match relayer_err {
3200            RelayerError::ValidationError(msg) => {
3201                assert!(msg.contains("Too many operations"));
3202                assert!(msg.contains("100"));
3203            }
3204            _ => panic!("Expected ValidationError"),
3205        }
3206    }
3207
3208    #[test]
3209    fn test_sequence_overflow_converts_to_internal_error() {
3210        let err = StellarTransactionUtilsError::SequenceOverflow("overflow msg".to_string());
3211        let relayer_err: RelayerError = err.into();
3212        match relayer_err {
3213            RelayerError::Internal(msg) => {
3214                assert_eq!(msg, "overflow msg");
3215            }
3216            _ => panic!("Expected Internal error"),
3217        }
3218    }
3219
3220    #[test]
3221    fn test_simulation_no_results_converts_to_internal_error() {
3222        let err = StellarTransactionUtilsError::SimulationNoResults;
3223        let relayer_err: RelayerError = err.into();
3224        match relayer_err {
3225            RelayerError::Internal(msg) => {
3226                assert!(msg.contains("no results"));
3227            }
3228            _ => panic!("Expected Internal error"),
3229        }
3230    }
3231
3232    #[test]
3233    fn test_asset_code_too_long_converts_to_validation_error() {
3234        let err =
3235            StellarTransactionUtilsError::AssetCodeTooLong(12, "VERYLONGASSETCODE".to_string());
3236        let relayer_err: RelayerError = err.into();
3237        match relayer_err {
3238            RelayerError::ValidationError(msg) => {
3239                assert!(msg.contains("Asset code too long"));
3240                assert!(msg.contains("12"));
3241            }
3242            _ => panic!("Expected ValidationError"),
3243        }
3244    }
3245
3246    #[test]
3247    fn test_invalid_asset_format_converts_to_validation_error() {
3248        let err = StellarTransactionUtilsError::InvalidAssetFormat("bad asset".to_string());
3249        let relayer_err: RelayerError = err.into();
3250        match relayer_err {
3251            RelayerError::ValidationError(msg) => {
3252                assert_eq!(msg, "bad asset");
3253            }
3254            _ => panic!("Expected ValidationError"),
3255        }
3256    }
3257
3258    #[test]
3259    fn test_invalid_account_address_converts_to_internal_error() {
3260        let err = StellarTransactionUtilsError::InvalidAccountAddress(
3261            "GABC".to_string(),
3262            "parse error".to_string(),
3263        );
3264        let relayer_err: RelayerError = err.into();
3265        match relayer_err {
3266            RelayerError::Internal(msg) => {
3267                assert_eq!(msg, "parse error");
3268            }
3269            _ => panic!("Expected Internal error"),
3270        }
3271    }
3272
3273    #[test]
3274    fn test_invalid_contract_address_converts_to_internal_error() {
3275        let err = StellarTransactionUtilsError::InvalidContractAddress(
3276            "CABC".to_string(),
3277            "contract parse error".to_string(),
3278        );
3279        let relayer_err: RelayerError = err.into();
3280        match relayer_err {
3281            RelayerError::Internal(msg) => {
3282                assert_eq!(msg, "contract parse error");
3283            }
3284            _ => panic!("Expected Internal error"),
3285        }
3286    }
3287
3288    #[test]
3289    fn test_symbol_creation_failed_converts_to_internal_error() {
3290        let err = StellarTransactionUtilsError::SymbolCreationFailed(
3291            "Balance".to_string(),
3292            "too long".to_string(),
3293        );
3294        let relayer_err: RelayerError = err.into();
3295        match relayer_err {
3296            RelayerError::Internal(msg) => {
3297                assert_eq!(msg, "too long");
3298            }
3299            _ => panic!("Expected Internal error"),
3300        }
3301    }
3302
3303    #[test]
3304    fn test_key_vector_creation_failed_converts_to_internal_error() {
3305        let err = StellarTransactionUtilsError::KeyVectorCreationFailed(
3306            "Balance".to_string(),
3307            "vec error".to_string(),
3308        );
3309        let relayer_err: RelayerError = err.into();
3310        match relayer_err {
3311            RelayerError::Internal(msg) => {
3312                assert_eq!(msg, "vec error");
3313            }
3314            _ => panic!("Expected Internal error"),
3315        }
3316    }
3317
3318    #[test]
3319    fn test_contract_data_query_persistent_failed_converts_to_internal_error() {
3320        let err = StellarTransactionUtilsError::ContractDataQueryPersistentFailed(
3321            "balance".to_string(),
3322            "rpc error".to_string(),
3323        );
3324        let relayer_err: RelayerError = err.into();
3325        match relayer_err {
3326            RelayerError::Internal(msg) => {
3327                assert_eq!(msg, "rpc error");
3328            }
3329            _ => panic!("Expected Internal error"),
3330        }
3331    }
3332
3333    #[test]
3334    fn test_contract_data_query_temporary_failed_converts_to_internal_error() {
3335        let err = StellarTransactionUtilsError::ContractDataQueryTemporaryFailed(
3336            "balance".to_string(),
3337            "temp error".to_string(),
3338        );
3339        let relayer_err: RelayerError = err.into();
3340        match relayer_err {
3341            RelayerError::Internal(msg) => {
3342                assert_eq!(msg, "temp error");
3343            }
3344            _ => panic!("Expected Internal error"),
3345        }
3346    }
3347
3348    #[test]
3349    fn test_ledger_entry_parse_failed_converts_to_internal_error() {
3350        let err = StellarTransactionUtilsError::LedgerEntryParseFailed(
3351            "entry".to_string(),
3352            "xdr error".to_string(),
3353        );
3354        let relayer_err: RelayerError = err.into();
3355        match relayer_err {
3356            RelayerError::Internal(msg) => {
3357                assert_eq!(msg, "xdr error");
3358            }
3359            _ => panic!("Expected Internal error"),
3360        }
3361    }
3362
3363    #[test]
3364    fn test_no_entries_found_converts_to_validation_error() {
3365        let err = StellarTransactionUtilsError::NoEntriesFound("balance".to_string());
3366        let relayer_err: RelayerError = err.into();
3367        match relayer_err {
3368            RelayerError::ValidationError(msg) => {
3369                assert!(msg.contains("No entries found"));
3370            }
3371            _ => panic!("Expected ValidationError"),
3372        }
3373    }
3374
3375    #[test]
3376    fn test_empty_entries_converts_to_validation_error() {
3377        let err = StellarTransactionUtilsError::EmptyEntries("balance".to_string());
3378        let relayer_err: RelayerError = err.into();
3379        match relayer_err {
3380            RelayerError::ValidationError(msg) => {
3381                assert!(msg.contains("Empty entries"));
3382            }
3383            _ => panic!("Expected ValidationError"),
3384        }
3385    }
3386
3387    #[test]
3388    fn test_unexpected_ledger_entry_type_converts_to_validation_error() {
3389        let err = StellarTransactionUtilsError::UnexpectedLedgerEntryType("balance".to_string());
3390        let relayer_err: RelayerError = err.into();
3391        match relayer_err {
3392            RelayerError::ValidationError(msg) => {
3393                assert!(msg.contains("Unexpected ledger entry type"));
3394            }
3395            _ => panic!("Expected ValidationError"),
3396        }
3397    }
3398
3399    #[test]
3400    fn test_invalid_issuer_length_converts_to_validation_error() {
3401        let err = StellarTransactionUtilsError::InvalidIssuerLength(56, "SHORT".to_string());
3402        let relayer_err: RelayerError = err.into();
3403        match relayer_err {
3404            RelayerError::ValidationError(msg) => {
3405                assert!(msg.contains("56"));
3406                assert!(msg.contains("SHORT"));
3407            }
3408            _ => panic!("Expected ValidationError"),
3409        }
3410    }
3411
3412    #[test]
3413    fn test_invalid_issuer_prefix_converts_to_validation_error() {
3414        let err = StellarTransactionUtilsError::InvalidIssuerPrefix('G', "CABC123".to_string());
3415        let relayer_err: RelayerError = err.into();
3416        match relayer_err {
3417            RelayerError::ValidationError(msg) => {
3418                assert!(msg.contains("'G'"));
3419                assert!(msg.contains("CABC123"));
3420            }
3421            _ => panic!("Expected ValidationError"),
3422        }
3423    }
3424
3425    #[test]
3426    fn test_account_fetch_failed_converts_to_provider_error() {
3427        let err = StellarTransactionUtilsError::AccountFetchFailed("fetch error".to_string());
3428        let relayer_err: RelayerError = err.into();
3429        match relayer_err {
3430            RelayerError::ProviderError(msg) => {
3431                assert_eq!(msg, "fetch error");
3432            }
3433            _ => panic!("Expected ProviderError"),
3434        }
3435    }
3436
3437    #[test]
3438    fn test_trustline_query_failed_converts_to_provider_error() {
3439        let err = StellarTransactionUtilsError::TrustlineQueryFailed(
3440            "USDC".to_string(),
3441            "rpc fail".to_string(),
3442        );
3443        let relayer_err: RelayerError = err.into();
3444        match relayer_err {
3445            RelayerError::ProviderError(msg) => {
3446                assert_eq!(msg, "rpc fail");
3447            }
3448            _ => panic!("Expected ProviderError"),
3449        }
3450    }
3451
3452    #[test]
3453    fn test_contract_invocation_failed_converts_to_provider_error() {
3454        let err = StellarTransactionUtilsError::ContractInvocationFailed(
3455            "transfer".to_string(),
3456            "invoke error".to_string(),
3457        );
3458        let relayer_err: RelayerError = err.into();
3459        match relayer_err {
3460            RelayerError::ProviderError(msg) => {
3461                assert_eq!(msg, "invoke error");
3462            }
3463            _ => panic!("Expected ProviderError"),
3464        }
3465    }
3466
3467    #[test]
3468    fn test_xdr_parse_failed_converts_to_internal_error() {
3469        let err = StellarTransactionUtilsError::XdrParseFailed("xdr parse fail".to_string());
3470        let relayer_err: RelayerError = err.into();
3471        match relayer_err {
3472            RelayerError::Internal(msg) => {
3473                assert_eq!(msg, "xdr parse fail");
3474            }
3475            _ => panic!("Expected Internal error"),
3476        }
3477    }
3478
3479    #[test]
3480    fn test_operation_extraction_failed_converts_to_internal_error() {
3481        let err =
3482            StellarTransactionUtilsError::OperationExtractionFailed("extract fail".to_string());
3483        let relayer_err: RelayerError = err.into();
3484        match relayer_err {
3485            RelayerError::Internal(msg) => {
3486                assert_eq!(msg, "extract fail");
3487            }
3488            _ => panic!("Expected Internal error"),
3489        }
3490    }
3491
3492    #[test]
3493    fn test_simulation_failed_converts_to_internal_error() {
3494        let err = StellarTransactionUtilsError::SimulationFailed("sim error".to_string());
3495        let relayer_err: RelayerError = err.into();
3496        match relayer_err {
3497            RelayerError::Internal(msg) => {
3498                assert_eq!(msg, "sim error");
3499            }
3500            _ => panic!("Expected Internal error"),
3501        }
3502    }
3503
3504    #[test]
3505    fn test_simulation_check_failed_converts_to_internal_error() {
3506        let err = StellarTransactionUtilsError::SimulationCheckFailed("check fail".to_string());
3507        let relayer_err: RelayerError = err.into();
3508        match relayer_err {
3509            RelayerError::Internal(msg) => {
3510                assert_eq!(msg, "check fail");
3511            }
3512            _ => panic!("Expected Internal error"),
3513        }
3514    }
3515
3516    #[test]
3517    fn test_dex_quote_failed_converts_to_internal_error() {
3518        let err = StellarTransactionUtilsError::DexQuoteFailed("dex error".to_string());
3519        let relayer_err: RelayerError = err.into();
3520        match relayer_err {
3521            RelayerError::Internal(msg) => {
3522                assert_eq!(msg, "dex error");
3523            }
3524            _ => panic!("Expected Internal error"),
3525        }
3526    }
3527
3528    #[test]
3529    fn test_empty_asset_code_converts_to_validation_error() {
3530        let err = StellarTransactionUtilsError::EmptyAssetCode("CODE:ISSUER".to_string());
3531        let relayer_err: RelayerError = err.into();
3532        match relayer_err {
3533            RelayerError::ValidationError(msg) => {
3534                assert!(msg.contains("Asset code cannot be empty"));
3535            }
3536            _ => panic!("Expected ValidationError"),
3537        }
3538    }
3539
3540    #[test]
3541    fn test_empty_issuer_address_converts_to_validation_error() {
3542        let err = StellarTransactionUtilsError::EmptyIssuerAddress("USDC:".to_string());
3543        let relayer_err: RelayerError = err.into();
3544        match relayer_err {
3545            RelayerError::ValidationError(msg) => {
3546                assert!(msg.contains("Issuer address cannot be empty"));
3547            }
3548            _ => panic!("Expected ValidationError"),
3549        }
3550    }
3551
3552    #[test]
3553    fn test_no_trustline_found_converts_to_validation_error() {
3554        let err =
3555            StellarTransactionUtilsError::NoTrustlineFound("USDC".to_string(), "GABC".to_string());
3556        let relayer_err: RelayerError = err.into();
3557        match relayer_err {
3558            RelayerError::ValidationError(msg) => {
3559                assert!(msg.contains("No trustline found"));
3560            }
3561            _ => panic!("Expected ValidationError"),
3562        }
3563    }
3564
3565    #[test]
3566    fn test_unsupported_trustline_version_converts_to_validation_error() {
3567        let err = StellarTransactionUtilsError::UnsupportedTrustlineVersion;
3568        let relayer_err: RelayerError = err.into();
3569        match relayer_err {
3570            RelayerError::ValidationError(msg) => {
3571                assert!(msg.contains("Unsupported trustline"));
3572            }
3573            _ => panic!("Expected ValidationError"),
3574        }
3575    }
3576
3577    #[test]
3578    fn test_unexpected_trustline_entry_type_converts_to_validation_error() {
3579        let err = StellarTransactionUtilsError::UnexpectedTrustlineEntryType;
3580        let relayer_err: RelayerError = err.into();
3581        match relayer_err {
3582            RelayerError::ValidationError(msg) => {
3583                assert!(msg.contains("Unexpected ledger entry type"));
3584            }
3585            _ => panic!("Expected ValidationError"),
3586        }
3587    }
3588
3589    #[test]
3590    fn test_balance_too_large_converts_to_validation_error() {
3591        let err = StellarTransactionUtilsError::BalanceTooLarge(1, 999);
3592        let relayer_err: RelayerError = err.into();
3593        match relayer_err {
3594            RelayerError::ValidationError(msg) => {
3595                assert!(msg.contains("Balance too large"));
3596            }
3597            _ => panic!("Expected ValidationError"),
3598        }
3599    }
3600
3601    #[test]
3602    fn test_negative_balance_i128_converts_to_validation_error() {
3603        let err = StellarTransactionUtilsError::NegativeBalanceI128(42);
3604        let relayer_err: RelayerError = err.into();
3605        match relayer_err {
3606            RelayerError::ValidationError(msg) => {
3607                assert!(msg.contains("Negative balance"));
3608            }
3609            _ => panic!("Expected ValidationError"),
3610        }
3611    }
3612
3613    #[test]
3614    fn test_negative_balance_i64_converts_to_validation_error() {
3615        let err = StellarTransactionUtilsError::NegativeBalanceI64(-5);
3616        let relayer_err: RelayerError = err.into();
3617        match relayer_err {
3618            RelayerError::ValidationError(msg) => {
3619                assert!(msg.contains("Negative balance"));
3620            }
3621            _ => panic!("Expected ValidationError"),
3622        }
3623    }
3624
3625    #[test]
3626    fn test_unexpected_balance_type_converts_to_validation_error() {
3627        let err = StellarTransactionUtilsError::UnexpectedBalanceType("Bool(true)".to_string());
3628        let relayer_err: RelayerError = err.into();
3629        match relayer_err {
3630            RelayerError::ValidationError(msg) => {
3631                assert!(msg.contains("Unexpected balance value type"));
3632            }
3633            _ => panic!("Expected ValidationError"),
3634        }
3635    }
3636
3637    #[test]
3638    fn test_unexpected_contract_data_entry_type_converts_to_validation_error() {
3639        let err = StellarTransactionUtilsError::UnexpectedContractDataEntryType;
3640        let relayer_err: RelayerError = err.into();
3641        match relayer_err {
3642            RelayerError::ValidationError(msg) => {
3643                assert!(msg.contains("Unexpected ledger entry type"));
3644            }
3645            _ => panic!("Expected ValidationError"),
3646        }
3647    }
3648
3649    #[test]
3650    fn test_native_asset_in_trustline_query_converts_to_validation_error() {
3651        let err = StellarTransactionUtilsError::NativeAssetInTrustlineQuery;
3652        let relayer_err: RelayerError = err.into();
3653        match relayer_err {
3654            RelayerError::ValidationError(msg) => {
3655                assert!(msg.contains("Native asset"));
3656            }
3657            _ => panic!("Expected ValidationError"),
3658        }
3659    }
3660}
3661
3662#[cfg(test)]
3663mod compute_resubmit_backoff_interval_tests {
3664    use super::compute_resubmit_backoff_interval;
3665    use chrono::Duration;
3666
3667    const BASE: i64 = 10;
3668    const MAX: i64 = 120;
3669
3670    #[test]
3671    fn returns_none_below_base() {
3672        assert!(compute_resubmit_backoff_interval(Duration::seconds(0), BASE, MAX).is_none());
3673        assert!(compute_resubmit_backoff_interval(Duration::seconds(5), BASE, MAX).is_none());
3674        assert!(compute_resubmit_backoff_interval(Duration::seconds(9), BASE, MAX).is_none());
3675    }
3676
3677    #[test]
3678    fn base_interval_at_1x() {
3679        // age 10-19s: ratio=1, log2(1)=0, interval = 10 * 2^0 = 10s
3680        assert_eq!(
3681            compute_resubmit_backoff_interval(Duration::seconds(10), BASE, MAX),
3682            Some(Duration::seconds(10))
3683        );
3684        assert_eq!(
3685            compute_resubmit_backoff_interval(Duration::seconds(19), BASE, MAX),
3686            Some(Duration::seconds(10))
3687        );
3688    }
3689
3690    #[test]
3691    fn doubles_at_2x() {
3692        // age 20-39s: ratio=2-3, log2(2)=1, interval = 10 * 2^1 = 20s
3693        assert_eq!(
3694            compute_resubmit_backoff_interval(Duration::seconds(20), BASE, MAX),
3695            Some(Duration::seconds(20))
3696        );
3697        assert_eq!(
3698            compute_resubmit_backoff_interval(Duration::seconds(39), BASE, MAX),
3699            Some(Duration::seconds(20))
3700        );
3701    }
3702
3703    #[test]
3704    fn quadruples_at_4x() {
3705        // age 40-79s: ratio=4-7, log2(4)=2, interval = 10 * 2^2 = 40s
3706        assert_eq!(
3707            compute_resubmit_backoff_interval(Duration::seconds(40), BASE, MAX),
3708            Some(Duration::seconds(40))
3709        );
3710        assert_eq!(
3711            compute_resubmit_backoff_interval(Duration::seconds(79), BASE, MAX),
3712            Some(Duration::seconds(40))
3713        );
3714    }
3715
3716    #[test]
3717    fn interval_at_8x() {
3718        // age 80-119s: ratio=8-11, log2(8)=3, interval = 10 * 2^3 = 80s
3719        assert_eq!(
3720            compute_resubmit_backoff_interval(Duration::seconds(80), BASE, MAX),
3721            Some(Duration::seconds(80))
3722        );
3723        assert_eq!(
3724            compute_resubmit_backoff_interval(Duration::seconds(119), BASE, MAX),
3725            Some(Duration::seconds(80))
3726        );
3727    }
3728
3729    #[test]
3730    fn capped_at_max() {
3731        // age 160s: ratio=16, log2(16)=4, interval = 10*16 = 160 → capped at 120s
3732        assert_eq!(
3733            compute_resubmit_backoff_interval(Duration::seconds(160), BASE, MAX),
3734            Some(Duration::seconds(MAX))
3735        );
3736        // age 1280s: ratio=128, log2(128)=7, interval = 10*128 = 1280 → capped at 120s
3737        assert_eq!(
3738            compute_resubmit_backoff_interval(Duration::seconds(1280), BASE, MAX),
3739            Some(Duration::seconds(MAX))
3740        );
3741    }
3742
3743    #[test]
3744    fn works_with_different_base_and_max() {
3745        // Verify the function is generic, not hardcoded to Stellar constants
3746        let base = 5;
3747        let max = 30;
3748        // age 5-9s: interval = 5s
3749        assert_eq!(
3750            compute_resubmit_backoff_interval(Duration::seconds(5), base, max),
3751            Some(Duration::seconds(5))
3752        );
3753        // age 10-19s: interval = 10s
3754        assert_eq!(
3755            compute_resubmit_backoff_interval(Duration::seconds(10), base, max),
3756            Some(Duration::seconds(10))
3757        );
3758        // age 40s: interval = 40 → capped at 30s
3759        assert_eq!(
3760            compute_resubmit_backoff_interval(Duration::seconds(40), base, max),
3761            Some(Duration::seconds(30))
3762        );
3763    }
3764}