openzeppelin_relayer/models/transaction/
repository.rs

1use super::evm::Speed;
2use crate::{
3    config::ServerConfig,
4    constants::{
5        DEFAULT_GAS_LIMIT, DEFAULT_TRANSACTION_SPEED, FINAL_TRANSACTION_STATUSES,
6        STELLAR_DEFAULT_MAX_FEE, STELLAR_DEFAULT_TRANSACTION_FEE,
7    },
8    domain::{
9        evm::PriceParams,
10        stellar::validation::{validate_operations, validate_soroban_memo_restriction},
11        xdr_utils::{is_signed, parse_transaction_xdr},
12        SignTransactionResponseEvm,
13    },
14    models::{
15        transaction::{
16            request::{evm::EvmTransactionRequest, stellar::StellarTransactionRequest},
17            stellar::{DecoratedSignature, MemoSpec, OperationSpec},
18        },
19        AddressError, EvmNetwork, NetworkRepoModel, NetworkTransactionRequest, NetworkType,
20        RelayerError, RelayerRepoModel, SignerError, StellarNetwork, StellarValidationError,
21        TransactionError, U256,
22    },
23    utils::{deserialize_optional_u128, serialize_optional_u128},
24};
25use alloy::{
26    consensus::{TxEip1559, TxLegacy},
27    primitives::{Address as AlloyAddress, Bytes, TxKind},
28    rpc::types::AccessList,
29};
30
31use chrono::{Duration, Utc};
32use serde::{Deserialize, Serialize};
33use std::{convert::TryFrom, str::FromStr};
34use strum::Display;
35
36use utoipa::ToSchema;
37use uuid::Uuid;
38
39use soroban_rs::xdr::{
40    Transaction as SorobanTransaction, TransactionEnvelope, TransactionV1Envelope, VecM,
41};
42
43#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema, Display)]
44#[serde(rename_all = "lowercase")]
45pub enum TransactionStatus {
46    Canceled,
47    Pending,
48    Sent,
49    Submitted,
50    Mined,
51    Confirmed,
52    Failed,
53    Expired,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize, Default)]
57pub struct TransactionUpdateRequest {
58    pub status: Option<TransactionStatus>,
59    pub status_reason: Option<String>,
60    pub sent_at: Option<String>,
61    pub confirmed_at: Option<String>,
62    pub network_data: Option<NetworkTransactionData>,
63    /// Timestamp when gas price was determined
64    pub priced_at: Option<String>,
65    /// History of transaction hashes
66    pub hashes: Option<Vec<String>>,
67    /// Number of no-ops in the transaction
68    pub noop_count: Option<u32>,
69    /// Whether the transaction is canceled
70    pub is_canceled: Option<bool>,
71    /// Timestamp when this transaction should be deleted (for final states)
72    pub delete_at: Option<String>,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct TransactionRepoModel {
77    pub id: String,
78    pub relayer_id: String,
79    pub status: TransactionStatus,
80    pub status_reason: Option<String>,
81    pub created_at: String,
82    pub sent_at: Option<String>,
83    pub confirmed_at: Option<String>,
84    pub valid_until: Option<String>,
85    /// Timestamp when this transaction should be deleted (for final states)
86    pub delete_at: Option<String>,
87    pub network_data: NetworkTransactionData,
88    /// Timestamp when gas price was determined
89    pub priced_at: Option<String>,
90    /// History of transaction hashes
91    pub hashes: Vec<String>,
92    pub network_type: NetworkType,
93    pub noop_count: Option<u32>,
94    pub is_canceled: Option<bool>,
95}
96
97impl TransactionRepoModel {
98    /// Validates the transaction repository model
99    ///
100    /// # Returns
101    /// * `Ok(())` if the transaction is valid
102    /// * `Err(TransactionError)` if validation fails
103    pub fn validate(&self) -> Result<(), TransactionError> {
104        Ok(())
105    }
106
107    /// Calculate when this transaction should be deleted based on its status and expiration hours
108    fn calculate_delete_at(expiration_hours: u64) -> Option<String> {
109        let delete_time = Utc::now() + Duration::hours(expiration_hours as i64);
110        Some(delete_time.to_rfc3339())
111    }
112
113    /// Update delete_at field if status changed to a final state
114    pub fn update_delete_at_if_final_status(&mut self) {
115        if self.delete_at.is_none() && FINAL_TRANSACTION_STATUSES.contains(&self.status) {
116            let expiration_hours = ServerConfig::get_transaction_expiration_hours();
117            self.delete_at = Self::calculate_delete_at(expiration_hours);
118        }
119    }
120
121    /// Apply partial updates to this transaction model
122    ///
123    /// This method encapsulates the business logic for updating transaction fields,
124    /// ensuring consistency across all repository implementations.
125    ///
126    /// # Arguments
127    /// * `update` - The partial update request containing the fields to update
128    pub fn apply_partial_update(&mut self, update: TransactionUpdateRequest) {
129        // Apply partial updates
130        if let Some(status) = update.status {
131            self.status = status;
132            self.update_delete_at_if_final_status();
133        }
134        if let Some(status_reason) = update.status_reason {
135            self.status_reason = Some(status_reason);
136        }
137        if let Some(sent_at) = update.sent_at {
138            self.sent_at = Some(sent_at);
139        }
140        if let Some(confirmed_at) = update.confirmed_at {
141            self.confirmed_at = Some(confirmed_at);
142        }
143        if let Some(network_data) = update.network_data {
144            self.network_data = network_data;
145        }
146        if let Some(priced_at) = update.priced_at {
147            self.priced_at = Some(priced_at);
148        }
149        if let Some(hashes) = update.hashes {
150            self.hashes = hashes;
151        }
152        if let Some(noop_count) = update.noop_count {
153            self.noop_count = Some(noop_count);
154        }
155        if let Some(is_canceled) = update.is_canceled {
156            self.is_canceled = Some(is_canceled);
157        }
158        if let Some(delete_at) = update.delete_at {
159            self.delete_at = Some(delete_at);
160        }
161    }
162
163    /// Creates a TransactionUpdateRequest to reset this transaction to its pre-prepare state.
164    /// This is used when a transaction needs to be retried from the beginning (e.g., bad sequence error).
165    ///
166    /// For Stellar transactions:
167    /// - Resets status to Pending
168    /// - Clears sent_at and confirmed_at timestamps
169    /// - Resets hashes array
170    /// - Calls reset_to_pre_prepare_state on the StellarTransactionData
171    ///
172    /// For other networks, only resets the common fields.
173    pub fn create_reset_update_request(
174        &self,
175    ) -> Result<TransactionUpdateRequest, TransactionError> {
176        let network_data = match &self.network_data {
177            NetworkTransactionData::Stellar(stellar_data) => Some(NetworkTransactionData::Stellar(
178                stellar_data.clone().reset_to_pre_prepare_state(),
179            )),
180            // For other networks, we don't modify the network data
181            _ => None,
182        };
183
184        Ok(TransactionUpdateRequest {
185            status: Some(TransactionStatus::Pending),
186            status_reason: None,
187            sent_at: None,
188            confirmed_at: None,
189            network_data,
190            priced_at: None,
191            hashes: Some(vec![]),
192            noop_count: None,
193            is_canceled: None,
194            delete_at: None,
195        })
196    }
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize)]
200#[serde(tag = "network_data", content = "data")]
201#[allow(clippy::large_enum_variant)]
202pub enum NetworkTransactionData {
203    Evm(EvmTransactionData),
204    Solana(SolanaTransactionData),
205    Stellar(StellarTransactionData),
206}
207
208impl NetworkTransactionData {
209    pub fn get_evm_transaction_data(&self) -> Result<EvmTransactionData, TransactionError> {
210        match self {
211            NetworkTransactionData::Evm(data) => Ok(data.clone()),
212            _ => Err(TransactionError::InvalidType(
213                "Expected EVM transaction".to_string(),
214            )),
215        }
216    }
217
218    pub fn get_solana_transaction_data(&self) -> Result<SolanaTransactionData, TransactionError> {
219        match self {
220            NetworkTransactionData::Solana(data) => Ok(data.clone()),
221            _ => Err(TransactionError::InvalidType(
222                "Expected Solana transaction".to_string(),
223            )),
224        }
225    }
226
227    pub fn get_stellar_transaction_data(&self) -> Result<StellarTransactionData, TransactionError> {
228        match self {
229            NetworkTransactionData::Stellar(data) => Ok(data.clone()),
230            _ => Err(TransactionError::InvalidType(
231                "Expected Stellar transaction".to_string(),
232            )),
233        }
234    }
235}
236
237#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema)]
238pub struct EvmTransactionDataSignature {
239    pub r: String,
240    pub s: String,
241    pub v: u8,
242    pub sig: String,
243}
244
245#[derive(Debug, Clone, Serialize, Deserialize)]
246pub struct EvmTransactionData {
247    #[serde(
248        serialize_with = "serialize_optional_u128",
249        deserialize_with = "deserialize_optional_u128",
250        default
251    )]
252    pub gas_price: Option<u128>,
253    pub gas_limit: Option<u64>,
254    pub nonce: Option<u64>,
255    pub value: U256,
256    pub data: Option<String>,
257    pub from: String,
258    pub to: Option<String>,
259    pub chain_id: u64,
260    pub hash: Option<String>,
261    pub signature: Option<EvmTransactionDataSignature>,
262    pub speed: Option<Speed>,
263    #[serde(
264        serialize_with = "serialize_optional_u128",
265        deserialize_with = "deserialize_optional_u128",
266        default
267    )]
268    pub max_fee_per_gas: Option<u128>,
269    #[serde(
270        serialize_with = "serialize_optional_u128",
271        deserialize_with = "deserialize_optional_u128",
272        default
273    )]
274    pub max_priority_fee_per_gas: Option<u128>,
275    pub raw: Option<Vec<u8>>,
276}
277
278impl EvmTransactionData {
279    /// Creates transaction data for replacement by combining existing transaction data with new request data.
280    ///
281    /// Preserves critical fields like chain_id, from address, and nonce while applying new transaction parameters.
282    /// Pricing fields are cleared and must be calculated separately.
283    ///
284    /// # Arguments
285    /// * `old_data` - The existing transaction data to preserve core fields from
286    /// * `request` - The new transaction request containing updated parameters
287    ///
288    /// # Returns
289    /// New `EvmTransactionData` configured for replacement transaction
290    pub fn for_replacement(old_data: &EvmTransactionData, request: &EvmTransactionRequest) -> Self {
291        Self {
292            // Preserve existing fields from old transaction
293            chain_id: old_data.chain_id,
294            from: old_data.from.clone(),
295            nonce: old_data.nonce, // Preserve original nonce for replacement
296
297            // Apply new fields from request
298            to: request.to.clone(),
299            value: request.value,
300            data: request.data.clone(),
301            gas_limit: request.gas_limit,
302            speed: request
303                .speed
304                .clone()
305                .or_else(|| old_data.speed.clone())
306                .or(Some(DEFAULT_TRANSACTION_SPEED)),
307
308            // Clear pricing fields - these will be calculated later
309            gas_price: None,
310            max_fee_per_gas: None,
311            max_priority_fee_per_gas: None,
312
313            // Reset signing fields
314            signature: None,
315            hash: None,
316            raw: None,
317        }
318    }
319
320    /// Updates the transaction data with calculated price parameters.
321    ///
322    /// # Arguments
323    /// * `price_params` - Calculated pricing parameters containing gas price and EIP-1559 fees
324    ///
325    /// # Returns
326    /// The updated `EvmTransactionData` with pricing information applied
327    pub fn with_price_params(mut self, price_params: PriceParams) -> Self {
328        self.gas_price = price_params.gas_price;
329        self.max_fee_per_gas = price_params.max_fee_per_gas;
330        self.max_priority_fee_per_gas = price_params.max_priority_fee_per_gas;
331
332        self
333    }
334
335    /// Updates the transaction data with an estimated gas limit.
336    ///
337    /// # Arguments
338    /// * `gas_limit` - The estimated gas limit for the transaction
339    ///
340    /// # Returns
341    /// The updated `EvmTransactionData` with the new gas limit
342    pub fn with_gas_estimate(mut self, gas_limit: u64) -> Self {
343        self.gas_limit = Some(gas_limit);
344        self
345    }
346
347    /// Updates the transaction data with a specific nonce value.
348    ///
349    /// # Arguments
350    /// * `nonce` - The nonce value to set for the transaction
351    ///
352    /// # Returns
353    /// The updated `EvmTransactionData` with the specified nonce
354    pub fn with_nonce(mut self, nonce: u64) -> Self {
355        self.nonce = Some(nonce);
356        self
357    }
358
359    /// Updates the transaction data with signature information from a signed transaction response.
360    ///
361    /// # Arguments
362    /// * `sig` - The signed transaction response containing signature, hash, and raw transaction data
363    ///
364    /// # Returns
365    /// The updated `EvmTransactionData` with signature information applied
366    pub fn with_signed_transaction_data(mut self, sig: SignTransactionResponseEvm) -> Self {
367        self.signature = Some(sig.signature);
368        self.hash = Some(sig.hash);
369        self.raw = Some(sig.raw);
370        self
371    }
372}
373
374#[cfg(test)]
375impl Default for EvmTransactionData {
376    fn default() -> Self {
377        Self {
378            from: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266".to_string(), // Standard Hardhat test address
379            to: Some("0x70997970C51812dc3A010C7d01b50e0d17dc79C8".to_string()), // Standard Hardhat test address
380            gas_price: Some(20000000000),
381            value: U256::from(1000000000000000000u128), // 1 ETH
382            data: Some("0x".to_string()),
383            nonce: Some(1),
384            chain_id: 1,
385            gas_limit: Some(DEFAULT_GAS_LIMIT),
386            hash: None,
387            signature: None,
388            speed: None,
389            max_fee_per_gas: None,
390            max_priority_fee_per_gas: None,
391            raw: None,
392        }
393    }
394}
395
396#[cfg(test)]
397impl Default for TransactionRepoModel {
398    fn default() -> Self {
399        Self {
400            id: "00000000-0000-0000-0000-000000000001".to_string(),
401            relayer_id: "00000000-0000-0000-0000-000000000002".to_string(),
402            status: TransactionStatus::Pending,
403            created_at: "2023-01-01T00:00:00Z".to_string(),
404            status_reason: None,
405            sent_at: None,
406            confirmed_at: None,
407            valid_until: None,
408            delete_at: None,
409            network_data: NetworkTransactionData::Evm(EvmTransactionData::default()),
410            network_type: NetworkType::Evm,
411            priced_at: None,
412            hashes: Vec::new(),
413            noop_count: None,
414            is_canceled: Some(false),
415        }
416    }
417}
418
419pub trait EvmTransactionDataTrait {
420    fn is_legacy(&self) -> bool;
421    fn is_eip1559(&self) -> bool;
422    fn is_speed(&self) -> bool;
423}
424
425impl EvmTransactionDataTrait for EvmTransactionData {
426    fn is_legacy(&self) -> bool {
427        self.gas_price.is_some()
428    }
429
430    fn is_eip1559(&self) -> bool {
431        self.max_fee_per_gas.is_some() && self.max_priority_fee_per_gas.is_some()
432    }
433
434    fn is_speed(&self) -> bool {
435        self.speed.is_some()
436    }
437}
438
439#[derive(Debug, Clone, Serialize, Deserialize)]
440pub struct SolanaTransactionData {
441    pub transaction: String,
442    pub signature: Option<String>,
443}
444
445/// Represents different input types for Stellar transactions
446#[derive(Debug, Clone, Serialize, Deserialize)]
447pub enum TransactionInput {
448    /// Operations to be built into a transaction
449    Operations(Vec<OperationSpec>),
450    /// Pre-built unsigned XDR that needs signing
451    UnsignedXdr(String),
452    /// Pre-built signed XDR that needs fee-bumping
453    SignedXdr { xdr: String, max_fee: i64 },
454}
455
456impl Default for TransactionInput {
457    fn default() -> Self {
458        TransactionInput::Operations(vec![])
459    }
460}
461
462impl TransactionInput {
463    /// Create a TransactionInput from a StellarTransactionRequest
464    pub fn from_stellar_request(
465        request: &StellarTransactionRequest,
466    ) -> Result<Self, TransactionError> {
467        // Handle XDR mode
468        if let Some(xdr) = &request.transaction_xdr {
469            let envelope = parse_transaction_xdr(xdr, false)
470                .map_err(|e| TransactionError::ValidationError(e.to_string()))?;
471
472            return if request.fee_bump == Some(true) {
473                // Fee bump requires signed XDR
474                if !is_signed(&envelope) {
475                    Err(TransactionError::ValidationError(
476                        "Cannot request fee_bump with unsigned XDR".to_string(),
477                    ))
478                } else {
479                    let max_fee = request.max_fee.unwrap_or(STELLAR_DEFAULT_MAX_FEE);
480                    Ok(TransactionInput::SignedXdr {
481                        xdr: xdr.clone(),
482                        max_fee,
483                    })
484                }
485            } else {
486                // No fee bump - must be unsigned
487                if is_signed(&envelope) {
488                    Err(TransactionError::ValidationError(
489                        StellarValidationError::UnexpectedSignedXdr.to_string(),
490                    ))
491                } else {
492                    Ok(TransactionInput::UnsignedXdr(xdr.clone()))
493                }
494            };
495        }
496
497        // Handle operations mode
498        if let Some(operations) = &request.operations {
499            if operations.is_empty() {
500                return Err(TransactionError::ValidationError(
501                    "Operations must not be empty".to_string(),
502                ));
503            }
504
505            if request.fee_bump == Some(true) {
506                return Err(TransactionError::ValidationError(
507                    "Cannot request fee_bump with operations mode".to_string(),
508                ));
509            }
510
511            // Validate operations
512            validate_operations(operations)
513                .map_err(|e| TransactionError::ValidationError(e.to_string()))?;
514
515            // Validate Soroban memo restriction
516            validate_soroban_memo_restriction(operations, &request.memo)
517                .map_err(|e| TransactionError::ValidationError(e.to_string()))?;
518
519            return Ok(TransactionInput::Operations(operations.clone()));
520        }
521
522        // Neither XDR nor operations provided
523        Err(TransactionError::ValidationError(
524            "Must provide either operations or transaction_xdr".to_string(),
525        ))
526    }
527}
528
529#[derive(Debug, Clone, Serialize, Deserialize)]
530pub struct StellarTransactionData {
531    pub source_account: String,
532    pub fee: Option<u32>,
533    pub sequence_number: Option<i64>,
534    pub memo: Option<MemoSpec>,
535    pub valid_until: Option<String>,
536    pub network_passphrase: String,
537    #[serde(skip_serializing, skip_deserializing)]
538    pub signatures: Vec<DecoratedSignature>,
539    pub hash: Option<String>,
540    #[serde(skip_serializing, skip_deserializing)]
541    pub simulation_transaction_data: Option<String>,
542    #[serde(skip)]
543    pub transaction_input: TransactionInput,
544    #[serde(skip_serializing, skip_deserializing)]
545    pub signed_envelope_xdr: Option<String>,
546}
547
548impl StellarTransactionData {
549    /// Resets the transaction data to its pre-prepare state by clearing all fields
550    /// that are populated during the prepare and submit phases.
551    ///
552    /// Fields preserved (from initial creation):
553    /// - source_account, network_passphrase, memo, valid_until, transaction_input
554    ///
555    /// Fields reset to None/empty:
556    /// - fee, sequence_number, signatures, signed_envelope_xdr, hash, simulation_transaction_data
557    pub fn reset_to_pre_prepare_state(mut self) -> Self {
558        // Reset all fields populated during prepare phase
559        self.fee = None;
560        self.sequence_number = None;
561        self.signatures = vec![];
562        self.signed_envelope_xdr = None;
563        self.simulation_transaction_data = None;
564
565        // Reset fields populated during submit phase
566        self.hash = None;
567
568        self
569    }
570
571    /// Updates the Stellar transaction data with a specific sequence number.
572    ///
573    /// # Arguments
574    /// * `sequence_number` - The sequence number for the Stellar account
575    ///
576    /// # Returns
577    /// The updated `StellarTransactionData` with the specified sequence number
578    pub fn with_sequence_number(mut self, sequence_number: i64) -> Self {
579        self.sequence_number = Some(sequence_number);
580        self
581    }
582
583    /// Updates the Stellar transaction data with the actual fee charged by the network.
584    ///
585    /// # Arguments
586    /// * `fee` - The actual fee charged in stroops
587    ///
588    /// # Returns
589    /// The updated `StellarTransactionData` with the specified fee
590    pub fn with_fee(mut self, fee: u32) -> Self {
591        self.fee = Some(fee);
592        self
593    }
594
595    /// Builds an unsigned envelope from any transaction input.
596    ///
597    /// Returns an envelope without signatures, suitable for simulation and fee calculation.
598    ///
599    /// # Returns
600    /// * `Ok(TransactionEnvelope)` containing the unsigned transaction
601    /// * `Err(SignerError)` if the transaction data cannot be converted
602    pub fn build_unsigned_envelope(&self) -> Result<TransactionEnvelope, SignerError> {
603        match &self.transaction_input {
604            TransactionInput::Operations(_) => {
605                // Build from operations without signatures
606                self.build_envelope_from_operations_unsigned()
607            }
608            TransactionInput::UnsignedXdr(xdr) => {
609                // Parse the XDR as-is (already unsigned)
610                self.parse_xdr_envelope(xdr)
611            }
612            TransactionInput::SignedXdr { xdr, .. } => {
613                // Parse the inner transaction (for fee-bump cases)
614                self.parse_xdr_envelope(xdr)
615            }
616        }
617    }
618
619    /// Gets the transaction envelope for simulation purposes.
620    ///
621    /// Convenience method that delegates to build_unsigned_envelope().
622    ///
623    /// # Returns
624    /// * `Ok(TransactionEnvelope)` containing the unsigned transaction
625    /// * `Err(SignerError)` if the transaction data cannot be converted
626    pub fn get_envelope_for_simulation(&self) -> Result<TransactionEnvelope, SignerError> {
627        self.build_unsigned_envelope()
628    }
629
630    /// Builds a signed envelope ready for submission to the network.
631    ///
632    /// Uses cached signed_envelope_xdr if available, otherwise builds from components.
633    ///
634    /// # Returns
635    /// * `Ok(TransactionEnvelope)` containing the signed transaction
636    /// * `Err(SignerError)` if the transaction data cannot be converted
637    pub fn build_signed_envelope(&self) -> Result<TransactionEnvelope, SignerError> {
638        // If we have a cached signed envelope, use it
639        if let Some(ref xdr) = self.signed_envelope_xdr {
640            return self.parse_xdr_envelope(xdr);
641        }
642
643        // Otherwise, build from components
644        match &self.transaction_input {
645            TransactionInput::Operations(_) => {
646                // Build from operations with signatures
647                self.build_envelope_from_operations_signed()
648            }
649            TransactionInput::UnsignedXdr(xdr) => {
650                // Parse and attach signatures
651                let envelope = self.parse_xdr_envelope(xdr)?;
652                self.attach_signatures_to_envelope(envelope)
653            }
654            TransactionInput::SignedXdr { xdr, .. } => {
655                // Already signed
656                self.parse_xdr_envelope(xdr)
657            }
658        }
659    }
660
661    /// Gets the transaction envelope for submission to the network.
662    ///
663    /// Convenience method that delegates to build_signed_envelope().
664    ///
665    /// # Returns
666    /// * `Ok(TransactionEnvelope)` containing the signed transaction
667    /// * `Err(SignerError)` if the transaction data cannot be converted
668    pub fn get_envelope_for_submission(&self) -> Result<TransactionEnvelope, SignerError> {
669        self.build_signed_envelope()
670    }
671
672    // Helper method to build unsigned envelope from operations
673    fn build_envelope_from_operations_unsigned(&self) -> Result<TransactionEnvelope, SignerError> {
674        let tx = SorobanTransaction::try_from(self.clone())?;
675        Ok(TransactionEnvelope::Tx(TransactionV1Envelope {
676            tx,
677            signatures: VecM::default(),
678        }))
679    }
680
681    // Helper method to build signed envelope from operations
682    fn build_envelope_from_operations_signed(&self) -> Result<TransactionEnvelope, SignerError> {
683        let tx = SorobanTransaction::try_from(self.clone())?;
684        let signatures = VecM::try_from(self.signatures.clone())
685            .map_err(|_| SignerError::ConversionError("too many signatures".into()))?;
686        Ok(TransactionEnvelope::Tx(TransactionV1Envelope {
687            tx,
688            signatures,
689        }))
690    }
691
692    // Helper method to parse XDR envelope
693    fn parse_xdr_envelope(&self, xdr: &str) -> Result<TransactionEnvelope, SignerError> {
694        use soroban_rs::xdr::{Limits, ReadXdr};
695        TransactionEnvelope::from_xdr_base64(xdr, Limits::none())
696            .map_err(|e| SignerError::ConversionError(format!("Invalid XDR: {}", e)))
697    }
698
699    // Helper method to attach signatures to an envelope
700    fn attach_signatures_to_envelope(
701        &self,
702        envelope: TransactionEnvelope,
703    ) -> Result<TransactionEnvelope, SignerError> {
704        use soroban_rs::xdr::{Limits, ReadXdr, WriteXdr};
705
706        // Serialize and re-parse to get a mutable version
707        let envelope_xdr = envelope.to_xdr_base64(Limits::none()).map_err(|e| {
708            SignerError::ConversionError(format!("Failed to serialize envelope: {}", e))
709        })?;
710
711        let mut envelope = TransactionEnvelope::from_xdr_base64(&envelope_xdr, Limits::none())
712            .map_err(|e| {
713                SignerError::ConversionError(format!("Failed to parse envelope: {}", e))
714            })?;
715
716        let sigs = VecM::try_from(self.signatures.clone())
717            .map_err(|_| SignerError::ConversionError("too many signatures".into()))?;
718
719        match &mut envelope {
720            TransactionEnvelope::Tx(ref mut v1) => v1.signatures = sigs,
721            TransactionEnvelope::TxV0(ref mut v0) => v0.signatures = sigs,
722            TransactionEnvelope::TxFeeBump(_) => {
723                return Err(SignerError::ConversionError(
724                    "Cannot attach signatures to fee-bump transaction directly".into(),
725                ));
726            }
727        }
728
729        Ok(envelope)
730    }
731
732    /// Updates instance with the given signature appended to the signatures list.
733    ///
734    /// # Arguments
735    /// * `sig` - The decorated signature to append
736    ///
737    /// # Returns
738    /// The updated `StellarTransactionData` with the new signature added
739    pub fn attach_signature(mut self, sig: DecoratedSignature) -> Self {
740        self.signatures.push(sig);
741        self
742    }
743
744    /// Updates instance with the transaction hash populated.
745    ///
746    /// # Arguments
747    /// * `hash` - The transaction hash to set
748    ///
749    /// # Returns
750    /// The updated `StellarTransactionData` with the hash field set
751    pub fn with_hash(mut self, hash: String) -> Self {
752        self.hash = Some(hash);
753        self
754    }
755
756    /// Return a new instance with simulation data applied (fees and transaction extension).
757    pub fn with_simulation_data(
758        mut self,
759        sim_response: soroban_rs::stellar_rpc_client::SimulateTransactionResponse,
760        operations_count: u64,
761    ) -> Result<Self, SignerError> {
762        use log::info;
763
764        // Update fee based on simulation (using soroban-helpers formula)
765        let inclusion_fee = operations_count * STELLAR_DEFAULT_TRANSACTION_FEE as u64;
766        let resource_fee = sim_response.min_resource_fee;
767
768        let updated_fee = u32::try_from(inclusion_fee + resource_fee)
769            .map_err(|_| SignerError::ConversionError("Fee too high".to_string()))?
770            .max(STELLAR_DEFAULT_TRANSACTION_FEE);
771        self.fee = Some(updated_fee);
772
773        // Store simulation transaction data for TransactionExt::V1
774        self.simulation_transaction_data = Some(sim_response.transaction_data);
775
776        info!(
777            "Applied simulation fee: {} stroops and stored transaction extension data",
778            updated_fee
779        );
780        Ok(self)
781    }
782}
783
784impl
785    TryFrom<(
786        &NetworkTransactionRequest,
787        &RelayerRepoModel,
788        &NetworkRepoModel,
789    )> for TransactionRepoModel
790{
791    type Error = RelayerError;
792
793    fn try_from(
794        (request, relayer_model, network_model): (
795            &NetworkTransactionRequest,
796            &RelayerRepoModel,
797            &NetworkRepoModel,
798        ),
799    ) -> Result<Self, Self::Error> {
800        let now = Utc::now().to_rfc3339();
801
802        match request {
803            NetworkTransactionRequest::Evm(evm_request) => {
804                let network = EvmNetwork::try_from(network_model.clone())?;
805                Ok(Self {
806                    id: Uuid::new_v4().to_string(),
807                    relayer_id: relayer_model.id.clone(),
808                    status: TransactionStatus::Pending,
809                    status_reason: None,
810                    created_at: now,
811                    sent_at: None,
812                    confirmed_at: None,
813                    valid_until: evm_request.valid_until.clone(),
814                    delete_at: None,
815                    network_type: NetworkType::Evm,
816                    network_data: NetworkTransactionData::Evm(EvmTransactionData {
817                        gas_price: evm_request.gas_price,
818                        gas_limit: evm_request.gas_limit,
819                        nonce: None,
820                        value: evm_request.value,
821                        data: evm_request.data.clone(),
822                        from: relayer_model.address.clone(),
823                        to: evm_request.to.clone(),
824                        chain_id: network.id(),
825                        hash: None,
826                        signature: None,
827                        speed: evm_request.speed.clone(),
828                        max_fee_per_gas: evm_request.max_fee_per_gas,
829                        max_priority_fee_per_gas: evm_request.max_priority_fee_per_gas,
830                        raw: None,
831                    }),
832                    priced_at: None,
833                    hashes: Vec::new(),
834                    noop_count: None,
835                    is_canceled: Some(false),
836                })
837            }
838            NetworkTransactionRequest::Solana(solana_request) => Ok(Self {
839                id: Uuid::new_v4().to_string(),
840                relayer_id: relayer_model.id.clone(),
841                status: TransactionStatus::Pending,
842                status_reason: None,
843                created_at: now,
844                sent_at: None,
845                confirmed_at: None,
846                valid_until: None,
847                delete_at: None,
848                network_type: NetworkType::Solana,
849                network_data: NetworkTransactionData::Solana(SolanaTransactionData {
850                    transaction: solana_request.transaction.clone().into_inner(),
851                    signature: None,
852                }),
853                priced_at: None,
854                hashes: Vec::new(),
855                noop_count: None,
856                is_canceled: Some(false),
857            }),
858            NetworkTransactionRequest::Stellar(stellar_request) => {
859                // Store the source account before consuming the request
860                let source_account = stellar_request.source_account.clone();
861
862                // Create the TransactionData before consuming the request
863                let stellar_data = StellarTransactionData {
864                    source_account: source_account.unwrap_or_else(|| relayer_model.address.clone()),
865                    memo: stellar_request.memo.clone(),
866                    valid_until: stellar_request.valid_until.clone(),
867                    network_passphrase: StellarNetwork::try_from(network_model.clone())?.passphrase,
868                    signatures: Vec::new(),
869                    hash: None,
870                    fee: None,
871                    sequence_number: None,
872                    simulation_transaction_data: None,
873                    transaction_input: TransactionInput::from_stellar_request(stellar_request)
874                        .map_err(|e| RelayerError::ValidationError(e.to_string()))?,
875                    signed_envelope_xdr: None,
876                };
877
878                Ok(Self {
879                    id: Uuid::new_v4().to_string(),
880                    relayer_id: relayer_model.id.clone(),
881                    status: TransactionStatus::Pending,
882                    status_reason: None,
883                    created_at: now,
884                    sent_at: None,
885                    confirmed_at: None,
886                    valid_until: None,
887                    delete_at: None,
888                    network_type: NetworkType::Stellar,
889                    network_data: NetworkTransactionData::Stellar(stellar_data),
890                    priced_at: None,
891                    hashes: Vec::new(),
892                    noop_count: None,
893                    is_canceled: Some(false),
894                })
895            }
896        }
897    }
898}
899
900impl EvmTransactionData {
901    /// Converts the transaction's 'to' field to an Alloy Address.
902    ///
903    /// # Returns
904    /// * `Ok(Some(AlloyAddress))` if the 'to' field contains a valid address
905    /// * `Ok(None)` if the 'to' field is None or empty (contract creation)
906    /// * `Err(SignerError)` if the address format is invalid
907    pub fn to_address(&self) -> Result<Option<AlloyAddress>, SignerError> {
908        Ok(match self.to.as_deref().filter(|s| !s.is_empty()) {
909            Some(addr_str) => Some(AlloyAddress::from_str(addr_str).map_err(|e| {
910                AddressError::ConversionError(format!("Invalid 'to' address: {}", e))
911            })?),
912            None => None,
913        })
914    }
915
916    /// Converts the transaction's data field from hex string to bytes.
917    ///
918    /// # Returns
919    /// * `Ok(Bytes)` containing the decoded transaction data
920    /// * `Err(SignerError)` if the hex string is invalid
921    pub fn data_to_bytes(&self) -> Result<Bytes, SignerError> {
922        Bytes::from_str(self.data.as_deref().unwrap_or(""))
923            .map_err(|e| SignerError::SigningError(format!("Invalid transaction data: {}", e)))
924    }
925}
926
927impl TryFrom<NetworkTransactionData> for TxLegacy {
928    type Error = SignerError;
929
930    fn try_from(tx: NetworkTransactionData) -> Result<Self, Self::Error> {
931        match tx {
932            NetworkTransactionData::Evm(tx) => {
933                let tx_kind = match tx.to_address()? {
934                    Some(addr) => TxKind::Call(addr),
935                    None => TxKind::Create,
936                };
937
938                Ok(Self {
939                    chain_id: Some(tx.chain_id),
940                    nonce: tx.nonce.unwrap_or(0),
941                    gas_limit: tx.gas_limit.unwrap_or(DEFAULT_GAS_LIMIT),
942                    gas_price: tx.gas_price.unwrap_or(0),
943                    to: tx_kind,
944                    value: tx.value,
945                    input: tx.data_to_bytes()?,
946                })
947            }
948            _ => Err(SignerError::SigningError(
949                "Not an EVM transaction".to_string(),
950            )),
951        }
952    }
953}
954
955impl TryFrom<NetworkTransactionData> for TxEip1559 {
956    type Error = SignerError;
957
958    fn try_from(tx: NetworkTransactionData) -> Result<Self, Self::Error> {
959        match tx {
960            NetworkTransactionData::Evm(tx) => {
961                let tx_kind = match tx.to_address()? {
962                    Some(addr) => TxKind::Call(addr),
963                    None => TxKind::Create,
964                };
965
966                Ok(Self {
967                    chain_id: tx.chain_id,
968                    nonce: tx.nonce.unwrap_or(0),
969                    gas_limit: tx.gas_limit.unwrap_or(DEFAULT_GAS_LIMIT),
970                    max_fee_per_gas: tx.max_fee_per_gas.unwrap_or(0),
971                    max_priority_fee_per_gas: tx.max_priority_fee_per_gas.unwrap_or(0),
972                    to: tx_kind,
973                    value: tx.value,
974                    access_list: AccessList::default(),
975                    input: tx.data_to_bytes()?,
976                })
977            }
978            _ => Err(SignerError::SigningError(
979                "Not an EVM transaction".to_string(),
980            )),
981        }
982    }
983}
984
985impl TryFrom<&EvmTransactionData> for TxLegacy {
986    type Error = SignerError;
987
988    fn try_from(tx: &EvmTransactionData) -> Result<Self, Self::Error> {
989        let tx_kind = match tx.to_address()? {
990            Some(addr) => TxKind::Call(addr),
991            None => TxKind::Create,
992        };
993
994        Ok(Self {
995            chain_id: Some(tx.chain_id),
996            nonce: tx.nonce.unwrap_or(0),
997            gas_limit: tx.gas_limit.unwrap_or(DEFAULT_GAS_LIMIT),
998            gas_price: tx.gas_price.unwrap_or(0),
999            to: tx_kind,
1000            value: tx.value,
1001            input: tx.data_to_bytes()?,
1002        })
1003    }
1004}
1005
1006impl TryFrom<EvmTransactionData> for TxLegacy {
1007    type Error = SignerError;
1008
1009    fn try_from(tx: EvmTransactionData) -> Result<Self, Self::Error> {
1010        Self::try_from(&tx)
1011    }
1012}
1013
1014impl TryFrom<&EvmTransactionData> for TxEip1559 {
1015    type Error = SignerError;
1016
1017    fn try_from(tx: &EvmTransactionData) -> Result<Self, Self::Error> {
1018        let tx_kind = match tx.to_address()? {
1019            Some(addr) => TxKind::Call(addr),
1020            None => TxKind::Create,
1021        };
1022
1023        Ok(Self {
1024            chain_id: tx.chain_id,
1025            nonce: tx.nonce.unwrap_or(0),
1026            gas_limit: tx.gas_limit.unwrap_or(DEFAULT_GAS_LIMIT),
1027            max_fee_per_gas: tx.max_fee_per_gas.unwrap_or(0),
1028            max_priority_fee_per_gas: tx.max_priority_fee_per_gas.unwrap_or(0),
1029            to: tx_kind,
1030            value: tx.value,
1031            access_list: AccessList::default(),
1032            input: tx.data_to_bytes()?,
1033        })
1034    }
1035}
1036
1037impl TryFrom<EvmTransactionData> for TxEip1559 {
1038    type Error = SignerError;
1039
1040    fn try_from(tx: EvmTransactionData) -> Result<Self, Self::Error> {
1041        Self::try_from(&tx)
1042    }
1043}
1044
1045impl From<&[u8; 65]> for EvmTransactionDataSignature {
1046    fn from(bytes: &[u8; 65]) -> Self {
1047        Self {
1048            r: hex::encode(&bytes[0..32]),
1049            s: hex::encode(&bytes[32..64]),
1050            v: bytes[64],
1051            sig: hex::encode(bytes),
1052        }
1053    }
1054}
1055
1056#[cfg(test)]
1057mod tests {
1058    use lazy_static::lazy_static;
1059    use soroban_rs::xdr::{BytesM, Signature, SignatureHint};
1060    use std::sync::Mutex;
1061
1062    use super::*;
1063    use crate::{
1064        config::{
1065            EvmNetworkConfig, NetworkConfigCommon, SolanaNetworkConfig, StellarNetworkConfig,
1066        },
1067        models::{
1068            network::NetworkConfigData,
1069            relayer::{
1070                RelayerEvmPolicy, RelayerNetworkPolicy, RelayerSolanaPolicy, RelayerStellarPolicy,
1071            },
1072            transaction::stellar::AssetSpec,
1073            EncodedSerializedTransaction,
1074        },
1075    };
1076
1077    // Use a mutex to ensure tests don't run in parallel when modifying env vars
1078    lazy_static! {
1079        static ref ENV_MUTEX: Mutex<()> = Mutex::new(());
1080    }
1081
1082    #[test]
1083    fn test_signature_from_bytes() {
1084        let test_bytes: [u8; 65] = [
1085            1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
1086            25, 26, 27, 28, 29, 30, 31, 32, // r (32 bytes)
1087            33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54,
1088            55, 56, 57, 58, 59, 60, 61, 62, 63, 64, // s (32 bytes)
1089            27, // v (1 byte)
1090        ];
1091
1092        let signature = EvmTransactionDataSignature::from(&test_bytes);
1093
1094        assert_eq!(signature.r.len(), 64); // 32 bytes in hex
1095        assert_eq!(signature.s.len(), 64); // 32 bytes in hex
1096        assert_eq!(signature.v, 27);
1097        assert_eq!(signature.sig.len(), 130); // 65 bytes in hex
1098    }
1099
1100    #[test]
1101    fn test_stellar_transaction_data_reset_to_pre_prepare_state() {
1102        let stellar_data = StellarTransactionData {
1103            source_account: "GTEST".to_string(),
1104            fee: Some(100),
1105            sequence_number: Some(42),
1106            memo: Some(MemoSpec::Text {
1107                value: "test memo".to_string(),
1108            }),
1109            valid_until: Some("2024-12-31".to_string()),
1110            network_passphrase: "Test Network".to_string(),
1111            signatures: vec![], // Simplified - empty for test
1112            hash: Some("test-hash".to_string()),
1113            simulation_transaction_data: Some("simulation-data".to_string()),
1114            transaction_input: TransactionInput::Operations(vec![OperationSpec::Payment {
1115                destination: "GDEST".to_string(),
1116                amount: 1000,
1117                asset: AssetSpec::Native,
1118            }]),
1119            signed_envelope_xdr: Some("signed-xdr".to_string()),
1120        };
1121
1122        let reset_data = stellar_data.clone().reset_to_pre_prepare_state();
1123
1124        // Fields that should be preserved
1125        assert_eq!(reset_data.source_account, stellar_data.source_account);
1126        assert_eq!(reset_data.memo, stellar_data.memo);
1127        assert_eq!(reset_data.valid_until, stellar_data.valid_until);
1128        assert_eq!(
1129            reset_data.network_passphrase,
1130            stellar_data.network_passphrase
1131        );
1132        assert!(matches!(
1133            reset_data.transaction_input,
1134            TransactionInput::Operations(_)
1135        ));
1136
1137        // Fields that should be reset
1138        assert_eq!(reset_data.fee, None);
1139        assert_eq!(reset_data.sequence_number, None);
1140        assert!(reset_data.signatures.is_empty());
1141        assert_eq!(reset_data.hash, None);
1142        assert_eq!(reset_data.simulation_transaction_data, None);
1143        assert_eq!(reset_data.signed_envelope_xdr, None);
1144    }
1145
1146    #[test]
1147    fn test_transaction_repo_model_create_reset_update_request() {
1148        let stellar_data = StellarTransactionData {
1149            source_account: "GTEST".to_string(),
1150            fee: Some(100),
1151            sequence_number: Some(42),
1152            memo: None,
1153            valid_until: None,
1154            network_passphrase: "Test Network".to_string(),
1155            signatures: vec![],
1156            hash: Some("test-hash".to_string()),
1157            simulation_transaction_data: None,
1158            transaction_input: TransactionInput::Operations(vec![]),
1159            signed_envelope_xdr: Some("signed-xdr".to_string()),
1160        };
1161
1162        let tx = TransactionRepoModel {
1163            id: "tx-1".to_string(),
1164            relayer_id: "relayer-1".to_string(),
1165            status: TransactionStatus::Failed,
1166            status_reason: Some("Bad sequence".to_string()),
1167            created_at: "2024-01-01".to_string(),
1168            sent_at: Some("2024-01-02".to_string()),
1169            confirmed_at: Some("2024-01-03".to_string()),
1170            valid_until: None,
1171            network_data: NetworkTransactionData::Stellar(stellar_data),
1172            priced_at: None,
1173            hashes: vec!["hash1".to_string(), "hash2".to_string()],
1174            network_type: NetworkType::Stellar,
1175            noop_count: None,
1176            is_canceled: None,
1177            delete_at: None,
1178        };
1179
1180        let update_req = tx.create_reset_update_request().unwrap();
1181
1182        // Check common fields
1183        assert_eq!(update_req.status, Some(TransactionStatus::Pending));
1184        assert_eq!(update_req.status_reason, None);
1185        assert_eq!(update_req.sent_at, None);
1186        assert_eq!(update_req.confirmed_at, None);
1187        assert_eq!(update_req.hashes, Some(vec![]));
1188
1189        // Check that network data was reset
1190        if let Some(NetworkTransactionData::Stellar(reset_data)) = update_req.network_data {
1191            assert_eq!(reset_data.fee, None);
1192            assert_eq!(reset_data.sequence_number, None);
1193            assert_eq!(reset_data.hash, None);
1194            assert_eq!(reset_data.signed_envelope_xdr, None);
1195        } else {
1196            panic!("Expected Stellar network data");
1197        }
1198    }
1199
1200    // Create a helper function to generate a sample EvmTransactionData for testing
1201    fn create_sample_evm_tx_data() -> EvmTransactionData {
1202        EvmTransactionData {
1203            gas_price: Some(20_000_000_000),
1204            gas_limit: Some(21000),
1205            nonce: Some(5),
1206            value: U256::from(1000000000000000000u128), // 1 ETH
1207            data: Some("0x".to_string()),
1208            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
1209            to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
1210            chain_id: 1,
1211            hash: None,
1212            signature: None,
1213            speed: None,
1214            max_fee_per_gas: None,
1215            max_priority_fee_per_gas: None,
1216            raw: None,
1217        }
1218    }
1219
1220    // Tests for EvmTransactionData methods
1221    #[test]
1222    fn test_evm_tx_with_price_params() {
1223        let tx_data = create_sample_evm_tx_data();
1224        let price_params = PriceParams {
1225            gas_price: None,
1226            max_fee_per_gas: Some(30_000_000_000),
1227            max_priority_fee_per_gas: Some(2_000_000_000),
1228            is_min_bumped: None,
1229            extra_fee: None,
1230            total_cost: U256::ZERO,
1231        };
1232
1233        let updated_tx = tx_data.with_price_params(price_params);
1234
1235        assert_eq!(updated_tx.max_fee_per_gas, Some(30_000_000_000));
1236        assert_eq!(updated_tx.max_priority_fee_per_gas, Some(2_000_000_000));
1237    }
1238
1239    #[test]
1240    fn test_evm_tx_with_gas_estimate() {
1241        let tx_data = create_sample_evm_tx_data();
1242        let new_gas_limit = 30000;
1243
1244        let updated_tx = tx_data.with_gas_estimate(new_gas_limit);
1245
1246        assert_eq!(updated_tx.gas_limit, Some(new_gas_limit));
1247    }
1248
1249    #[test]
1250    fn test_evm_tx_with_nonce() {
1251        let tx_data = create_sample_evm_tx_data();
1252        let new_nonce = 10;
1253
1254        let updated_tx = tx_data.with_nonce(new_nonce);
1255
1256        assert_eq!(updated_tx.nonce, Some(new_nonce));
1257    }
1258
1259    #[test]
1260    fn test_evm_tx_with_signed_transaction_data() {
1261        let tx_data = create_sample_evm_tx_data();
1262
1263        let signature = EvmTransactionDataSignature {
1264            r: "r_value".to_string(),
1265            s: "s_value".to_string(),
1266            v: 27,
1267            sig: "signature_value".to_string(),
1268        };
1269
1270        let signed_tx_response = SignTransactionResponseEvm {
1271            signature,
1272            hash: "0xabcdef1234567890".to_string(),
1273            raw: vec![1, 2, 3, 4, 5],
1274        };
1275
1276        let updated_tx = tx_data.with_signed_transaction_data(signed_tx_response);
1277
1278        assert_eq!(updated_tx.signature.as_ref().unwrap().r, "r_value");
1279        assert_eq!(updated_tx.signature.as_ref().unwrap().s, "s_value");
1280        assert_eq!(updated_tx.signature.as_ref().unwrap().v, 27);
1281        assert_eq!(updated_tx.hash, Some("0xabcdef1234567890".to_string()));
1282        assert_eq!(updated_tx.raw, Some(vec![1, 2, 3, 4, 5]));
1283    }
1284
1285    #[test]
1286    fn test_evm_tx_to_address() {
1287        // Test with valid address
1288        let tx_data = create_sample_evm_tx_data();
1289        let address_result = tx_data.to_address();
1290        assert!(address_result.is_ok());
1291        let address_option = address_result.unwrap();
1292        assert!(address_option.is_some());
1293        assert_eq!(
1294            address_option.unwrap().to_string().to_lowercase(),
1295            "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_lowercase()
1296        );
1297
1298        // Test with None address (contract creation)
1299        let mut contract_creation_tx = create_sample_evm_tx_data();
1300        contract_creation_tx.to = None;
1301        let address_result = contract_creation_tx.to_address();
1302        assert!(address_result.is_ok());
1303        assert!(address_result.unwrap().is_none());
1304
1305        // Test with empty address string
1306        let mut empty_address_tx = create_sample_evm_tx_data();
1307        empty_address_tx.to = Some("".to_string());
1308        let address_result = empty_address_tx.to_address();
1309        assert!(address_result.is_ok());
1310        assert!(address_result.unwrap().is_none());
1311
1312        // Test with invalid address
1313        let mut invalid_address_tx = create_sample_evm_tx_data();
1314        invalid_address_tx.to = Some("0xINVALID".to_string());
1315        let address_result = invalid_address_tx.to_address();
1316        assert!(address_result.is_err());
1317    }
1318
1319    #[test]
1320    fn test_evm_tx_data_to_bytes() {
1321        // Test with valid hex data
1322        let mut tx_data = create_sample_evm_tx_data();
1323        tx_data.data = Some("0x1234".to_string());
1324        let bytes_result = tx_data.data_to_bytes();
1325        assert!(bytes_result.is_ok());
1326        assert_eq!(bytes_result.unwrap().as_ref(), &[0x12, 0x34]);
1327
1328        // Test with empty data
1329        tx_data.data = Some("".to_string());
1330        assert!(tx_data.data_to_bytes().is_ok());
1331
1332        // Test with None data
1333        tx_data.data = None;
1334        assert!(tx_data.data_to_bytes().is_ok());
1335
1336        // Test with invalid hex data
1337        tx_data.data = Some("0xZZ".to_string());
1338        assert!(tx_data.data_to_bytes().is_err());
1339    }
1340
1341    // Tests for EvmTransactionDataTrait implementation
1342    #[test]
1343    fn test_evm_tx_is_legacy() {
1344        let mut tx_data = create_sample_evm_tx_data();
1345
1346        // Legacy transaction has gas_price
1347        assert!(tx_data.is_legacy());
1348
1349        // Not legacy if gas_price is None
1350        tx_data.gas_price = None;
1351        assert!(!tx_data.is_legacy());
1352    }
1353
1354    #[test]
1355    fn test_evm_tx_is_eip1559() {
1356        let mut tx_data = create_sample_evm_tx_data();
1357
1358        // Not EIP-1559 initially
1359        assert!(!tx_data.is_eip1559());
1360
1361        // Set EIP-1559 fields
1362        tx_data.max_fee_per_gas = Some(30_000_000_000);
1363        tx_data.max_priority_fee_per_gas = Some(2_000_000_000);
1364        assert!(tx_data.is_eip1559());
1365
1366        // Not EIP-1559 if one field is missing
1367        tx_data.max_priority_fee_per_gas = None;
1368        assert!(!tx_data.is_eip1559());
1369    }
1370
1371    #[test]
1372    fn test_evm_tx_is_speed() {
1373        let mut tx_data = create_sample_evm_tx_data();
1374
1375        // No speed initially
1376        assert!(!tx_data.is_speed());
1377
1378        // Set speed
1379        tx_data.speed = Some(Speed::Fast);
1380        assert!(tx_data.is_speed());
1381    }
1382
1383    // Tests for NetworkTransactionData methods
1384    #[test]
1385    fn test_network_tx_data_get_evm_transaction_data() {
1386        let evm_tx_data = create_sample_evm_tx_data();
1387        let network_data = NetworkTransactionData::Evm(evm_tx_data.clone());
1388
1389        // Should succeed for EVM data
1390        let result = network_data.get_evm_transaction_data();
1391        assert!(result.is_ok());
1392        assert_eq!(result.unwrap().chain_id, evm_tx_data.chain_id);
1393
1394        // Should fail for non-EVM data
1395        let solana_data = NetworkTransactionData::Solana(SolanaTransactionData {
1396            transaction: "transaction_123".to_string(),
1397            signature: None,
1398        });
1399        assert!(solana_data.get_evm_transaction_data().is_err());
1400    }
1401
1402    #[test]
1403    fn test_network_tx_data_get_solana_transaction_data() {
1404        let solana_tx_data = SolanaTransactionData {
1405            transaction: "transaction_123".to_string(),
1406            signature: None,
1407        };
1408        let network_data = NetworkTransactionData::Solana(solana_tx_data.clone());
1409
1410        // Should succeed for Solana data
1411        let result = network_data.get_solana_transaction_data();
1412        assert!(result.is_ok());
1413        assert_eq!(result.unwrap().transaction, solana_tx_data.transaction);
1414
1415        // Should fail for non-Solana data
1416        let evm_data = NetworkTransactionData::Evm(create_sample_evm_tx_data());
1417        assert!(evm_data.get_solana_transaction_data().is_err());
1418    }
1419
1420    #[test]
1421    fn test_network_tx_data_get_stellar_transaction_data() {
1422        let stellar_tx_data = StellarTransactionData {
1423            source_account: "account123".to_string(),
1424            fee: Some(100),
1425            sequence_number: Some(5),
1426            memo: Some(MemoSpec::Text {
1427                value: "Test memo".to_string(),
1428            }),
1429            valid_until: Some("2025-01-01T00:00:00Z".to_string()),
1430            network_passphrase: "Test SDF Network ; September 2015".to_string(),
1431            signatures: Vec::new(),
1432            hash: Some("hash123".to_string()),
1433            simulation_transaction_data: None,
1434            transaction_input: TransactionInput::Operations(vec![OperationSpec::Payment {
1435                destination: "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ".to_string(),
1436                amount: 100000000, // 10 XLM in stroops
1437                asset: AssetSpec::Native,
1438            }]),
1439            signed_envelope_xdr: None,
1440        };
1441        let network_data = NetworkTransactionData::Stellar(stellar_tx_data.clone());
1442
1443        // Should succeed for Stellar data
1444        let result = network_data.get_stellar_transaction_data();
1445        assert!(result.is_ok());
1446        assert_eq!(
1447            result.unwrap().source_account,
1448            stellar_tx_data.source_account
1449        );
1450
1451        // Should fail for non-Stellar data
1452        let evm_data = NetworkTransactionData::Evm(create_sample_evm_tx_data());
1453        assert!(evm_data.get_stellar_transaction_data().is_err());
1454    }
1455
1456    // Test for TryFrom<NetworkTransactionData> for TxLegacy
1457    #[test]
1458    fn test_try_from_network_tx_data_for_tx_legacy() {
1459        // Create a valid EVM transaction
1460        let evm_tx_data = create_sample_evm_tx_data();
1461        let network_data = NetworkTransactionData::Evm(evm_tx_data.clone());
1462
1463        // Should convert successfully
1464        let result = TxLegacy::try_from(network_data);
1465        assert!(result.is_ok());
1466        let tx_legacy = result.unwrap();
1467
1468        // Verify fields
1469        assert_eq!(tx_legacy.chain_id, Some(evm_tx_data.chain_id));
1470        assert_eq!(tx_legacy.nonce, evm_tx_data.nonce.unwrap());
1471        assert_eq!(tx_legacy.gas_limit, evm_tx_data.gas_limit.unwrap_or(21000));
1472        assert_eq!(tx_legacy.gas_price, evm_tx_data.gas_price.unwrap());
1473        assert_eq!(tx_legacy.value, evm_tx_data.value);
1474
1475        // Should fail for non-EVM data
1476        let solana_data = NetworkTransactionData::Solana(SolanaTransactionData {
1477            transaction: "transaction_123".to_string(),
1478            signature: None,
1479        });
1480        assert!(TxLegacy::try_from(solana_data).is_err());
1481    }
1482
1483    #[test]
1484    fn test_try_from_evm_tx_data_for_tx_legacy() {
1485        // Create a valid EVM transaction with legacy fields
1486        let evm_tx_data = create_sample_evm_tx_data();
1487
1488        // Should convert successfully
1489        let result = TxLegacy::try_from(evm_tx_data.clone());
1490        assert!(result.is_ok());
1491        let tx_legacy = result.unwrap();
1492
1493        // Verify fields
1494        assert_eq!(tx_legacy.chain_id, Some(evm_tx_data.chain_id));
1495        assert_eq!(tx_legacy.nonce, evm_tx_data.nonce.unwrap());
1496        assert_eq!(tx_legacy.gas_limit, evm_tx_data.gas_limit.unwrap_or(21000));
1497        assert_eq!(tx_legacy.gas_price, evm_tx_data.gas_price.unwrap());
1498        assert_eq!(tx_legacy.value, evm_tx_data.value);
1499    }
1500
1501    fn dummy_signature() -> DecoratedSignature {
1502        let hint = SignatureHint([0; 4]);
1503        let bytes: Vec<u8> = vec![0u8; 64];
1504        let bytes_m: BytesM<64> = bytes.try_into().expect("BytesM conversion");
1505        DecoratedSignature {
1506            hint,
1507            signature: Signature(bytes_m),
1508        }
1509    }
1510
1511    fn test_stellar_tx_data() -> StellarTransactionData {
1512        StellarTransactionData {
1513            source_account: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
1514            fee: Some(100),
1515            sequence_number: Some(1),
1516            memo: None,
1517            valid_until: None,
1518            network_passphrase: "Test SDF Network ; September 2015".to_string(),
1519            signatures: Vec::new(),
1520            hash: None,
1521            simulation_transaction_data: None,
1522            transaction_input: TransactionInput::Operations(vec![OperationSpec::Payment {
1523                destination: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
1524                amount: 1000,
1525                asset: AssetSpec::Native,
1526            }]),
1527            signed_envelope_xdr: None,
1528        }
1529    }
1530
1531    #[test]
1532    fn test_with_sequence_number() {
1533        let tx = test_stellar_tx_data();
1534        let updated = tx.with_sequence_number(42);
1535        assert_eq!(updated.sequence_number, Some(42));
1536    }
1537
1538    #[test]
1539    fn test_get_envelope_for_simulation() {
1540        let tx = test_stellar_tx_data();
1541        let env = tx.get_envelope_for_simulation();
1542        assert!(env.is_ok());
1543        let env = env.unwrap();
1544        // Should be a TransactionV1Envelope with no signatures
1545        match env {
1546            soroban_rs::xdr::TransactionEnvelope::Tx(tx_env) => {
1547                assert_eq!(tx_env.signatures.len(), 0);
1548            }
1549            _ => {
1550                panic!("Expected TransactionEnvelope::Tx variant");
1551            }
1552        }
1553    }
1554
1555    #[test]
1556    fn test_get_envelope_for_submission() {
1557        let mut tx = test_stellar_tx_data();
1558        tx.signatures.push(dummy_signature());
1559        let env = tx.get_envelope_for_submission();
1560        assert!(env.is_ok());
1561        let env = env.unwrap();
1562        match env {
1563            soroban_rs::xdr::TransactionEnvelope::Tx(tx_env) => {
1564                assert_eq!(tx_env.signatures.len(), 1);
1565            }
1566            _ => {
1567                panic!("Expected TransactionEnvelope::Tx variant");
1568            }
1569        }
1570    }
1571
1572    #[test]
1573    fn test_attach_signature() {
1574        let tx = test_stellar_tx_data();
1575        let sig = dummy_signature();
1576        let updated = tx.attach_signature(sig.clone());
1577        assert_eq!(updated.signatures.len(), 1);
1578        assert_eq!(updated.signatures[0], sig);
1579    }
1580
1581    #[test]
1582    fn test_with_hash() {
1583        let tx = test_stellar_tx_data();
1584        let updated = tx.with_hash("hash123".to_string());
1585        assert_eq!(updated.hash, Some("hash123".to_string()));
1586    }
1587
1588    #[test]
1589    fn test_evm_tx_for_replacement() {
1590        let old_data = create_sample_evm_tx_data();
1591        let new_request = EvmTransactionRequest {
1592            to: Some("0xNewRecipient".to_string()),
1593            value: U256::from(2000000000000000000u64), // 2 ETH
1594            data: Some("0xNewData".to_string()),
1595            gas_limit: Some(25000),
1596            gas_price: Some(30000000000), // 30 Gwei (should be ignored)
1597            max_fee_per_gas: Some(40000000000), // Should be ignored
1598            max_priority_fee_per_gas: Some(2000000000), // Should be ignored
1599            speed: Some(Speed::Fast),
1600            valid_until: None,
1601        };
1602
1603        let result = EvmTransactionData::for_replacement(&old_data, &new_request);
1604
1605        // Should preserve old data fields
1606        assert_eq!(result.chain_id, old_data.chain_id);
1607        assert_eq!(result.from, old_data.from);
1608        assert_eq!(result.nonce, old_data.nonce);
1609
1610        // Should use new request fields
1611        assert_eq!(result.to, new_request.to);
1612        assert_eq!(result.value, new_request.value);
1613        assert_eq!(result.data, new_request.data);
1614        assert_eq!(result.gas_limit, new_request.gas_limit);
1615        assert_eq!(result.speed, new_request.speed);
1616
1617        // Should clear all pricing fields (regardless of what's in the request)
1618        assert_eq!(result.gas_price, None);
1619        assert_eq!(result.max_fee_per_gas, None);
1620        assert_eq!(result.max_priority_fee_per_gas, None);
1621
1622        // Should reset signing fields
1623        assert_eq!(result.signature, None);
1624        assert_eq!(result.hash, None);
1625        assert_eq!(result.raw, None);
1626    }
1627
1628    #[test]
1629    fn test_transaction_repo_model_validate() {
1630        let transaction = TransactionRepoModel::default();
1631        let result = transaction.validate();
1632        assert!(result.is_ok());
1633    }
1634
1635    #[test]
1636    fn test_try_from_network_transaction_request_evm() {
1637        use crate::models::{NetworkRepoModel, NetworkType, RelayerRepoModel};
1638
1639        let evm_request = NetworkTransactionRequest::Evm(EvmTransactionRequest {
1640            to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
1641            value: U256::from(1000000000000000000u128),
1642            data: Some("0x1234".to_string()),
1643            gas_limit: Some(21000),
1644            gas_price: Some(20000000000),
1645            max_fee_per_gas: None,
1646            max_priority_fee_per_gas: None,
1647            speed: Some(Speed::Fast),
1648            valid_until: Some("2024-12-31T23:59:59Z".to_string()),
1649        });
1650
1651        let relayer_model = RelayerRepoModel {
1652            id: "relayer-id".to_string(),
1653            name: "Test Relayer".to_string(),
1654            network: "network-id".to_string(),
1655            paused: false,
1656            network_type: NetworkType::Evm,
1657            signer_id: "signer-id".to_string(),
1658            policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
1659            address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
1660            notification_id: None,
1661            system_disabled: false,
1662            custom_rpc_urls: None,
1663        };
1664
1665        let network_model = NetworkRepoModel {
1666            id: "evm:ethereum".to_string(),
1667            name: "ethereum".to_string(),
1668            network_type: NetworkType::Evm,
1669            config: NetworkConfigData::Evm(EvmNetworkConfig {
1670                common: NetworkConfigCommon {
1671                    network: "ethereum".to_string(),
1672                    from: None,
1673                    rpc_urls: Some(vec!["https://mainnet.infura.io".to_string()]),
1674                    explorer_urls: Some(vec!["https://etherscan.io".to_string()]),
1675                    average_blocktime_ms: Some(12000),
1676                    is_testnet: Some(false),
1677                    tags: Some(vec!["mainnet".to_string()]),
1678                },
1679                chain_id: Some(1),
1680                required_confirmations: Some(12),
1681                features: None,
1682                symbol: Some("ETH".to_string()),
1683            }),
1684        };
1685
1686        let result = TransactionRepoModel::try_from((&evm_request, &relayer_model, &network_model));
1687        assert!(result.is_ok());
1688        let transaction = result.unwrap();
1689
1690        assert_eq!(transaction.relayer_id, relayer_model.id);
1691        assert_eq!(transaction.status, TransactionStatus::Pending);
1692        assert_eq!(transaction.network_type, NetworkType::Evm);
1693        assert_eq!(
1694            transaction.valid_until,
1695            Some("2024-12-31T23:59:59Z".to_string())
1696        );
1697        assert!(transaction.is_canceled == Some(false));
1698
1699        if let NetworkTransactionData::Evm(evm_data) = transaction.network_data {
1700            assert_eq!(evm_data.from, relayer_model.address);
1701            assert_eq!(
1702                evm_data.to,
1703                Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string())
1704            );
1705            assert_eq!(evm_data.value, U256::from(1000000000000000000u128));
1706            assert_eq!(evm_data.chain_id, 1);
1707            assert_eq!(evm_data.gas_limit, Some(21000));
1708            assert_eq!(evm_data.gas_price, Some(20000000000));
1709            assert_eq!(evm_data.speed, Some(Speed::Fast));
1710        } else {
1711            panic!("Expected EVM transaction data");
1712        }
1713    }
1714
1715    #[test]
1716    fn test_try_from_network_transaction_request_solana() {
1717        use crate::models::{
1718            NetworkRepoModel, NetworkTransactionRequest, NetworkType, RelayerRepoModel,
1719        };
1720
1721        let solana_request = NetworkTransactionRequest::Solana(
1722            crate::models::transaction::request::solana::SolanaTransactionRequest {
1723                transaction: EncodedSerializedTransaction::new("transaction_123".to_string()),
1724            },
1725        );
1726
1727        let relayer_model = RelayerRepoModel {
1728            id: "relayer-id".to_string(),
1729            name: "Test Solana Relayer".to_string(),
1730            network: "network-id".to_string(),
1731            paused: false,
1732            network_type: NetworkType::Solana,
1733            signer_id: "signer-id".to_string(),
1734            policies: RelayerNetworkPolicy::Solana(RelayerSolanaPolicy::default()),
1735            address: "solana_address".to_string(),
1736            notification_id: None,
1737            system_disabled: false,
1738            custom_rpc_urls: None,
1739        };
1740
1741        let network_model = NetworkRepoModel {
1742            id: "solana:mainnet".to_string(),
1743            name: "mainnet".to_string(),
1744            network_type: NetworkType::Solana,
1745            config: NetworkConfigData::Solana(SolanaNetworkConfig {
1746                common: NetworkConfigCommon {
1747                    network: "mainnet".to_string(),
1748                    from: None,
1749                    rpc_urls: Some(vec!["https://api.mainnet-beta.solana.com".to_string()]),
1750                    explorer_urls: Some(vec!["https://explorer.solana.com".to_string()]),
1751                    average_blocktime_ms: Some(400),
1752                    is_testnet: Some(false),
1753                    tags: Some(vec!["mainnet".to_string()]),
1754                },
1755            }),
1756        };
1757
1758        let result =
1759            TransactionRepoModel::try_from((&solana_request, &relayer_model, &network_model));
1760        assert!(result.is_ok());
1761        let transaction = result.unwrap();
1762
1763        assert_eq!(transaction.relayer_id, relayer_model.id);
1764        assert_eq!(transaction.status, TransactionStatus::Pending);
1765        assert_eq!(transaction.network_type, NetworkType::Solana);
1766        assert_eq!(transaction.valid_until, None);
1767
1768        if let NetworkTransactionData::Solana(solana_data) = transaction.network_data {
1769            assert_eq!(solana_data.transaction, "transaction_123".to_string());
1770            assert_eq!(solana_data.signature, None);
1771        } else {
1772            panic!("Expected Solana transaction data");
1773        }
1774    }
1775
1776    #[test]
1777    fn test_try_from_network_transaction_request_stellar() {
1778        use crate::models::transaction::request::stellar::StellarTransactionRequest;
1779        use crate::models::{
1780            NetworkRepoModel, NetworkTransactionRequest, NetworkType, RelayerRepoModel,
1781        };
1782
1783        let stellar_request = NetworkTransactionRequest::Stellar(StellarTransactionRequest {
1784            source_account: Some(
1785                "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
1786            ),
1787            network: "mainnet".to_string(),
1788            operations: Some(vec![OperationSpec::Payment {
1789                destination: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
1790                amount: 1000000,
1791                asset: AssetSpec::Native,
1792            }]),
1793            memo: Some(MemoSpec::Text {
1794                value: "Test memo".to_string(),
1795            }),
1796            valid_until: Some("2024-12-31T23:59:59Z".to_string()),
1797            transaction_xdr: None,
1798            fee_bump: None,
1799            max_fee: None,
1800        });
1801
1802        let relayer_model = RelayerRepoModel {
1803            id: "relayer-id".to_string(),
1804            name: "Test Stellar Relayer".to_string(),
1805            network: "network-id".to_string(),
1806            paused: false,
1807            network_type: NetworkType::Stellar,
1808            signer_id: "signer-id".to_string(),
1809            policies: RelayerNetworkPolicy::Stellar(RelayerStellarPolicy::default()),
1810            address: "stellar_address".to_string(),
1811            notification_id: None,
1812            system_disabled: false,
1813            custom_rpc_urls: None,
1814        };
1815
1816        let network_model = NetworkRepoModel {
1817            id: "stellar:mainnet".to_string(),
1818            name: "mainnet".to_string(),
1819            network_type: NetworkType::Stellar,
1820            config: NetworkConfigData::Stellar(StellarNetworkConfig {
1821                common: NetworkConfigCommon {
1822                    network: "mainnet".to_string(),
1823                    from: None,
1824                    rpc_urls: Some(vec!["https://horizon.stellar.org".to_string()]),
1825                    explorer_urls: Some(vec!["https://stellarchain.io".to_string()]),
1826                    average_blocktime_ms: Some(5000),
1827                    is_testnet: Some(false),
1828                    tags: Some(vec!["mainnet".to_string()]),
1829                },
1830                passphrase: Some("Public Global Stellar Network ; September 2015".to_string()),
1831            }),
1832        };
1833
1834        let result =
1835            TransactionRepoModel::try_from((&stellar_request, &relayer_model, &network_model));
1836        assert!(result.is_ok());
1837        let transaction = result.unwrap();
1838
1839        assert_eq!(transaction.relayer_id, relayer_model.id);
1840        assert_eq!(transaction.status, TransactionStatus::Pending);
1841        assert_eq!(transaction.network_type, NetworkType::Stellar);
1842        assert_eq!(transaction.valid_until, None);
1843
1844        if let NetworkTransactionData::Stellar(stellar_data) = transaction.network_data {
1845            assert_eq!(
1846                stellar_data.source_account,
1847                "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
1848            );
1849            // Check that transaction_input contains the operations
1850            if let TransactionInput::Operations(ops) = &stellar_data.transaction_input {
1851                assert_eq!(ops.len(), 1);
1852                if let OperationSpec::Payment {
1853                    destination,
1854                    amount,
1855                    asset,
1856                } = &ops[0]
1857                {
1858                    assert_eq!(
1859                        destination,
1860                        "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
1861                    );
1862                    assert_eq!(amount, &1000000);
1863                    assert_eq!(asset, &AssetSpec::Native);
1864                } else {
1865                    panic!("Expected Payment operation");
1866                }
1867            } else {
1868                panic!("Expected Operations transaction input");
1869            }
1870            assert_eq!(
1871                stellar_data.memo,
1872                Some(MemoSpec::Text {
1873                    value: "Test memo".to_string()
1874                })
1875            );
1876            assert_eq!(
1877                stellar_data.valid_until,
1878                Some("2024-12-31T23:59:59Z".to_string())
1879            );
1880            assert_eq!(stellar_data.signatures.len(), 0);
1881            assert_eq!(stellar_data.hash, None);
1882            assert_eq!(stellar_data.fee, None);
1883            assert_eq!(stellar_data.sequence_number, None);
1884        } else {
1885            panic!("Expected Stellar transaction data");
1886        }
1887    }
1888
1889    #[test]
1890    fn test_try_from_network_transaction_data_for_tx_eip1559() {
1891        // Create a valid EVM transaction with EIP-1559 fields
1892        let mut evm_tx_data = create_sample_evm_tx_data();
1893        evm_tx_data.max_fee_per_gas = Some(30_000_000_000);
1894        evm_tx_data.max_priority_fee_per_gas = Some(2_000_000_000);
1895        let network_data = NetworkTransactionData::Evm(evm_tx_data.clone());
1896
1897        // Should convert successfully
1898        let result = TxEip1559::try_from(network_data);
1899        assert!(result.is_ok());
1900        let tx_eip1559 = result.unwrap();
1901
1902        // Verify fields
1903        assert_eq!(tx_eip1559.chain_id, evm_tx_data.chain_id);
1904        assert_eq!(tx_eip1559.nonce, evm_tx_data.nonce.unwrap());
1905        assert_eq!(tx_eip1559.gas_limit, evm_tx_data.gas_limit.unwrap_or(21000));
1906        assert_eq!(
1907            tx_eip1559.max_fee_per_gas,
1908            evm_tx_data.max_fee_per_gas.unwrap()
1909        );
1910        assert_eq!(
1911            tx_eip1559.max_priority_fee_per_gas,
1912            evm_tx_data.max_priority_fee_per_gas.unwrap()
1913        );
1914        assert_eq!(tx_eip1559.value, evm_tx_data.value);
1915        assert!(tx_eip1559.access_list.0.is_empty());
1916
1917        // Should fail for non-EVM data
1918        let solana_data = NetworkTransactionData::Solana(SolanaTransactionData {
1919            transaction: "transaction_123".to_string(),
1920            signature: None,
1921        });
1922        assert!(TxEip1559::try_from(solana_data).is_err());
1923    }
1924
1925    #[test]
1926    fn test_evm_transaction_data_defaults() {
1927        let default_data = EvmTransactionData::default();
1928
1929        assert_eq!(
1930            default_data.from,
1931            "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
1932        );
1933        assert_eq!(
1934            default_data.to,
1935            Some("0x70997970C51812dc3A010C7d01b50e0d17dc79C8".to_string())
1936        );
1937        assert_eq!(default_data.gas_price, Some(20000000000));
1938        assert_eq!(default_data.value, U256::from(1000000000000000000u128));
1939        assert_eq!(default_data.data, Some("0x".to_string()));
1940        assert_eq!(default_data.nonce, Some(1));
1941        assert_eq!(default_data.chain_id, 1);
1942        assert_eq!(default_data.gas_limit, Some(21000));
1943        assert_eq!(default_data.hash, None);
1944        assert_eq!(default_data.signature, None);
1945        assert_eq!(default_data.speed, None);
1946        assert_eq!(default_data.max_fee_per_gas, None);
1947        assert_eq!(default_data.max_priority_fee_per_gas, None);
1948        assert_eq!(default_data.raw, None);
1949    }
1950
1951    #[test]
1952    fn test_transaction_repo_model_defaults() {
1953        let default_model = TransactionRepoModel::default();
1954
1955        assert_eq!(default_model.id, "00000000-0000-0000-0000-000000000001");
1956        assert_eq!(
1957            default_model.relayer_id,
1958            "00000000-0000-0000-0000-000000000002"
1959        );
1960        assert_eq!(default_model.status, TransactionStatus::Pending);
1961        assert_eq!(default_model.created_at, "2023-01-01T00:00:00Z");
1962        assert_eq!(default_model.status_reason, None);
1963        assert_eq!(default_model.sent_at, None);
1964        assert_eq!(default_model.confirmed_at, None);
1965        assert_eq!(default_model.valid_until, None);
1966        assert_eq!(default_model.delete_at, None);
1967        assert_eq!(default_model.network_type, NetworkType::Evm);
1968        assert_eq!(default_model.priced_at, None);
1969        assert_eq!(default_model.hashes.len(), 0);
1970        assert_eq!(default_model.noop_count, None);
1971        assert_eq!(default_model.is_canceled, Some(false));
1972    }
1973
1974    #[test]
1975    fn test_evm_tx_for_replacement_with_speed_fallback() {
1976        let mut old_data = create_sample_evm_tx_data();
1977        old_data.speed = Some(Speed::SafeLow);
1978
1979        // Request with no speed - should use old data's speed
1980        let new_request = EvmTransactionRequest {
1981            to: Some("0xNewRecipient".to_string()),
1982            value: U256::from(2000000000000000000u64),
1983            data: Some("0xNewData".to_string()),
1984            gas_limit: Some(25000),
1985            gas_price: None,
1986            max_fee_per_gas: None,
1987            max_priority_fee_per_gas: None,
1988            speed: None,
1989            valid_until: None,
1990        };
1991
1992        let result = EvmTransactionData::for_replacement(&old_data, &new_request);
1993        assert_eq!(result.speed, Some(Speed::SafeLow));
1994
1995        // Old data with no speed - should use default
1996        let mut old_data_no_speed = create_sample_evm_tx_data();
1997        old_data_no_speed.speed = None;
1998
1999        let result2 = EvmTransactionData::for_replacement(&old_data_no_speed, &new_request);
2000        assert_eq!(result2.speed, Some(DEFAULT_TRANSACTION_SPEED));
2001    }
2002
2003    #[test]
2004    fn test_transaction_status_serialization() {
2005        use serde_json;
2006
2007        // Test serialization of different status values
2008        assert_eq!(
2009            serde_json::to_string(&TransactionStatus::Pending).unwrap(),
2010            "\"pending\""
2011        );
2012        assert_eq!(
2013            serde_json::to_string(&TransactionStatus::Sent).unwrap(),
2014            "\"sent\""
2015        );
2016        assert_eq!(
2017            serde_json::to_string(&TransactionStatus::Mined).unwrap(),
2018            "\"mined\""
2019        );
2020        assert_eq!(
2021            serde_json::to_string(&TransactionStatus::Failed).unwrap(),
2022            "\"failed\""
2023        );
2024        assert_eq!(
2025            serde_json::to_string(&TransactionStatus::Confirmed).unwrap(),
2026            "\"confirmed\""
2027        );
2028        assert_eq!(
2029            serde_json::to_string(&TransactionStatus::Canceled).unwrap(),
2030            "\"canceled\""
2031        );
2032        assert_eq!(
2033            serde_json::to_string(&TransactionStatus::Submitted).unwrap(),
2034            "\"submitted\""
2035        );
2036        assert_eq!(
2037            serde_json::to_string(&TransactionStatus::Expired).unwrap(),
2038            "\"expired\""
2039        );
2040    }
2041
2042    #[test]
2043    fn test_evm_tx_contract_creation() {
2044        // Test transaction data for contract creation (no 'to' address)
2045        let mut tx_data = create_sample_evm_tx_data();
2046        tx_data.to = None;
2047
2048        let tx_legacy = TxLegacy::try_from(&tx_data).unwrap();
2049        assert_eq!(tx_legacy.to, TxKind::Create);
2050
2051        let tx_eip1559 = TxEip1559::try_from(&tx_data).unwrap();
2052        assert_eq!(tx_eip1559.to, TxKind::Create);
2053    }
2054
2055    #[test]
2056    fn test_evm_tx_default_values_in_conversion() {
2057        // Test conversion with missing nonce and gas price
2058        let mut tx_data = create_sample_evm_tx_data();
2059        tx_data.nonce = None;
2060        tx_data.gas_price = None;
2061        tx_data.max_fee_per_gas = None;
2062        tx_data.max_priority_fee_per_gas = None;
2063
2064        let tx_legacy = TxLegacy::try_from(&tx_data).unwrap();
2065        assert_eq!(tx_legacy.nonce, 0); // Default nonce
2066        assert_eq!(tx_legacy.gas_price, 0); // Default gas price
2067
2068        let tx_eip1559 = TxEip1559::try_from(&tx_data).unwrap();
2069        assert_eq!(tx_eip1559.nonce, 0); // Default nonce
2070        assert_eq!(tx_eip1559.max_fee_per_gas, 0); // Default max fee
2071        assert_eq!(tx_eip1559.max_priority_fee_per_gas, 0); // Default max priority fee
2072    }
2073
2074    // Helper function to create test network and relayer models
2075    fn test_models() -> (NetworkRepoModel, RelayerRepoModel) {
2076        use crate::config::{NetworkConfigCommon, StellarNetworkConfig};
2077        use crate::constants::DEFAULT_STELLAR_MIN_BALANCE;
2078
2079        let network_config = NetworkConfigData::Stellar(StellarNetworkConfig {
2080            common: NetworkConfigCommon {
2081                network: "testnet".to_string(),
2082                from: None,
2083                rpc_urls: Some(vec!["https://test.stellar.org".to_string()]),
2084                explorer_urls: None,
2085                average_blocktime_ms: Some(5000), // 5 seconds for Stellar
2086                is_testnet: Some(true),
2087                tags: None,
2088            },
2089            passphrase: Some("Test SDF Network ; September 2015".to_string()),
2090        });
2091
2092        let network_model = NetworkRepoModel {
2093            id: "stellar:testnet".to_string(),
2094            name: "testnet".to_string(),
2095            network_type: NetworkType::Stellar,
2096            config: network_config,
2097        };
2098
2099        let relayer_model = RelayerRepoModel {
2100            id: "test-relayer".to_string(),
2101            name: "Test Relayer".to_string(),
2102            network: "stellar:testnet".to_string(),
2103            paused: false,
2104            network_type: NetworkType::Stellar,
2105            signer_id: "test-signer".to_string(),
2106            policies: RelayerNetworkPolicy::Stellar(RelayerStellarPolicy {
2107                max_fee: None,
2108                timeout_seconds: None,
2109                min_balance: Some(DEFAULT_STELLAR_MIN_BALANCE),
2110            }),
2111            address: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2112            notification_id: None,
2113            system_disabled: false,
2114            custom_rpc_urls: None,
2115        };
2116
2117        (network_model, relayer_model)
2118    }
2119
2120    #[test]
2121    fn test_stellar_xdr_transaction_input_conversion() {
2122        let (network_model, relayer_model) = test_models();
2123
2124        // Test case 1: Operations mode (existing behavior)
2125        let stellar_request = StellarTransactionRequest {
2126            source_account: Some(
2127                "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2128            ),
2129            network: "testnet".to_string(),
2130            operations: Some(vec![OperationSpec::Payment {
2131                destination: "GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB".to_string(),
2132                amount: 1000000,
2133                asset: AssetSpec::Native,
2134            }]),
2135            memo: None,
2136            valid_until: None,
2137            transaction_xdr: None,
2138            fee_bump: None,
2139            max_fee: None,
2140        };
2141
2142        let request = NetworkTransactionRequest::Stellar(stellar_request);
2143        let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2144        assert!(result.is_ok());
2145
2146        let tx_model = result.unwrap();
2147        if let NetworkTransactionData::Stellar(ref stellar_data) = tx_model.network_data {
2148            assert!(matches!(
2149                stellar_data.transaction_input,
2150                TransactionInput::Operations(_)
2151            ));
2152        } else {
2153            panic!("Expected Stellar transaction data");
2154        }
2155
2156        // Test case 2: Unsigned XDR mode
2157        // This is a valid unsigned transaction created with stellar CLI
2158        let unsigned_xdr = "AAAAAgAAAACige4lTdwSB/sto4SniEdJ2kOa2X65s5bqkd40J4DjSwAAAGQAAHAkAAAADgAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAKKB7iVN3BIH+y2jhKeIR0naQ5rZfrmzluqR3jQngONLAAAAAAAAAAAAD0JAAAAAAAAAAAA=";
2159        let stellar_request = StellarTransactionRequest {
2160            source_account: None,
2161            network: "testnet".to_string(),
2162            operations: Some(vec![]),
2163            memo: None,
2164            valid_until: None,
2165            transaction_xdr: Some(unsigned_xdr.to_string()),
2166            fee_bump: None,
2167            max_fee: None,
2168        };
2169
2170        let request = NetworkTransactionRequest::Stellar(stellar_request);
2171        let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2172        assert!(result.is_ok());
2173
2174        let tx_model = result.unwrap();
2175        if let NetworkTransactionData::Stellar(ref stellar_data) = tx_model.network_data {
2176            assert!(matches!(
2177                stellar_data.transaction_input,
2178                TransactionInput::UnsignedXdr(_)
2179            ));
2180        } else {
2181            panic!("Expected Stellar transaction data");
2182        }
2183
2184        // Test case 3: Signed XDR with fee_bump
2185        // Create a signed XDR by duplicating the test logic from xdr_tests
2186        let signed_xdr = {
2187            use soroban_rs::xdr::{Limits, TransactionEnvelope, TransactionV1Envelope, WriteXdr};
2188            use stellar_strkey::ed25519::PublicKey;
2189
2190            // Use the same transaction structure but add a dummy signature
2191            let source_pk =
2192                PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
2193                    .unwrap();
2194            let dest_pk =
2195                PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
2196                    .unwrap();
2197
2198            let payment_op = soroban_rs::xdr::PaymentOp {
2199                destination: soroban_rs::xdr::MuxedAccount::Ed25519(soroban_rs::xdr::Uint256(
2200                    dest_pk.0,
2201                )),
2202                asset: soroban_rs::xdr::Asset::Native,
2203                amount: 1000000,
2204            };
2205
2206            let operation = soroban_rs::xdr::Operation {
2207                source_account: None,
2208                body: soroban_rs::xdr::OperationBody::Payment(payment_op),
2209            };
2210
2211            let operations: soroban_rs::xdr::VecM<soroban_rs::xdr::Operation, 100> =
2212                vec![operation].try_into().unwrap();
2213
2214            let tx = soroban_rs::xdr::Transaction {
2215                source_account: soroban_rs::xdr::MuxedAccount::Ed25519(soroban_rs::xdr::Uint256(
2216                    source_pk.0,
2217                )),
2218                fee: 100,
2219                seq_num: soroban_rs::xdr::SequenceNumber(1),
2220                cond: soroban_rs::xdr::Preconditions::None,
2221                memo: soroban_rs::xdr::Memo::None,
2222                operations,
2223                ext: soroban_rs::xdr::TransactionExt::V0,
2224            };
2225
2226            // Add a dummy signature
2227            let hint = soroban_rs::xdr::SignatureHint([0; 4]);
2228            let sig_bytes: Vec<u8> = vec![0u8; 64];
2229            let sig_bytes_m: soroban_rs::xdr::BytesM<64> = sig_bytes.try_into().unwrap();
2230            let sig = soroban_rs::xdr::DecoratedSignature {
2231                hint,
2232                signature: soroban_rs::xdr::Signature(sig_bytes_m),
2233            };
2234
2235            let envelope = TransactionV1Envelope {
2236                tx,
2237                signatures: vec![sig].try_into().unwrap(),
2238            };
2239
2240            let tx_envelope = TransactionEnvelope::Tx(envelope);
2241            tx_envelope.to_xdr_base64(Limits::none()).unwrap()
2242        };
2243        let stellar_request = StellarTransactionRequest {
2244            source_account: None,
2245            network: "testnet".to_string(),
2246            operations: Some(vec![]),
2247            memo: None,
2248            valid_until: None,
2249            transaction_xdr: Some(signed_xdr.to_string()),
2250            fee_bump: Some(true),
2251            max_fee: Some(20000000),
2252        };
2253
2254        let request = NetworkTransactionRequest::Stellar(stellar_request);
2255        let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2256        assert!(result.is_ok());
2257
2258        let tx_model = result.unwrap();
2259        if let NetworkTransactionData::Stellar(ref stellar_data) = tx_model.network_data {
2260            match &stellar_data.transaction_input {
2261                TransactionInput::SignedXdr { xdr, max_fee } => {
2262                    assert_eq!(xdr, &signed_xdr);
2263                    assert_eq!(*max_fee, 20000000);
2264                }
2265                _ => panic!("Expected SignedXdr transaction input"),
2266            }
2267        } else {
2268            panic!("Expected Stellar transaction data");
2269        }
2270
2271        // Test case 4: Signed XDR without fee_bump should fail
2272        let stellar_request = StellarTransactionRequest {
2273            source_account: None,
2274            network: "testnet".to_string(),
2275            operations: Some(vec![]),
2276            memo: None,
2277            valid_until: None,
2278            transaction_xdr: Some(signed_xdr.clone()),
2279            fee_bump: None,
2280            max_fee: None,
2281        };
2282
2283        let request = NetworkTransactionRequest::Stellar(stellar_request);
2284        let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2285        assert!(result.is_err());
2286        assert!(result
2287            .unwrap_err()
2288            .to_string()
2289            .contains("Expected unsigned XDR but received signed XDR"));
2290
2291        // Test case 5: Operations with fee_bump should fail
2292        let stellar_request = StellarTransactionRequest {
2293            source_account: Some(
2294                "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2295            ),
2296            network: "testnet".to_string(),
2297            operations: Some(vec![OperationSpec::Payment {
2298                destination: "GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB".to_string(),
2299                amount: 1000000,
2300                asset: AssetSpec::Native,
2301            }]),
2302            memo: None,
2303            valid_until: None,
2304            transaction_xdr: None,
2305            fee_bump: Some(true),
2306            max_fee: None,
2307        };
2308
2309        let request = NetworkTransactionRequest::Stellar(stellar_request);
2310        let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2311        assert!(result.is_err());
2312        assert!(result
2313            .unwrap_err()
2314            .to_string()
2315            .contains("Cannot request fee_bump with operations mode"));
2316    }
2317
2318    #[test]
2319    fn test_invoke_host_function_must_be_exclusive() {
2320        let (network_model, relayer_model) = test_models();
2321
2322        // Test case 1: Single InvokeHostFunction - should succeed
2323        let stellar_request = StellarTransactionRequest {
2324            source_account: Some(
2325                "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2326            ),
2327            network: "testnet".to_string(),
2328            operations: Some(vec![OperationSpec::InvokeContract {
2329                contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2330                    .to_string(),
2331                function_name: "transfer".to_string(),
2332                args: vec![],
2333                auth: None,
2334            }]),
2335            memo: None,
2336            valid_until: None,
2337            transaction_xdr: None,
2338            fee_bump: None,
2339            max_fee: None,
2340        };
2341
2342        let request = NetworkTransactionRequest::Stellar(stellar_request);
2343        let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2344        assert!(result.is_ok(), "Single InvokeHostFunction should succeed");
2345
2346        // Test case 2: InvokeHostFunction mixed with Payment - should fail
2347        let stellar_request = StellarTransactionRequest {
2348            source_account: Some(
2349                "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2350            ),
2351            network: "testnet".to_string(),
2352            operations: Some(vec![
2353                OperationSpec::Payment {
2354                    destination: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
2355                        .to_string(),
2356                    amount: 1000,
2357                    asset: AssetSpec::Native,
2358                },
2359                OperationSpec::InvokeContract {
2360                    contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2361                        .to_string(),
2362                    function_name: "transfer".to_string(),
2363                    args: vec![],
2364                    auth: None,
2365                },
2366            ]),
2367            memo: None,
2368            valid_until: None,
2369            transaction_xdr: None,
2370            fee_bump: None,
2371            max_fee: None,
2372        };
2373
2374        let request = NetworkTransactionRequest::Stellar(stellar_request);
2375        let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2376
2377        match result {
2378            Ok(_) => panic!("Expected Soroban operation mixed with Payment to fail"),
2379            Err(err) => {
2380                let err_str = err.to_string();
2381                assert!(
2382                    err_str.contains("Soroban operations must be exclusive"),
2383                    "Expected error about Soroban operation exclusivity, got: {}",
2384                    err_str
2385                );
2386            }
2387        }
2388
2389        // Test case 3: Multiple InvokeHostFunction operations - should fail
2390        let stellar_request = StellarTransactionRequest {
2391            source_account: Some(
2392                "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2393            ),
2394            network: "testnet".to_string(),
2395            operations: Some(vec![
2396                OperationSpec::InvokeContract {
2397                    contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2398                        .to_string(),
2399                    function_name: "transfer".to_string(),
2400                    args: vec![],
2401                    auth: None,
2402                },
2403                OperationSpec::InvokeContract {
2404                    contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2405                        .to_string(),
2406                    function_name: "approve".to_string(),
2407                    args: vec![],
2408                    auth: None,
2409                },
2410            ]),
2411            memo: None,
2412            valid_until: None,
2413            transaction_xdr: None,
2414            fee_bump: None,
2415            max_fee: None,
2416        };
2417
2418        let request = NetworkTransactionRequest::Stellar(stellar_request);
2419        let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2420
2421        match result {
2422            Ok(_) => panic!("Expected multiple Soroban operations to fail"),
2423            Err(err) => {
2424                let err_str = err.to_string();
2425                assert!(
2426                    err_str.contains("Transaction can contain at most one Soroban operation"),
2427                    "Expected error about multiple Soroban operations, got: {}",
2428                    err_str
2429                );
2430            }
2431        }
2432
2433        // Test case 4: Multiple Payment operations - should succeed
2434        let stellar_request = StellarTransactionRequest {
2435            source_account: Some(
2436                "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2437            ),
2438            network: "testnet".to_string(),
2439            operations: Some(vec![
2440                OperationSpec::Payment {
2441                    destination: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
2442                        .to_string(),
2443                    amount: 1000,
2444                    asset: AssetSpec::Native,
2445                },
2446                OperationSpec::Payment {
2447                    destination: "GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"
2448                        .to_string(),
2449                    amount: 2000,
2450                    asset: AssetSpec::Native,
2451                },
2452            ]),
2453            memo: None,
2454            valid_until: None,
2455            transaction_xdr: None,
2456            fee_bump: None,
2457            max_fee: None,
2458        };
2459
2460        let request = NetworkTransactionRequest::Stellar(stellar_request);
2461        let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2462        assert!(result.is_ok(), "Multiple Payment operations should succeed");
2463
2464        // Test case 5: InvokeHostFunction with non-None memo - should fail
2465        let stellar_request = StellarTransactionRequest {
2466            source_account: Some(
2467                "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2468            ),
2469            network: "testnet".to_string(),
2470            operations: Some(vec![OperationSpec::InvokeContract {
2471                contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2472                    .to_string(),
2473                function_name: "transfer".to_string(),
2474                args: vec![],
2475                auth: None,
2476            }]),
2477            memo: Some(MemoSpec::Text {
2478                value: "This should fail".to_string(),
2479            }),
2480            valid_until: None,
2481            transaction_xdr: None,
2482            fee_bump: None,
2483            max_fee: None,
2484        };
2485
2486        let request = NetworkTransactionRequest::Stellar(stellar_request);
2487        let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2488
2489        match result {
2490            Ok(_) => panic!("Expected InvokeHostFunction with non-None memo to fail"),
2491            Err(err) => {
2492                let err_str = err.to_string();
2493                assert!(
2494                    err_str.contains("Soroban operations cannot have a memo"),
2495                    "Expected error about memo restriction, got: {}",
2496                    err_str
2497                );
2498            }
2499        }
2500
2501        // Test case 6: InvokeHostFunction with memo None - should succeed
2502        let stellar_request = StellarTransactionRequest {
2503            source_account: Some(
2504                "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2505            ),
2506            network: "testnet".to_string(),
2507            operations: Some(vec![OperationSpec::InvokeContract {
2508                contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2509                    .to_string(),
2510                function_name: "transfer".to_string(),
2511                args: vec![],
2512                auth: None,
2513            }]),
2514            memo: Some(MemoSpec::None),
2515            valid_until: None,
2516            transaction_xdr: None,
2517            fee_bump: None,
2518            max_fee: None,
2519        };
2520
2521        let request = NetworkTransactionRequest::Stellar(stellar_request);
2522        let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2523        assert!(
2524            result.is_ok(),
2525            "InvokeHostFunction with MemoSpec::None should succeed"
2526        );
2527
2528        // Test case 7: InvokeHostFunction with no memo field - should succeed
2529        let stellar_request = StellarTransactionRequest {
2530            source_account: Some(
2531                "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2532            ),
2533            network: "testnet".to_string(),
2534            operations: Some(vec![OperationSpec::InvokeContract {
2535                contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2536                    .to_string(),
2537                function_name: "transfer".to_string(),
2538                args: vec![],
2539                auth: None,
2540            }]),
2541            memo: None,
2542            valid_until: None,
2543            transaction_xdr: None,
2544            fee_bump: None,
2545            max_fee: None,
2546        };
2547
2548        let request = NetworkTransactionRequest::Stellar(stellar_request);
2549        let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2550        assert!(
2551            result.is_ok(),
2552            "InvokeHostFunction with no memo should succeed"
2553        );
2554
2555        // Test case 8: Payment operation with memo - should succeed
2556        let stellar_request = StellarTransactionRequest {
2557            source_account: Some(
2558                "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2559            ),
2560            network: "testnet".to_string(),
2561            operations: Some(vec![OperationSpec::Payment {
2562                destination: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2563                amount: 1000,
2564                asset: AssetSpec::Native,
2565            }]),
2566            memo: Some(MemoSpec::Text {
2567                value: "Payment memo is allowed".to_string(),
2568            }),
2569            valid_until: None,
2570            transaction_xdr: None,
2571            fee_bump: None,
2572            max_fee: None,
2573        };
2574
2575        let request = NetworkTransactionRequest::Stellar(stellar_request);
2576        let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2577        assert!(result.is_ok(), "Payment operation with memo should succeed");
2578    }
2579
2580    #[test]
2581    fn test_update_delete_at_if_final_status_does_not_update_when_delete_at_already_set() {
2582        let _lock = match ENV_MUTEX.lock() {
2583            Ok(guard) => guard,
2584            Err(poisoned) => poisoned.into_inner(),
2585        };
2586
2587        use std::env;
2588
2589        // Set custom expiration hours for test
2590        env::set_var("TRANSACTION_EXPIRATION_HOURS", "6");
2591
2592        let mut transaction = create_test_transaction();
2593        transaction.delete_at = Some("2024-01-01T00:00:00Z".to_string());
2594        transaction.status = TransactionStatus::Confirmed; // Final status
2595
2596        let original_delete_at = transaction.delete_at.clone();
2597
2598        transaction.update_delete_at_if_final_status();
2599
2600        // Should not change delete_at when it's already set
2601        assert_eq!(transaction.delete_at, original_delete_at);
2602
2603        // Cleanup
2604        env::remove_var("TRANSACTION_EXPIRATION_HOURS");
2605    }
2606
2607    #[test]
2608    fn test_update_delete_at_if_final_status_does_not_update_when_status_not_final() {
2609        let _lock = match ENV_MUTEX.lock() {
2610            Ok(guard) => guard,
2611            Err(poisoned) => poisoned.into_inner(),
2612        };
2613
2614        use std::env;
2615
2616        // Set custom expiration hours for test
2617        env::set_var("TRANSACTION_EXPIRATION_HOURS", "6");
2618
2619        let mut transaction = create_test_transaction();
2620        transaction.delete_at = None;
2621        transaction.status = TransactionStatus::Pending; // Non-final status
2622
2623        transaction.update_delete_at_if_final_status();
2624
2625        // Should not set delete_at for non-final status
2626        assert!(transaction.delete_at.is_none());
2627
2628        // Cleanup
2629        env::remove_var("TRANSACTION_EXPIRATION_HOURS");
2630    }
2631
2632    #[test]
2633    fn test_update_delete_at_if_final_status_sets_delete_at_for_final_statuses() {
2634        let _lock = match ENV_MUTEX.lock() {
2635            Ok(guard) => guard,
2636            Err(poisoned) => poisoned.into_inner(),
2637        };
2638
2639        use crate::config::ServerConfig;
2640        use chrono::{DateTime, Duration, Utc};
2641        use std::env;
2642
2643        // Set custom expiration hours for test
2644        env::set_var("TRANSACTION_EXPIRATION_HOURS", "3"); // Use 3 hours for this test
2645
2646        // Verify the env var is actually set correctly
2647        let actual_hours = ServerConfig::get_transaction_expiration_hours();
2648        assert_eq!(
2649            actual_hours, 3,
2650            "Environment variable should be set to 3 hours"
2651        );
2652
2653        let final_statuses = vec![
2654            TransactionStatus::Canceled,
2655            TransactionStatus::Confirmed,
2656            TransactionStatus::Failed,
2657            TransactionStatus::Expired,
2658        ];
2659
2660        for status in final_statuses {
2661            let mut transaction = create_test_transaction();
2662            transaction.delete_at = None;
2663            transaction.status = status.clone();
2664
2665            let before_update = Utc::now();
2666            transaction.update_delete_at_if_final_status();
2667
2668            // Should set delete_at for final status
2669            assert!(
2670                transaction.delete_at.is_some(),
2671                "delete_at should be set for status: {:?}",
2672                status
2673            );
2674
2675            // Verify the timestamp is reasonable
2676            let delete_at_str = transaction.delete_at.unwrap();
2677            let delete_at = DateTime::parse_from_rfc3339(&delete_at_str)
2678                .expect("delete_at should be valid RFC3339")
2679                .with_timezone(&Utc);
2680
2681            // Should be approximately 3 hours from before_update
2682            let duration_from_before = delete_at.signed_duration_since(before_update);
2683            let expected_duration = Duration::hours(3);
2684            let tolerance = Duration::minutes(5); // Allow 5 minutes tolerance
2685
2686            // Debug information
2687            let actual_hours_at_runtime = ServerConfig::get_transaction_expiration_hours();
2688
2689            assert!(
2690                duration_from_before >= expected_duration - tolerance &&
2691                duration_from_before <= expected_duration + tolerance,
2692                "delete_at should be approximately 3 hours from now for status: {:?}. Duration from start: {:?}, Expected: {:?}, Config hours at runtime: {}",
2693                status, duration_from_before, expected_duration, actual_hours_at_runtime
2694            );
2695        }
2696
2697        // Cleanup
2698        env::remove_var("TRANSACTION_EXPIRATION_HOURS");
2699    }
2700
2701    #[test]
2702    fn test_update_delete_at_if_final_status_uses_default_expiration_hours() {
2703        let _lock = match ENV_MUTEX.lock() {
2704            Ok(guard) => guard,
2705            Err(poisoned) => poisoned.into_inner(),
2706        };
2707
2708        use chrono::{DateTime, Duration, Utc};
2709        use std::env;
2710
2711        // Remove env var to test default behavior
2712        env::remove_var("TRANSACTION_EXPIRATION_HOURS");
2713
2714        let mut transaction = create_test_transaction();
2715        transaction.delete_at = None;
2716        transaction.status = TransactionStatus::Confirmed;
2717
2718        let before_update = Utc::now();
2719        transaction.update_delete_at_if_final_status();
2720
2721        // Should set delete_at using default value (4 hours)
2722        assert!(transaction.delete_at.is_some());
2723
2724        let delete_at_str = transaction.delete_at.unwrap();
2725        let delete_at = DateTime::parse_from_rfc3339(&delete_at_str)
2726            .expect("delete_at should be valid RFC3339")
2727            .with_timezone(&Utc);
2728
2729        // Should be approximately 4 hours from before_update (default value)
2730        let duration_from_before = delete_at.signed_duration_since(before_update);
2731        let expected_duration = Duration::hours(4);
2732        let tolerance = Duration::minutes(5); // Allow 5 minutes tolerance
2733
2734        assert!(
2735            duration_from_before >= expected_duration - tolerance &&
2736            duration_from_before <= expected_duration + tolerance,
2737            "delete_at should be approximately 4 hours from now (default). Duration from start: {:?}, Expected: {:?}",
2738            duration_from_before, expected_duration
2739        );
2740    }
2741
2742    #[test]
2743    fn test_update_delete_at_if_final_status_with_custom_expiration_hours() {
2744        let _lock = match ENV_MUTEX.lock() {
2745            Ok(guard) => guard,
2746            Err(poisoned) => poisoned.into_inner(),
2747        };
2748
2749        use chrono::{DateTime, Duration, Utc};
2750        use std::env;
2751
2752        // Test with various custom expiration hours
2753        let test_cases = vec![1, 2, 6, 12]; // 1 hour, 2 hours, 6 hours, 12 hours
2754
2755        for expiration_hours in test_cases {
2756            env::set_var("TRANSACTION_EXPIRATION_HOURS", expiration_hours.to_string());
2757
2758            let mut transaction = create_test_transaction();
2759            transaction.delete_at = None;
2760            transaction.status = TransactionStatus::Failed;
2761
2762            let before_update = Utc::now();
2763            transaction.update_delete_at_if_final_status();
2764
2765            assert!(
2766                transaction.delete_at.is_some(),
2767                "delete_at should be set for {} hours",
2768                expiration_hours
2769            );
2770
2771            let delete_at_str = transaction.delete_at.unwrap();
2772            let delete_at = DateTime::parse_from_rfc3339(&delete_at_str)
2773                .expect("delete_at should be valid RFC3339")
2774                .with_timezone(&Utc);
2775
2776            let duration_from_before = delete_at.signed_duration_since(before_update);
2777            let expected_duration = Duration::hours(expiration_hours as i64);
2778            let tolerance = Duration::minutes(5); // Allow 5 minutes tolerance
2779
2780            assert!(
2781                duration_from_before >= expected_duration - tolerance &&
2782                duration_from_before <= expected_duration + tolerance,
2783                "delete_at should be approximately {} hours from now. Duration from start: {:?}, Expected: {:?}",
2784                expiration_hours, duration_from_before, expected_duration
2785            );
2786        }
2787
2788        // Cleanup
2789        env::remove_var("TRANSACTION_EXPIRATION_HOURS");
2790    }
2791
2792    #[test]
2793    fn test_calculate_delete_at_with_various_hours() {
2794        use chrono::{DateTime, Utc};
2795
2796        let test_cases = vec![0, 1, 6, 12, 24, 48];
2797
2798        for hours in test_cases {
2799            let before_calc = Utc::now();
2800            let result = TransactionRepoModel::calculate_delete_at(hours);
2801            let after_calc = Utc::now();
2802
2803            assert!(
2804                result.is_some(),
2805                "calculate_delete_at should return Some for {} hours",
2806                hours
2807            );
2808
2809            let delete_at_str = result.unwrap();
2810            let delete_at = DateTime::parse_from_rfc3339(&delete_at_str)
2811                .expect("Result should be valid RFC3339")
2812                .with_timezone(&Utc);
2813
2814            let expected_min =
2815                before_calc + chrono::Duration::hours(hours as i64) - chrono::Duration::seconds(1);
2816            let expected_max =
2817                after_calc + chrono::Duration::hours(hours as i64) + chrono::Duration::seconds(1);
2818
2819            assert!(
2820                delete_at >= expected_min && delete_at <= expected_max,
2821                "Calculated delete_at should be approximately {} hours from now. Got: {}, Expected between: {} and {}",
2822                hours, delete_at, expected_min, expected_max
2823            );
2824        }
2825    }
2826
2827    #[test]
2828    fn test_update_delete_at_if_final_status_idempotent() {
2829        let _lock = match ENV_MUTEX.lock() {
2830            Ok(guard) => guard,
2831            Err(poisoned) => poisoned.into_inner(),
2832        };
2833
2834        use std::env;
2835
2836        env::set_var("TRANSACTION_EXPIRATION_HOURS", "8");
2837
2838        let mut transaction = create_test_transaction();
2839        transaction.delete_at = None;
2840        transaction.status = TransactionStatus::Confirmed;
2841
2842        // First call should set delete_at
2843        transaction.update_delete_at_if_final_status();
2844        let first_delete_at = transaction.delete_at.clone();
2845        assert!(first_delete_at.is_some());
2846
2847        // Second call should not change delete_at (idempotent)
2848        transaction.update_delete_at_if_final_status();
2849        assert_eq!(transaction.delete_at, first_delete_at);
2850
2851        // Third call should not change delete_at (idempotent)
2852        transaction.update_delete_at_if_final_status();
2853        assert_eq!(transaction.delete_at, first_delete_at);
2854
2855        // Cleanup
2856        env::remove_var("TRANSACTION_EXPIRATION_HOURS");
2857    }
2858
2859    /// Helper function to create a test transaction for testing delete_at functionality
2860    fn create_test_transaction() -> TransactionRepoModel {
2861        TransactionRepoModel {
2862            id: "test-transaction-id".to_string(),
2863            relayer_id: "test-relayer-id".to_string(),
2864            status: TransactionStatus::Pending,
2865            status_reason: None,
2866            created_at: "2024-01-01T00:00:00Z".to_string(),
2867            sent_at: None,
2868            confirmed_at: None,
2869            valid_until: None,
2870            delete_at: None,
2871            network_data: NetworkTransactionData::Evm(EvmTransactionData {
2872                gas_price: None,
2873                gas_limit: Some(21000),
2874                nonce: Some(0),
2875                value: U256::from(0),
2876                data: None,
2877                from: "0x1234567890123456789012345678901234567890".to_string(),
2878                to: Some("0x0987654321098765432109876543210987654321".to_string()),
2879                chain_id: 1,
2880                hash: None,
2881                signature: None,
2882                speed: None,
2883                max_fee_per_gas: None,
2884                max_priority_fee_per_gas: None,
2885                raw: None,
2886            }),
2887            priced_at: None,
2888            hashes: vec![],
2889            network_type: NetworkType::Evm,
2890            noop_count: None,
2891            is_canceled: None,
2892        }
2893    }
2894
2895    #[test]
2896    fn test_apply_partial_update() {
2897        // Create a test transaction
2898        let mut transaction = create_test_transaction();
2899
2900        // Create a partial update request
2901        let update = TransactionUpdateRequest {
2902            status: Some(TransactionStatus::Confirmed),
2903            status_reason: Some("Transaction confirmed".to_string()),
2904            sent_at: Some("2023-01-01T12:00:00Z".to_string()),
2905            confirmed_at: Some("2023-01-01T12:05:00Z".to_string()),
2906            hashes: Some(vec!["0x123".to_string(), "0x456".to_string()]),
2907            is_canceled: Some(false),
2908            ..Default::default()
2909        };
2910
2911        // Apply the partial update
2912        transaction.apply_partial_update(update);
2913
2914        // Verify the updates were applied
2915        assert_eq!(transaction.status, TransactionStatus::Confirmed);
2916        assert_eq!(
2917            transaction.status_reason,
2918            Some("Transaction confirmed".to_string())
2919        );
2920        assert_eq!(
2921            transaction.sent_at,
2922            Some("2023-01-01T12:00:00Z".to_string())
2923        );
2924        assert_eq!(
2925            transaction.confirmed_at,
2926            Some("2023-01-01T12:05:00Z".to_string())
2927        );
2928        assert_eq!(
2929            transaction.hashes,
2930            vec!["0x123".to_string(), "0x456".to_string()]
2931        );
2932        assert_eq!(transaction.is_canceled, Some(false));
2933
2934        // Verify that delete_at was set because status changed to final
2935        assert!(transaction.delete_at.is_some());
2936    }
2937
2938    #[test]
2939    fn test_apply_partial_update_preserves_unchanged_fields() {
2940        // Create a test transaction with initial values
2941        let mut transaction = TransactionRepoModel {
2942            id: "test-tx".to_string(),
2943            relayer_id: "test-relayer".to_string(),
2944            status: TransactionStatus::Pending,
2945            status_reason: Some("Initial reason".to_string()),
2946            created_at: Utc::now().to_rfc3339(),
2947            sent_at: Some("2023-01-01T10:00:00Z".to_string()),
2948            confirmed_at: None,
2949            valid_until: None,
2950            delete_at: None,
2951            network_data: NetworkTransactionData::Evm(EvmTransactionData::default()),
2952            priced_at: None,
2953            hashes: vec!["0xoriginal".to_string()],
2954            network_type: NetworkType::Evm,
2955            noop_count: Some(5),
2956            is_canceled: Some(true),
2957        };
2958
2959        // Create a partial update that only changes status
2960        let update = TransactionUpdateRequest {
2961            status: Some(TransactionStatus::Sent),
2962            ..Default::default()
2963        };
2964
2965        // Apply the partial update
2966        transaction.apply_partial_update(update);
2967
2968        // Verify only status changed, other fields preserved
2969        assert_eq!(transaction.status, TransactionStatus::Sent);
2970        assert_eq!(
2971            transaction.status_reason,
2972            Some("Initial reason".to_string())
2973        );
2974        assert_eq!(
2975            transaction.sent_at,
2976            Some("2023-01-01T10:00:00Z".to_string())
2977        );
2978        assert_eq!(transaction.confirmed_at, None);
2979        assert_eq!(transaction.hashes, vec!["0xoriginal".to_string()]);
2980        assert_eq!(transaction.noop_count, Some(5));
2981        assert_eq!(transaction.is_canceled, Some(true));
2982
2983        // Status is not final, so delete_at should remain None
2984        assert!(transaction.delete_at.is_none());
2985    }
2986
2987    #[test]
2988    fn test_apply_partial_update_empty_update() {
2989        // Create a test transaction
2990        let mut transaction = create_test_transaction();
2991        let original_transaction = transaction.clone();
2992
2993        // Apply an empty update
2994        let update = TransactionUpdateRequest::default();
2995        transaction.apply_partial_update(update);
2996
2997        // Verify nothing changed
2998        assert_eq!(transaction.id, original_transaction.id);
2999        assert_eq!(transaction.status, original_transaction.status);
3000        assert_eq!(
3001            transaction.status_reason,
3002            original_transaction.status_reason
3003        );
3004        assert_eq!(transaction.sent_at, original_transaction.sent_at);
3005        assert_eq!(transaction.confirmed_at, original_transaction.confirmed_at);
3006        assert_eq!(transaction.hashes, original_transaction.hashes);
3007        assert_eq!(transaction.noop_count, original_transaction.noop_count);
3008        assert_eq!(transaction.is_canceled, original_transaction.is_canceled);
3009        assert_eq!(transaction.delete_at, original_transaction.delete_at);
3010    }
3011}