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    pub signatures: Vec<DecoratedSignature>,
538    pub hash: Option<String>,
539    pub simulation_transaction_data: Option<String>,
540    pub transaction_input: TransactionInput,
541    pub signed_envelope_xdr: Option<String>,
542}
543
544impl StellarTransactionData {
545    /// Resets the transaction data to its pre-prepare state by clearing all fields
546    /// that are populated during the prepare and submit phases.
547    ///
548    /// Fields preserved (from initial creation):
549    /// - source_account, network_passphrase, memo, valid_until, transaction_input
550    ///
551    /// Fields reset to None/empty:
552    /// - fee, sequence_number, signatures, signed_envelope_xdr, hash, simulation_transaction_data
553    pub fn reset_to_pre_prepare_state(mut self) -> Self {
554        // Reset all fields populated during prepare phase
555        self.fee = None;
556        self.sequence_number = None;
557        self.signatures = vec![];
558        self.signed_envelope_xdr = None;
559        self.simulation_transaction_data = None;
560
561        // Reset fields populated during submit phase
562        self.hash = None;
563
564        self
565    }
566
567    /// Updates the Stellar transaction data with a specific sequence number.
568    ///
569    /// # Arguments
570    /// * `sequence_number` - The sequence number for the Stellar account
571    ///
572    /// # Returns
573    /// The updated `StellarTransactionData` with the specified sequence number
574    pub fn with_sequence_number(mut self, sequence_number: i64) -> Self {
575        self.sequence_number = Some(sequence_number);
576        self
577    }
578
579    /// Updates the Stellar transaction data with the actual fee charged by the network.
580    ///
581    /// # Arguments
582    /// * `fee` - The actual fee charged in stroops
583    ///
584    /// # Returns
585    /// The updated `StellarTransactionData` with the specified fee
586    pub fn with_fee(mut self, fee: u32) -> Self {
587        self.fee = Some(fee);
588        self
589    }
590
591    /// Builds an unsigned envelope from any transaction input.
592    ///
593    /// Returns an envelope without signatures, suitable for simulation and fee calculation.
594    ///
595    /// # Returns
596    /// * `Ok(TransactionEnvelope)` containing the unsigned transaction
597    /// * `Err(SignerError)` if the transaction data cannot be converted
598    pub fn build_unsigned_envelope(&self) -> Result<TransactionEnvelope, SignerError> {
599        match &self.transaction_input {
600            TransactionInput::Operations(_) => {
601                // Build from operations without signatures
602                self.build_envelope_from_operations_unsigned()
603            }
604            TransactionInput::UnsignedXdr(xdr) => {
605                // Parse the XDR as-is (already unsigned)
606                self.parse_xdr_envelope(xdr)
607            }
608            TransactionInput::SignedXdr { xdr, .. } => {
609                // Parse the inner transaction (for fee-bump cases)
610                self.parse_xdr_envelope(xdr)
611            }
612        }
613    }
614
615    /// Gets the transaction envelope for simulation purposes.
616    ///
617    /// Convenience method that delegates to build_unsigned_envelope().
618    ///
619    /// # Returns
620    /// * `Ok(TransactionEnvelope)` containing the unsigned transaction
621    /// * `Err(SignerError)` if the transaction data cannot be converted
622    pub fn get_envelope_for_simulation(&self) -> Result<TransactionEnvelope, SignerError> {
623        self.build_unsigned_envelope()
624    }
625
626    /// Builds a signed envelope ready for submission to the network.
627    ///
628    /// Uses cached signed_envelope_xdr if available, otherwise builds from components.
629    ///
630    /// # Returns
631    /// * `Ok(TransactionEnvelope)` containing the signed transaction
632    /// * `Err(SignerError)` if the transaction data cannot be converted
633    pub fn build_signed_envelope(&self) -> Result<TransactionEnvelope, SignerError> {
634        // If we have a cached signed envelope, use it
635        if let Some(ref xdr) = self.signed_envelope_xdr {
636            return self.parse_xdr_envelope(xdr);
637        }
638
639        // Otherwise, build from components
640        match &self.transaction_input {
641            TransactionInput::Operations(_) => {
642                // Build from operations with signatures
643                self.build_envelope_from_operations_signed()
644            }
645            TransactionInput::UnsignedXdr(xdr) => {
646                // Parse and attach signatures
647                let envelope = self.parse_xdr_envelope(xdr)?;
648                self.attach_signatures_to_envelope(envelope)
649            }
650            TransactionInput::SignedXdr { xdr, .. } => {
651                // Already signed
652                self.parse_xdr_envelope(xdr)
653            }
654        }
655    }
656
657    /// Gets the transaction envelope for submission to the network.
658    ///
659    /// Convenience method that delegates to build_signed_envelope().
660    ///
661    /// # Returns
662    /// * `Ok(TransactionEnvelope)` containing the signed transaction
663    /// * `Err(SignerError)` if the transaction data cannot be converted
664    pub fn get_envelope_for_submission(&self) -> Result<TransactionEnvelope, SignerError> {
665        self.build_signed_envelope()
666    }
667
668    // Helper method to build unsigned envelope from operations
669    fn build_envelope_from_operations_unsigned(&self) -> Result<TransactionEnvelope, SignerError> {
670        let tx = SorobanTransaction::try_from(self.clone())?;
671        Ok(TransactionEnvelope::Tx(TransactionV1Envelope {
672            tx,
673            signatures: VecM::default(),
674        }))
675    }
676
677    // Helper method to build signed envelope from operations
678    fn build_envelope_from_operations_signed(&self) -> Result<TransactionEnvelope, SignerError> {
679        let tx = SorobanTransaction::try_from(self.clone())?;
680        let signatures = VecM::try_from(self.signatures.clone())
681            .map_err(|_| SignerError::ConversionError("too many signatures".into()))?;
682        Ok(TransactionEnvelope::Tx(TransactionV1Envelope {
683            tx,
684            signatures,
685        }))
686    }
687
688    // Helper method to parse XDR envelope
689    fn parse_xdr_envelope(&self, xdr: &str) -> Result<TransactionEnvelope, SignerError> {
690        use soroban_rs::xdr::{Limits, ReadXdr};
691        TransactionEnvelope::from_xdr_base64(xdr, Limits::none())
692            .map_err(|e| SignerError::ConversionError(format!("Invalid XDR: {}", e)))
693    }
694
695    // Helper method to attach signatures to an envelope
696    fn attach_signatures_to_envelope(
697        &self,
698        envelope: TransactionEnvelope,
699    ) -> Result<TransactionEnvelope, SignerError> {
700        use soroban_rs::xdr::{Limits, ReadXdr, WriteXdr};
701
702        // Serialize and re-parse to get a mutable version
703        let envelope_xdr = envelope.to_xdr_base64(Limits::none()).map_err(|e| {
704            SignerError::ConversionError(format!("Failed to serialize envelope: {}", e))
705        })?;
706
707        let mut envelope = TransactionEnvelope::from_xdr_base64(&envelope_xdr, Limits::none())
708            .map_err(|e| {
709                SignerError::ConversionError(format!("Failed to parse envelope: {}", e))
710            })?;
711
712        let sigs = VecM::try_from(self.signatures.clone())
713            .map_err(|_| SignerError::ConversionError("too many signatures".into()))?;
714
715        match &mut envelope {
716            TransactionEnvelope::Tx(ref mut v1) => v1.signatures = sigs,
717            TransactionEnvelope::TxV0(ref mut v0) => v0.signatures = sigs,
718            TransactionEnvelope::TxFeeBump(_) => {
719                return Err(SignerError::ConversionError(
720                    "Cannot attach signatures to fee-bump transaction directly".into(),
721                ));
722            }
723        }
724
725        Ok(envelope)
726    }
727
728    /// Updates instance with the given signature appended to the signatures list.
729    ///
730    /// # Arguments
731    /// * `sig` - The decorated signature to append
732    ///
733    /// # Returns
734    /// The updated `StellarTransactionData` with the new signature added
735    pub fn attach_signature(mut self, sig: DecoratedSignature) -> Self {
736        self.signatures.push(sig);
737        self
738    }
739
740    /// Updates instance with the transaction hash populated.
741    ///
742    /// # Arguments
743    /// * `hash` - The transaction hash to set
744    ///
745    /// # Returns
746    /// The updated `StellarTransactionData` with the hash field set
747    pub fn with_hash(mut self, hash: String) -> Self {
748        self.hash = Some(hash);
749        self
750    }
751
752    /// Return a new instance with simulation data applied (fees and transaction extension).
753    pub fn with_simulation_data(
754        mut self,
755        sim_response: soroban_rs::stellar_rpc_client::SimulateTransactionResponse,
756        operations_count: u64,
757    ) -> Result<Self, SignerError> {
758        use tracing::info;
759
760        // Update fee based on simulation (using soroban-helpers formula)
761        let inclusion_fee = operations_count * STELLAR_DEFAULT_TRANSACTION_FEE as u64;
762        let resource_fee = sim_response.min_resource_fee;
763
764        let updated_fee = u32::try_from(inclusion_fee + resource_fee)
765            .map_err(|_| SignerError::ConversionError("Fee too high".to_string()))?
766            .max(STELLAR_DEFAULT_TRANSACTION_FEE);
767        self.fee = Some(updated_fee);
768
769        // Store simulation transaction data for TransactionExt::V1
770        self.simulation_transaction_data = Some(sim_response.transaction_data);
771
772        info!(
773            "Applied simulation fee: {} stroops and stored transaction extension data",
774            updated_fee
775        );
776        Ok(self)
777    }
778}
779
780impl
781    TryFrom<(
782        &NetworkTransactionRequest,
783        &RelayerRepoModel,
784        &NetworkRepoModel,
785    )> for TransactionRepoModel
786{
787    type Error = RelayerError;
788
789    fn try_from(
790        (request, relayer_model, network_model): (
791            &NetworkTransactionRequest,
792            &RelayerRepoModel,
793            &NetworkRepoModel,
794        ),
795    ) -> Result<Self, Self::Error> {
796        let now = Utc::now().to_rfc3339();
797
798        match request {
799            NetworkTransactionRequest::Evm(evm_request) => {
800                let network = EvmNetwork::try_from(network_model.clone())?;
801                Ok(Self {
802                    id: Uuid::new_v4().to_string(),
803                    relayer_id: relayer_model.id.clone(),
804                    status: TransactionStatus::Pending,
805                    status_reason: None,
806                    created_at: now,
807                    sent_at: None,
808                    confirmed_at: None,
809                    valid_until: evm_request.valid_until.clone(),
810                    delete_at: None,
811                    network_type: NetworkType::Evm,
812                    network_data: NetworkTransactionData::Evm(EvmTransactionData {
813                        gas_price: evm_request.gas_price,
814                        gas_limit: evm_request.gas_limit,
815                        nonce: None,
816                        value: evm_request.value,
817                        data: evm_request.data.clone(),
818                        from: relayer_model.address.clone(),
819                        to: evm_request.to.clone(),
820                        chain_id: network.id(),
821                        hash: None,
822                        signature: None,
823                        speed: evm_request.speed.clone(),
824                        max_fee_per_gas: evm_request.max_fee_per_gas,
825                        max_priority_fee_per_gas: evm_request.max_priority_fee_per_gas,
826                        raw: None,
827                    }),
828                    priced_at: None,
829                    hashes: Vec::new(),
830                    noop_count: None,
831                    is_canceled: Some(false),
832                })
833            }
834            NetworkTransactionRequest::Solana(solana_request) => Ok(Self {
835                id: Uuid::new_v4().to_string(),
836                relayer_id: relayer_model.id.clone(),
837                status: TransactionStatus::Pending,
838                status_reason: None,
839                created_at: now,
840                sent_at: None,
841                confirmed_at: None,
842                valid_until: None,
843                delete_at: None,
844                network_type: NetworkType::Solana,
845                network_data: NetworkTransactionData::Solana(SolanaTransactionData {
846                    transaction: solana_request.transaction.clone().into_inner(),
847                    signature: None,
848                }),
849                priced_at: None,
850                hashes: Vec::new(),
851                noop_count: None,
852                is_canceled: Some(false),
853            }),
854            NetworkTransactionRequest::Stellar(stellar_request) => {
855                // Store the source account before consuming the request
856                let source_account = stellar_request.source_account.clone();
857
858                // Create the TransactionData before consuming the request
859                let stellar_data = StellarTransactionData {
860                    source_account: source_account.unwrap_or_else(|| relayer_model.address.clone()),
861                    memo: stellar_request.memo.clone(),
862                    valid_until: stellar_request.valid_until.clone(),
863                    network_passphrase: StellarNetwork::try_from(network_model.clone())?.passphrase,
864                    signatures: Vec::new(),
865                    hash: None,
866                    fee: None,
867                    sequence_number: None,
868                    simulation_transaction_data: None,
869                    transaction_input: TransactionInput::from_stellar_request(stellar_request)
870                        .map_err(|e| RelayerError::ValidationError(e.to_string()))?,
871                    signed_envelope_xdr: None,
872                };
873
874                Ok(Self {
875                    id: Uuid::new_v4().to_string(),
876                    relayer_id: relayer_model.id.clone(),
877                    status: TransactionStatus::Pending,
878                    status_reason: None,
879                    created_at: now,
880                    sent_at: None,
881                    confirmed_at: None,
882                    valid_until: None,
883                    delete_at: None,
884                    network_type: NetworkType::Stellar,
885                    network_data: NetworkTransactionData::Stellar(stellar_data),
886                    priced_at: None,
887                    hashes: Vec::new(),
888                    noop_count: None,
889                    is_canceled: Some(false),
890                })
891            }
892        }
893    }
894}
895
896impl EvmTransactionData {
897    /// Converts the transaction's 'to' field to an Alloy Address.
898    ///
899    /// # Returns
900    /// * `Ok(Some(AlloyAddress))` if the 'to' field contains a valid address
901    /// * `Ok(None)` if the 'to' field is None or empty (contract creation)
902    /// * `Err(SignerError)` if the address format is invalid
903    pub fn to_address(&self) -> Result<Option<AlloyAddress>, SignerError> {
904        Ok(match self.to.as_deref().filter(|s| !s.is_empty()) {
905            Some(addr_str) => Some(AlloyAddress::from_str(addr_str).map_err(|e| {
906                AddressError::ConversionError(format!("Invalid 'to' address: {}", e))
907            })?),
908            None => None,
909        })
910    }
911
912    /// Converts the transaction's data field from hex string to bytes.
913    ///
914    /// # Returns
915    /// * `Ok(Bytes)` containing the decoded transaction data
916    /// * `Err(SignerError)` if the hex string is invalid
917    pub fn data_to_bytes(&self) -> Result<Bytes, SignerError> {
918        Bytes::from_str(self.data.as_deref().unwrap_or(""))
919            .map_err(|e| SignerError::SigningError(format!("Invalid transaction data: {}", e)))
920    }
921}
922
923impl TryFrom<NetworkTransactionData> for TxLegacy {
924    type Error = SignerError;
925
926    fn try_from(tx: NetworkTransactionData) -> Result<Self, Self::Error> {
927        match tx {
928            NetworkTransactionData::Evm(tx) => {
929                let tx_kind = match tx.to_address()? {
930                    Some(addr) => TxKind::Call(addr),
931                    None => TxKind::Create,
932                };
933
934                Ok(Self {
935                    chain_id: Some(tx.chain_id),
936                    nonce: tx.nonce.unwrap_or(0),
937                    gas_limit: tx.gas_limit.unwrap_or(DEFAULT_GAS_LIMIT),
938                    gas_price: tx.gas_price.unwrap_or(0),
939                    to: tx_kind,
940                    value: tx.value,
941                    input: tx.data_to_bytes()?,
942                })
943            }
944            _ => Err(SignerError::SigningError(
945                "Not an EVM transaction".to_string(),
946            )),
947        }
948    }
949}
950
951impl TryFrom<NetworkTransactionData> for TxEip1559 {
952    type Error = SignerError;
953
954    fn try_from(tx: NetworkTransactionData) -> Result<Self, Self::Error> {
955        match tx {
956            NetworkTransactionData::Evm(tx) => {
957                let tx_kind = match tx.to_address()? {
958                    Some(addr) => TxKind::Call(addr),
959                    None => TxKind::Create,
960                };
961
962                Ok(Self {
963                    chain_id: tx.chain_id,
964                    nonce: tx.nonce.unwrap_or(0),
965                    gas_limit: tx.gas_limit.unwrap_or(DEFAULT_GAS_LIMIT),
966                    max_fee_per_gas: tx.max_fee_per_gas.unwrap_or(0),
967                    max_priority_fee_per_gas: tx.max_priority_fee_per_gas.unwrap_or(0),
968                    to: tx_kind,
969                    value: tx.value,
970                    access_list: AccessList::default(),
971                    input: tx.data_to_bytes()?,
972                })
973            }
974            _ => Err(SignerError::SigningError(
975                "Not an EVM transaction".to_string(),
976            )),
977        }
978    }
979}
980
981impl TryFrom<&EvmTransactionData> for TxLegacy {
982    type Error = SignerError;
983
984    fn try_from(tx: &EvmTransactionData) -> Result<Self, Self::Error> {
985        let tx_kind = match tx.to_address()? {
986            Some(addr) => TxKind::Call(addr),
987            None => TxKind::Create,
988        };
989
990        Ok(Self {
991            chain_id: Some(tx.chain_id),
992            nonce: tx.nonce.unwrap_or(0),
993            gas_limit: tx.gas_limit.unwrap_or(DEFAULT_GAS_LIMIT),
994            gas_price: tx.gas_price.unwrap_or(0),
995            to: tx_kind,
996            value: tx.value,
997            input: tx.data_to_bytes()?,
998        })
999    }
1000}
1001
1002impl TryFrom<EvmTransactionData> for TxLegacy {
1003    type Error = SignerError;
1004
1005    fn try_from(tx: EvmTransactionData) -> Result<Self, Self::Error> {
1006        Self::try_from(&tx)
1007    }
1008}
1009
1010impl TryFrom<&EvmTransactionData> for TxEip1559 {
1011    type Error = SignerError;
1012
1013    fn try_from(tx: &EvmTransactionData) -> Result<Self, Self::Error> {
1014        let tx_kind = match tx.to_address()? {
1015            Some(addr) => TxKind::Call(addr),
1016            None => TxKind::Create,
1017        };
1018
1019        Ok(Self {
1020            chain_id: tx.chain_id,
1021            nonce: tx.nonce.unwrap_or(0),
1022            gas_limit: tx.gas_limit.unwrap_or(DEFAULT_GAS_LIMIT),
1023            max_fee_per_gas: tx.max_fee_per_gas.unwrap_or(0),
1024            max_priority_fee_per_gas: tx.max_priority_fee_per_gas.unwrap_or(0),
1025            to: tx_kind,
1026            value: tx.value,
1027            access_list: AccessList::default(),
1028            input: tx.data_to_bytes()?,
1029        })
1030    }
1031}
1032
1033impl TryFrom<EvmTransactionData> for TxEip1559 {
1034    type Error = SignerError;
1035
1036    fn try_from(tx: EvmTransactionData) -> Result<Self, Self::Error> {
1037        Self::try_from(&tx)
1038    }
1039}
1040
1041impl From<&[u8; 65]> for EvmTransactionDataSignature {
1042    fn from(bytes: &[u8; 65]) -> Self {
1043        Self {
1044            r: hex::encode(&bytes[0..32]),
1045            s: hex::encode(&bytes[32..64]),
1046            v: bytes[64],
1047            sig: hex::encode(bytes),
1048        }
1049    }
1050}
1051
1052#[cfg(test)]
1053mod tests {
1054    use lazy_static::lazy_static;
1055    use soroban_rs::xdr::{BytesM, Signature, SignatureHint};
1056    use std::sync::Mutex;
1057
1058    use super::*;
1059    use crate::{
1060        config::{
1061            EvmNetworkConfig, NetworkConfigCommon, SolanaNetworkConfig, StellarNetworkConfig,
1062        },
1063        models::{
1064            network::NetworkConfigData,
1065            relayer::{
1066                RelayerEvmPolicy, RelayerNetworkPolicy, RelayerSolanaPolicy, RelayerStellarPolicy,
1067            },
1068            transaction::stellar::AssetSpec,
1069            EncodedSerializedTransaction,
1070        },
1071    };
1072
1073    // Use a mutex to ensure tests don't run in parallel when modifying env vars
1074    lazy_static! {
1075        static ref ENV_MUTEX: Mutex<()> = Mutex::new(());
1076    }
1077
1078    #[test]
1079    fn test_signature_from_bytes() {
1080        let test_bytes: [u8; 65] = [
1081            1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
1082            25, 26, 27, 28, 29, 30, 31, 32, // r (32 bytes)
1083            33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54,
1084            55, 56, 57, 58, 59, 60, 61, 62, 63, 64, // s (32 bytes)
1085            27, // v (1 byte)
1086        ];
1087
1088        let signature = EvmTransactionDataSignature::from(&test_bytes);
1089
1090        assert_eq!(signature.r.len(), 64); // 32 bytes in hex
1091        assert_eq!(signature.s.len(), 64); // 32 bytes in hex
1092        assert_eq!(signature.v, 27);
1093        assert_eq!(signature.sig.len(), 130); // 65 bytes in hex
1094    }
1095
1096    #[test]
1097    fn test_stellar_transaction_data_reset_to_pre_prepare_state() {
1098        let stellar_data = StellarTransactionData {
1099            source_account: "GTEST".to_string(),
1100            fee: Some(100),
1101            sequence_number: Some(42),
1102            memo: Some(MemoSpec::Text {
1103                value: "test memo".to_string(),
1104            }),
1105            valid_until: Some("2024-12-31".to_string()),
1106            network_passphrase: "Test Network".to_string(),
1107            signatures: vec![], // Simplified - empty for test
1108            hash: Some("test-hash".to_string()),
1109            simulation_transaction_data: Some("simulation-data".to_string()),
1110            transaction_input: TransactionInput::Operations(vec![OperationSpec::Payment {
1111                destination: "GDEST".to_string(),
1112                amount: 1000,
1113                asset: AssetSpec::Native,
1114            }]),
1115            signed_envelope_xdr: Some("signed-xdr".to_string()),
1116        };
1117
1118        let reset_data = stellar_data.clone().reset_to_pre_prepare_state();
1119
1120        // Fields that should be preserved
1121        assert_eq!(reset_data.source_account, stellar_data.source_account);
1122        assert_eq!(reset_data.memo, stellar_data.memo);
1123        assert_eq!(reset_data.valid_until, stellar_data.valid_until);
1124        assert_eq!(
1125            reset_data.network_passphrase,
1126            stellar_data.network_passphrase
1127        );
1128        assert!(matches!(
1129            reset_data.transaction_input,
1130            TransactionInput::Operations(_)
1131        ));
1132
1133        // Fields that should be reset
1134        assert_eq!(reset_data.fee, None);
1135        assert_eq!(reset_data.sequence_number, None);
1136        assert!(reset_data.signatures.is_empty());
1137        assert_eq!(reset_data.hash, None);
1138        assert_eq!(reset_data.simulation_transaction_data, None);
1139        assert_eq!(reset_data.signed_envelope_xdr, None);
1140    }
1141
1142    #[test]
1143    fn test_transaction_repo_model_create_reset_update_request() {
1144        let stellar_data = StellarTransactionData {
1145            source_account: "GTEST".to_string(),
1146            fee: Some(100),
1147            sequence_number: Some(42),
1148            memo: None,
1149            valid_until: None,
1150            network_passphrase: "Test Network".to_string(),
1151            signatures: vec![],
1152            hash: Some("test-hash".to_string()),
1153            simulation_transaction_data: None,
1154            transaction_input: TransactionInput::Operations(vec![]),
1155            signed_envelope_xdr: Some("signed-xdr".to_string()),
1156        };
1157
1158        let tx = TransactionRepoModel {
1159            id: "tx-1".to_string(),
1160            relayer_id: "relayer-1".to_string(),
1161            status: TransactionStatus::Failed,
1162            status_reason: Some("Bad sequence".to_string()),
1163            created_at: "2024-01-01".to_string(),
1164            sent_at: Some("2024-01-02".to_string()),
1165            confirmed_at: Some("2024-01-03".to_string()),
1166            valid_until: None,
1167            network_data: NetworkTransactionData::Stellar(stellar_data),
1168            priced_at: None,
1169            hashes: vec!["hash1".to_string(), "hash2".to_string()],
1170            network_type: NetworkType::Stellar,
1171            noop_count: None,
1172            is_canceled: None,
1173            delete_at: None,
1174        };
1175
1176        let update_req = tx.create_reset_update_request().unwrap();
1177
1178        // Check common fields
1179        assert_eq!(update_req.status, Some(TransactionStatus::Pending));
1180        assert_eq!(update_req.status_reason, None);
1181        assert_eq!(update_req.sent_at, None);
1182        assert_eq!(update_req.confirmed_at, None);
1183        assert_eq!(update_req.hashes, Some(vec![]));
1184
1185        // Check that network data was reset
1186        if let Some(NetworkTransactionData::Stellar(reset_data)) = update_req.network_data {
1187            assert_eq!(reset_data.fee, None);
1188            assert_eq!(reset_data.sequence_number, None);
1189            assert_eq!(reset_data.hash, None);
1190            assert_eq!(reset_data.signed_envelope_xdr, None);
1191        } else {
1192            panic!("Expected Stellar network data");
1193        }
1194    }
1195
1196    // Create a helper function to generate a sample EvmTransactionData for testing
1197    fn create_sample_evm_tx_data() -> EvmTransactionData {
1198        EvmTransactionData {
1199            gas_price: Some(20_000_000_000),
1200            gas_limit: Some(21000),
1201            nonce: Some(5),
1202            value: U256::from(1000000000000000000u128), // 1 ETH
1203            data: Some("0x".to_string()),
1204            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
1205            to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
1206            chain_id: 1,
1207            hash: None,
1208            signature: None,
1209            speed: None,
1210            max_fee_per_gas: None,
1211            max_priority_fee_per_gas: None,
1212            raw: None,
1213        }
1214    }
1215
1216    // Tests for EvmTransactionData methods
1217    #[test]
1218    fn test_evm_tx_with_price_params() {
1219        let tx_data = create_sample_evm_tx_data();
1220        let price_params = PriceParams {
1221            gas_price: None,
1222            max_fee_per_gas: Some(30_000_000_000),
1223            max_priority_fee_per_gas: Some(2_000_000_000),
1224            is_min_bumped: None,
1225            extra_fee: None,
1226            total_cost: U256::ZERO,
1227        };
1228
1229        let updated_tx = tx_data.with_price_params(price_params);
1230
1231        assert_eq!(updated_tx.max_fee_per_gas, Some(30_000_000_000));
1232        assert_eq!(updated_tx.max_priority_fee_per_gas, Some(2_000_000_000));
1233    }
1234
1235    #[test]
1236    fn test_evm_tx_with_gas_estimate() {
1237        let tx_data = create_sample_evm_tx_data();
1238        let new_gas_limit = 30000;
1239
1240        let updated_tx = tx_data.with_gas_estimate(new_gas_limit);
1241
1242        assert_eq!(updated_tx.gas_limit, Some(new_gas_limit));
1243    }
1244
1245    #[test]
1246    fn test_evm_tx_with_nonce() {
1247        let tx_data = create_sample_evm_tx_data();
1248        let new_nonce = 10;
1249
1250        let updated_tx = tx_data.with_nonce(new_nonce);
1251
1252        assert_eq!(updated_tx.nonce, Some(new_nonce));
1253    }
1254
1255    #[test]
1256    fn test_evm_tx_with_signed_transaction_data() {
1257        let tx_data = create_sample_evm_tx_data();
1258
1259        let signature = EvmTransactionDataSignature {
1260            r: "r_value".to_string(),
1261            s: "s_value".to_string(),
1262            v: 27,
1263            sig: "signature_value".to_string(),
1264        };
1265
1266        let signed_tx_response = SignTransactionResponseEvm {
1267            signature,
1268            hash: "0xabcdef1234567890".to_string(),
1269            raw: vec![1, 2, 3, 4, 5],
1270        };
1271
1272        let updated_tx = tx_data.with_signed_transaction_data(signed_tx_response);
1273
1274        assert_eq!(updated_tx.signature.as_ref().unwrap().r, "r_value");
1275        assert_eq!(updated_tx.signature.as_ref().unwrap().s, "s_value");
1276        assert_eq!(updated_tx.signature.as_ref().unwrap().v, 27);
1277        assert_eq!(updated_tx.hash, Some("0xabcdef1234567890".to_string()));
1278        assert_eq!(updated_tx.raw, Some(vec![1, 2, 3, 4, 5]));
1279    }
1280
1281    #[test]
1282    fn test_evm_tx_to_address() {
1283        // Test with valid address
1284        let tx_data = create_sample_evm_tx_data();
1285        let address_result = tx_data.to_address();
1286        assert!(address_result.is_ok());
1287        let address_option = address_result.unwrap();
1288        assert!(address_option.is_some());
1289        assert_eq!(
1290            address_option.unwrap().to_string().to_lowercase(),
1291            "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_lowercase()
1292        );
1293
1294        // Test with None address (contract creation)
1295        let mut contract_creation_tx = create_sample_evm_tx_data();
1296        contract_creation_tx.to = None;
1297        let address_result = contract_creation_tx.to_address();
1298        assert!(address_result.is_ok());
1299        assert!(address_result.unwrap().is_none());
1300
1301        // Test with empty address string
1302        let mut empty_address_tx = create_sample_evm_tx_data();
1303        empty_address_tx.to = Some("".to_string());
1304        let address_result = empty_address_tx.to_address();
1305        assert!(address_result.is_ok());
1306        assert!(address_result.unwrap().is_none());
1307
1308        // Test with invalid address
1309        let mut invalid_address_tx = create_sample_evm_tx_data();
1310        invalid_address_tx.to = Some("0xINVALID".to_string());
1311        let address_result = invalid_address_tx.to_address();
1312        assert!(address_result.is_err());
1313    }
1314
1315    #[test]
1316    fn test_evm_tx_data_to_bytes() {
1317        // Test with valid hex data
1318        let mut tx_data = create_sample_evm_tx_data();
1319        tx_data.data = Some("0x1234".to_string());
1320        let bytes_result = tx_data.data_to_bytes();
1321        assert!(bytes_result.is_ok());
1322        assert_eq!(bytes_result.unwrap().as_ref(), &[0x12, 0x34]);
1323
1324        // Test with empty data
1325        tx_data.data = Some("".to_string());
1326        assert!(tx_data.data_to_bytes().is_ok());
1327
1328        // Test with None data
1329        tx_data.data = None;
1330        assert!(tx_data.data_to_bytes().is_ok());
1331
1332        // Test with invalid hex data
1333        tx_data.data = Some("0xZZ".to_string());
1334        assert!(tx_data.data_to_bytes().is_err());
1335    }
1336
1337    // Tests for EvmTransactionDataTrait implementation
1338    #[test]
1339    fn test_evm_tx_is_legacy() {
1340        let mut tx_data = create_sample_evm_tx_data();
1341
1342        // Legacy transaction has gas_price
1343        assert!(tx_data.is_legacy());
1344
1345        // Not legacy if gas_price is None
1346        tx_data.gas_price = None;
1347        assert!(!tx_data.is_legacy());
1348    }
1349
1350    #[test]
1351    fn test_evm_tx_is_eip1559() {
1352        let mut tx_data = create_sample_evm_tx_data();
1353
1354        // Not EIP-1559 initially
1355        assert!(!tx_data.is_eip1559());
1356
1357        // Set EIP-1559 fields
1358        tx_data.max_fee_per_gas = Some(30_000_000_000);
1359        tx_data.max_priority_fee_per_gas = Some(2_000_000_000);
1360        assert!(tx_data.is_eip1559());
1361
1362        // Not EIP-1559 if one field is missing
1363        tx_data.max_priority_fee_per_gas = None;
1364        assert!(!tx_data.is_eip1559());
1365    }
1366
1367    #[test]
1368    fn test_evm_tx_is_speed() {
1369        let mut tx_data = create_sample_evm_tx_data();
1370
1371        // No speed initially
1372        assert!(!tx_data.is_speed());
1373
1374        // Set speed
1375        tx_data.speed = Some(Speed::Fast);
1376        assert!(tx_data.is_speed());
1377    }
1378
1379    // Tests for NetworkTransactionData methods
1380    #[test]
1381    fn test_network_tx_data_get_evm_transaction_data() {
1382        let evm_tx_data = create_sample_evm_tx_data();
1383        let network_data = NetworkTransactionData::Evm(evm_tx_data.clone());
1384
1385        // Should succeed for EVM data
1386        let result = network_data.get_evm_transaction_data();
1387        assert!(result.is_ok());
1388        assert_eq!(result.unwrap().chain_id, evm_tx_data.chain_id);
1389
1390        // Should fail for non-EVM data
1391        let solana_data = NetworkTransactionData::Solana(SolanaTransactionData {
1392            transaction: "transaction_123".to_string(),
1393            signature: None,
1394        });
1395        assert!(solana_data.get_evm_transaction_data().is_err());
1396    }
1397
1398    #[test]
1399    fn test_network_tx_data_get_solana_transaction_data() {
1400        let solana_tx_data = SolanaTransactionData {
1401            transaction: "transaction_123".to_string(),
1402            signature: None,
1403        };
1404        let network_data = NetworkTransactionData::Solana(solana_tx_data.clone());
1405
1406        // Should succeed for Solana data
1407        let result = network_data.get_solana_transaction_data();
1408        assert!(result.is_ok());
1409        assert_eq!(result.unwrap().transaction, solana_tx_data.transaction);
1410
1411        // Should fail for non-Solana data
1412        let evm_data = NetworkTransactionData::Evm(create_sample_evm_tx_data());
1413        assert!(evm_data.get_solana_transaction_data().is_err());
1414    }
1415
1416    #[test]
1417    fn test_network_tx_data_get_stellar_transaction_data() {
1418        let stellar_tx_data = StellarTransactionData {
1419            source_account: "account123".to_string(),
1420            fee: Some(100),
1421            sequence_number: Some(5),
1422            memo: Some(MemoSpec::Text {
1423                value: "Test memo".to_string(),
1424            }),
1425            valid_until: Some("2025-01-01T00:00:00Z".to_string()),
1426            network_passphrase: "Test SDF Network ; September 2015".to_string(),
1427            signatures: Vec::new(),
1428            hash: Some("hash123".to_string()),
1429            simulation_transaction_data: None,
1430            transaction_input: TransactionInput::Operations(vec![OperationSpec::Payment {
1431                destination: "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ".to_string(),
1432                amount: 100000000, // 10 XLM in stroops
1433                asset: AssetSpec::Native,
1434            }]),
1435            signed_envelope_xdr: None,
1436        };
1437        let network_data = NetworkTransactionData::Stellar(stellar_tx_data.clone());
1438
1439        // Should succeed for Stellar data
1440        let result = network_data.get_stellar_transaction_data();
1441        assert!(result.is_ok());
1442        assert_eq!(
1443            result.unwrap().source_account,
1444            stellar_tx_data.source_account
1445        );
1446
1447        // Should fail for non-Stellar data
1448        let evm_data = NetworkTransactionData::Evm(create_sample_evm_tx_data());
1449        assert!(evm_data.get_stellar_transaction_data().is_err());
1450    }
1451
1452    // Test for TryFrom<NetworkTransactionData> for TxLegacy
1453    #[test]
1454    fn test_try_from_network_tx_data_for_tx_legacy() {
1455        // Create a valid EVM transaction
1456        let evm_tx_data = create_sample_evm_tx_data();
1457        let network_data = NetworkTransactionData::Evm(evm_tx_data.clone());
1458
1459        // Should convert successfully
1460        let result = TxLegacy::try_from(network_data);
1461        assert!(result.is_ok());
1462        let tx_legacy = result.unwrap();
1463
1464        // Verify fields
1465        assert_eq!(tx_legacy.chain_id, Some(evm_tx_data.chain_id));
1466        assert_eq!(tx_legacy.nonce, evm_tx_data.nonce.unwrap());
1467        assert_eq!(tx_legacy.gas_limit, evm_tx_data.gas_limit.unwrap_or(21000));
1468        assert_eq!(tx_legacy.gas_price, evm_tx_data.gas_price.unwrap());
1469        assert_eq!(tx_legacy.value, evm_tx_data.value);
1470
1471        // Should fail for non-EVM data
1472        let solana_data = NetworkTransactionData::Solana(SolanaTransactionData {
1473            transaction: "transaction_123".to_string(),
1474            signature: None,
1475        });
1476        assert!(TxLegacy::try_from(solana_data).is_err());
1477    }
1478
1479    #[test]
1480    fn test_try_from_evm_tx_data_for_tx_legacy() {
1481        // Create a valid EVM transaction with legacy fields
1482        let evm_tx_data = create_sample_evm_tx_data();
1483
1484        // Should convert successfully
1485        let result = TxLegacy::try_from(evm_tx_data.clone());
1486        assert!(result.is_ok());
1487        let tx_legacy = result.unwrap();
1488
1489        // Verify fields
1490        assert_eq!(tx_legacy.chain_id, Some(evm_tx_data.chain_id));
1491        assert_eq!(tx_legacy.nonce, evm_tx_data.nonce.unwrap());
1492        assert_eq!(tx_legacy.gas_limit, evm_tx_data.gas_limit.unwrap_or(21000));
1493        assert_eq!(tx_legacy.gas_price, evm_tx_data.gas_price.unwrap());
1494        assert_eq!(tx_legacy.value, evm_tx_data.value);
1495    }
1496
1497    fn dummy_signature() -> DecoratedSignature {
1498        let hint = SignatureHint([0; 4]);
1499        let bytes: Vec<u8> = vec![0u8; 64];
1500        let bytes_m: BytesM<64> = bytes.try_into().expect("BytesM conversion");
1501        DecoratedSignature {
1502            hint,
1503            signature: Signature(bytes_m),
1504        }
1505    }
1506
1507    fn test_stellar_tx_data() -> StellarTransactionData {
1508        StellarTransactionData {
1509            source_account: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
1510            fee: Some(100),
1511            sequence_number: Some(1),
1512            memo: None,
1513            valid_until: None,
1514            network_passphrase: "Test SDF Network ; September 2015".to_string(),
1515            signatures: Vec::new(),
1516            hash: None,
1517            simulation_transaction_data: None,
1518            transaction_input: TransactionInput::Operations(vec![OperationSpec::Payment {
1519                destination: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
1520                amount: 1000,
1521                asset: AssetSpec::Native,
1522            }]),
1523            signed_envelope_xdr: None,
1524        }
1525    }
1526
1527    #[test]
1528    fn test_with_sequence_number() {
1529        let tx = test_stellar_tx_data();
1530        let updated = tx.with_sequence_number(42);
1531        assert_eq!(updated.sequence_number, Some(42));
1532    }
1533
1534    #[test]
1535    fn test_get_envelope_for_simulation() {
1536        let tx = test_stellar_tx_data();
1537        let env = tx.get_envelope_for_simulation();
1538        assert!(env.is_ok());
1539        let env = env.unwrap();
1540        // Should be a TransactionV1Envelope with no signatures
1541        match env {
1542            soroban_rs::xdr::TransactionEnvelope::Tx(tx_env) => {
1543                assert_eq!(tx_env.signatures.len(), 0);
1544            }
1545            _ => {
1546                panic!("Expected TransactionEnvelope::Tx variant");
1547            }
1548        }
1549    }
1550
1551    #[test]
1552    fn test_get_envelope_for_submission() {
1553        let mut tx = test_stellar_tx_data();
1554        tx.signatures.push(dummy_signature());
1555        let env = tx.get_envelope_for_submission();
1556        assert!(env.is_ok());
1557        let env = env.unwrap();
1558        match env {
1559            soroban_rs::xdr::TransactionEnvelope::Tx(tx_env) => {
1560                assert_eq!(tx_env.signatures.len(), 1);
1561            }
1562            _ => {
1563                panic!("Expected TransactionEnvelope::Tx variant");
1564            }
1565        }
1566    }
1567
1568    #[test]
1569    fn test_attach_signature() {
1570        let tx = test_stellar_tx_data();
1571        let sig = dummy_signature();
1572        let updated = tx.attach_signature(sig.clone());
1573        assert_eq!(updated.signatures.len(), 1);
1574        assert_eq!(updated.signatures[0], sig);
1575    }
1576
1577    #[test]
1578    fn test_with_hash() {
1579        let tx = test_stellar_tx_data();
1580        let updated = tx.with_hash("hash123".to_string());
1581        assert_eq!(updated.hash, Some("hash123".to_string()));
1582    }
1583
1584    #[test]
1585    fn test_evm_tx_for_replacement() {
1586        let old_data = create_sample_evm_tx_data();
1587        let new_request = EvmTransactionRequest {
1588            to: Some("0xNewRecipient".to_string()),
1589            value: U256::from(2000000000000000000u64), // 2 ETH
1590            data: Some("0xNewData".to_string()),
1591            gas_limit: Some(25000),
1592            gas_price: Some(30000000000), // 30 Gwei (should be ignored)
1593            max_fee_per_gas: Some(40000000000), // Should be ignored
1594            max_priority_fee_per_gas: Some(2000000000), // Should be ignored
1595            speed: Some(Speed::Fast),
1596            valid_until: None,
1597        };
1598
1599        let result = EvmTransactionData::for_replacement(&old_data, &new_request);
1600
1601        // Should preserve old data fields
1602        assert_eq!(result.chain_id, old_data.chain_id);
1603        assert_eq!(result.from, old_data.from);
1604        assert_eq!(result.nonce, old_data.nonce);
1605
1606        // Should use new request fields
1607        assert_eq!(result.to, new_request.to);
1608        assert_eq!(result.value, new_request.value);
1609        assert_eq!(result.data, new_request.data);
1610        assert_eq!(result.gas_limit, new_request.gas_limit);
1611        assert_eq!(result.speed, new_request.speed);
1612
1613        // Should clear all pricing fields (regardless of what's in the request)
1614        assert_eq!(result.gas_price, None);
1615        assert_eq!(result.max_fee_per_gas, None);
1616        assert_eq!(result.max_priority_fee_per_gas, None);
1617
1618        // Should reset signing fields
1619        assert_eq!(result.signature, None);
1620        assert_eq!(result.hash, None);
1621        assert_eq!(result.raw, None);
1622    }
1623
1624    #[test]
1625    fn test_transaction_repo_model_validate() {
1626        let transaction = TransactionRepoModel::default();
1627        let result = transaction.validate();
1628        assert!(result.is_ok());
1629    }
1630
1631    #[test]
1632    fn test_try_from_network_transaction_request_evm() {
1633        use crate::models::{NetworkRepoModel, NetworkType, RelayerRepoModel};
1634
1635        let evm_request = NetworkTransactionRequest::Evm(EvmTransactionRequest {
1636            to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
1637            value: U256::from(1000000000000000000u128),
1638            data: Some("0x1234".to_string()),
1639            gas_limit: Some(21000),
1640            gas_price: Some(20000000000),
1641            max_fee_per_gas: None,
1642            max_priority_fee_per_gas: None,
1643            speed: Some(Speed::Fast),
1644            valid_until: Some("2024-12-31T23:59:59Z".to_string()),
1645        });
1646
1647        let relayer_model = RelayerRepoModel {
1648            id: "relayer-id".to_string(),
1649            name: "Test Relayer".to_string(),
1650            network: "network-id".to_string(),
1651            paused: false,
1652            network_type: NetworkType::Evm,
1653            signer_id: "signer-id".to_string(),
1654            policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
1655            address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
1656            notification_id: None,
1657            system_disabled: false,
1658            custom_rpc_urls: None,
1659            ..Default::default()
1660        };
1661
1662        let network_model = NetworkRepoModel {
1663            id: "evm:ethereum".to_string(),
1664            name: "ethereum".to_string(),
1665            network_type: NetworkType::Evm,
1666            config: NetworkConfigData::Evm(EvmNetworkConfig {
1667                common: NetworkConfigCommon {
1668                    network: "ethereum".to_string(),
1669                    from: None,
1670                    rpc_urls: Some(vec!["https://mainnet.infura.io".to_string()]),
1671                    explorer_urls: Some(vec!["https://etherscan.io".to_string()]),
1672                    average_blocktime_ms: Some(12000),
1673                    is_testnet: Some(false),
1674                    tags: Some(vec!["mainnet".to_string()]),
1675                },
1676                chain_id: Some(1),
1677                required_confirmations: Some(12),
1678                features: None,
1679                symbol: Some("ETH".to_string()),
1680                gas_price_cache: None,
1681            }),
1682        };
1683
1684        let result = TransactionRepoModel::try_from((&evm_request, &relayer_model, &network_model));
1685        assert!(result.is_ok());
1686        let transaction = result.unwrap();
1687
1688        assert_eq!(transaction.relayer_id, relayer_model.id);
1689        assert_eq!(transaction.status, TransactionStatus::Pending);
1690        assert_eq!(transaction.network_type, NetworkType::Evm);
1691        assert_eq!(
1692            transaction.valid_until,
1693            Some("2024-12-31T23:59:59Z".to_string())
1694        );
1695        assert!(transaction.is_canceled == Some(false));
1696
1697        if let NetworkTransactionData::Evm(evm_data) = transaction.network_data {
1698            assert_eq!(evm_data.from, relayer_model.address);
1699            assert_eq!(
1700                evm_data.to,
1701                Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string())
1702            );
1703            assert_eq!(evm_data.value, U256::from(1000000000000000000u128));
1704            assert_eq!(evm_data.chain_id, 1);
1705            assert_eq!(evm_data.gas_limit, Some(21000));
1706            assert_eq!(evm_data.gas_price, Some(20000000000));
1707            assert_eq!(evm_data.speed, Some(Speed::Fast));
1708        } else {
1709            panic!("Expected EVM transaction data");
1710        }
1711    }
1712
1713    #[test]
1714    fn test_try_from_network_transaction_request_solana() {
1715        use crate::models::{
1716            NetworkRepoModel, NetworkTransactionRequest, NetworkType, RelayerRepoModel,
1717        };
1718
1719        let solana_request = NetworkTransactionRequest::Solana(
1720            crate::models::transaction::request::solana::SolanaTransactionRequest {
1721                transaction: EncodedSerializedTransaction::new("transaction_123".to_string()),
1722            },
1723        );
1724
1725        let relayer_model = RelayerRepoModel {
1726            id: "relayer-id".to_string(),
1727            name: "Test Solana Relayer".to_string(),
1728            network: "network-id".to_string(),
1729            paused: false,
1730            network_type: NetworkType::Solana,
1731            signer_id: "signer-id".to_string(),
1732            policies: RelayerNetworkPolicy::Solana(RelayerSolanaPolicy::default()),
1733            address: "solana_address".to_string(),
1734            notification_id: None,
1735            system_disabled: false,
1736            custom_rpc_urls: None,
1737            ..Default::default()
1738        };
1739
1740        let network_model = NetworkRepoModel {
1741            id: "solana:mainnet".to_string(),
1742            name: "mainnet".to_string(),
1743            network_type: NetworkType::Solana,
1744            config: NetworkConfigData::Solana(SolanaNetworkConfig {
1745                common: NetworkConfigCommon {
1746                    network: "mainnet".to_string(),
1747                    from: None,
1748                    rpc_urls: Some(vec!["https://api.mainnet-beta.solana.com".to_string()]),
1749                    explorer_urls: Some(vec!["https://explorer.solana.com".to_string()]),
1750                    average_blocktime_ms: Some(400),
1751                    is_testnet: Some(false),
1752                    tags: Some(vec!["mainnet".to_string()]),
1753                },
1754            }),
1755        };
1756
1757        let result =
1758            TransactionRepoModel::try_from((&solana_request, &relayer_model, &network_model));
1759        assert!(result.is_ok());
1760        let transaction = result.unwrap();
1761
1762        assert_eq!(transaction.relayer_id, relayer_model.id);
1763        assert_eq!(transaction.status, TransactionStatus::Pending);
1764        assert_eq!(transaction.network_type, NetworkType::Solana);
1765        assert_eq!(transaction.valid_until, None);
1766
1767        if let NetworkTransactionData::Solana(solana_data) = transaction.network_data {
1768            assert_eq!(solana_data.transaction, "transaction_123".to_string());
1769            assert_eq!(solana_data.signature, None);
1770        } else {
1771            panic!("Expected Solana transaction data");
1772        }
1773    }
1774
1775    #[test]
1776    fn test_try_from_network_transaction_request_stellar() {
1777        use crate::models::transaction::request::stellar::StellarTransactionRequest;
1778        use crate::models::{
1779            NetworkRepoModel, NetworkTransactionRequest, NetworkType, RelayerRepoModel,
1780        };
1781
1782        let stellar_request = NetworkTransactionRequest::Stellar(StellarTransactionRequest {
1783            source_account: Some(
1784                "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
1785            ),
1786            network: "mainnet".to_string(),
1787            operations: Some(vec![OperationSpec::Payment {
1788                destination: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
1789                amount: 1000000,
1790                asset: AssetSpec::Native,
1791            }]),
1792            memo: Some(MemoSpec::Text {
1793                value: "Test memo".to_string(),
1794            }),
1795            valid_until: Some("2024-12-31T23:59:59Z".to_string()),
1796            transaction_xdr: None,
1797            fee_bump: None,
1798            max_fee: None,
1799        });
1800
1801        let relayer_model = RelayerRepoModel {
1802            id: "relayer-id".to_string(),
1803            name: "Test Stellar Relayer".to_string(),
1804            network: "network-id".to_string(),
1805            paused: false,
1806            network_type: NetworkType::Stellar,
1807            signer_id: "signer-id".to_string(),
1808            policies: RelayerNetworkPolicy::Stellar(RelayerStellarPolicy::default()),
1809            address: "stellar_address".to_string(),
1810            notification_id: None,
1811            system_disabled: false,
1812            custom_rpc_urls: None,
1813            ..Default::default()
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                concurrent_transactions: None,
2111            }),
2112            address: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2113            notification_id: None,
2114            system_disabled: false,
2115            custom_rpc_urls: None,
2116            ..Default::default()
2117        };
2118
2119        (network_model, relayer_model)
2120    }
2121
2122    #[test]
2123    fn test_stellar_transaction_data_serialization_roundtrip() {
2124        use crate::models::transaction::stellar::asset::AssetSpec;
2125        use crate::models::transaction::stellar::operation::OperationSpec;
2126        use soroban_rs::xdr::{BytesM, Signature, SignatureHint};
2127
2128        // Create a dummy signature
2129        let hint = SignatureHint([1, 2, 3, 4]);
2130        let sig_bytes: Vec<u8> = vec![5u8; 64];
2131        let sig_bytes_m: BytesM<64> = sig_bytes.try_into().unwrap();
2132        let dummy_signature = DecoratedSignature {
2133            hint,
2134            signature: Signature(sig_bytes_m),
2135        };
2136
2137        // Create a StellarTransactionData with operations, signatures, and other fields
2138        let original_data = StellarTransactionData {
2139            source_account: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2140            fee: Some(100),
2141            sequence_number: Some(12345),
2142            memo: None,
2143            valid_until: None,
2144            network_passphrase: "Test SDF Network ; September 2015".to_string(),
2145            signatures: vec![dummy_signature.clone()],
2146            hash: Some("test-hash".to_string()),
2147            simulation_transaction_data: Some("simulation-data".to_string()),
2148            transaction_input: TransactionInput::Operations(vec![OperationSpec::Payment {
2149                destination: "GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB".to_string(),
2150                amount: 1000,
2151                asset: AssetSpec::Native,
2152            }]),
2153            signed_envelope_xdr: Some("signed-xdr-data".to_string()),
2154        };
2155
2156        // Serialize to JSON
2157        let json = serde_json::to_string(&original_data).expect("Failed to serialize");
2158
2159        // Deserialize from JSON
2160        let deserialized_data: StellarTransactionData =
2161            serde_json::from_str(&json).expect("Failed to deserialize");
2162
2163        // Verify that transaction_input is preserved
2164        match (
2165            &original_data.transaction_input,
2166            &deserialized_data.transaction_input,
2167        ) {
2168            (TransactionInput::Operations(orig_ops), TransactionInput::Operations(deser_ops)) => {
2169                assert_eq!(orig_ops.len(), deser_ops.len());
2170                assert_eq!(orig_ops, deser_ops);
2171            }
2172            _ => panic!("Transaction input type mismatch"),
2173        }
2174
2175        // Verify signatures are preserved
2176        assert_eq!(
2177            original_data.signatures.len(),
2178            deserialized_data.signatures.len()
2179        );
2180        assert_eq!(original_data.signatures, deserialized_data.signatures);
2181
2182        // Verify other fields are preserved
2183        assert_eq!(
2184            original_data.source_account,
2185            deserialized_data.source_account
2186        );
2187        assert_eq!(original_data.fee, deserialized_data.fee);
2188        assert_eq!(
2189            original_data.sequence_number,
2190            deserialized_data.sequence_number
2191        );
2192        assert_eq!(
2193            original_data.network_passphrase,
2194            deserialized_data.network_passphrase
2195        );
2196        assert_eq!(original_data.hash, deserialized_data.hash);
2197        assert_eq!(
2198            original_data.simulation_transaction_data,
2199            deserialized_data.simulation_transaction_data
2200        );
2201        assert_eq!(
2202            original_data.signed_envelope_xdr,
2203            deserialized_data.signed_envelope_xdr
2204        );
2205    }
2206
2207    #[test]
2208    fn test_stellar_xdr_transaction_input_conversion() {
2209        let (network_model, relayer_model) = test_models();
2210
2211        // Test case 1: Operations mode (existing behavior)
2212        let stellar_request = StellarTransactionRequest {
2213            source_account: Some(
2214                "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2215            ),
2216            network: "testnet".to_string(),
2217            operations: Some(vec![OperationSpec::Payment {
2218                destination: "GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB".to_string(),
2219                amount: 1000000,
2220                asset: AssetSpec::Native,
2221            }]),
2222            memo: None,
2223            valid_until: None,
2224            transaction_xdr: None,
2225            fee_bump: None,
2226            max_fee: None,
2227        };
2228
2229        let request = NetworkTransactionRequest::Stellar(stellar_request);
2230        let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2231        assert!(result.is_ok());
2232
2233        let tx_model = result.unwrap();
2234        if let NetworkTransactionData::Stellar(ref stellar_data) = tx_model.network_data {
2235            assert!(matches!(
2236                stellar_data.transaction_input,
2237                TransactionInput::Operations(_)
2238            ));
2239        } else {
2240            panic!("Expected Stellar transaction data");
2241        }
2242
2243        // Test case 2: Unsigned XDR mode
2244        // This is a valid unsigned transaction created with stellar CLI
2245        let unsigned_xdr = "AAAAAgAAAACige4lTdwSB/sto4SniEdJ2kOa2X65s5bqkd40J4DjSwAAAGQAAHAkAAAADgAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAKKB7iVN3BIH+y2jhKeIR0naQ5rZfrmzluqR3jQngONLAAAAAAAAAAAAD0JAAAAAAAAAAAA=";
2246        let stellar_request = StellarTransactionRequest {
2247            source_account: None,
2248            network: "testnet".to_string(),
2249            operations: Some(vec![]),
2250            memo: None,
2251            valid_until: None,
2252            transaction_xdr: Some(unsigned_xdr.to_string()),
2253            fee_bump: None,
2254            max_fee: None,
2255        };
2256
2257        let request = NetworkTransactionRequest::Stellar(stellar_request);
2258        let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2259        assert!(result.is_ok());
2260
2261        let tx_model = result.unwrap();
2262        if let NetworkTransactionData::Stellar(ref stellar_data) = tx_model.network_data {
2263            assert!(matches!(
2264                stellar_data.transaction_input,
2265                TransactionInput::UnsignedXdr(_)
2266            ));
2267        } else {
2268            panic!("Expected Stellar transaction data");
2269        }
2270
2271        // Test case 3: Signed XDR with fee_bump
2272        // Create a signed XDR by duplicating the test logic from xdr_tests
2273        let signed_xdr = {
2274            use soroban_rs::xdr::{Limits, TransactionEnvelope, TransactionV1Envelope, WriteXdr};
2275            use stellar_strkey::ed25519::PublicKey;
2276
2277            // Use the same transaction structure but add a dummy signature
2278            let source_pk =
2279                PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
2280                    .unwrap();
2281            let dest_pk =
2282                PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
2283                    .unwrap();
2284
2285            let payment_op = soroban_rs::xdr::PaymentOp {
2286                destination: soroban_rs::xdr::MuxedAccount::Ed25519(soroban_rs::xdr::Uint256(
2287                    dest_pk.0,
2288                )),
2289                asset: soroban_rs::xdr::Asset::Native,
2290                amount: 1000000,
2291            };
2292
2293            let operation = soroban_rs::xdr::Operation {
2294                source_account: None,
2295                body: soroban_rs::xdr::OperationBody::Payment(payment_op),
2296            };
2297
2298            let operations: soroban_rs::xdr::VecM<soroban_rs::xdr::Operation, 100> =
2299                vec![operation].try_into().unwrap();
2300
2301            let tx = soroban_rs::xdr::Transaction {
2302                source_account: soroban_rs::xdr::MuxedAccount::Ed25519(soroban_rs::xdr::Uint256(
2303                    source_pk.0,
2304                )),
2305                fee: 100,
2306                seq_num: soroban_rs::xdr::SequenceNumber(1),
2307                cond: soroban_rs::xdr::Preconditions::None,
2308                memo: soroban_rs::xdr::Memo::None,
2309                operations,
2310                ext: soroban_rs::xdr::TransactionExt::V0,
2311            };
2312
2313            // Add a dummy signature
2314            let hint = soroban_rs::xdr::SignatureHint([0; 4]);
2315            let sig_bytes: Vec<u8> = vec![0u8; 64];
2316            let sig_bytes_m: soroban_rs::xdr::BytesM<64> = sig_bytes.try_into().unwrap();
2317            let sig = soroban_rs::xdr::DecoratedSignature {
2318                hint,
2319                signature: soroban_rs::xdr::Signature(sig_bytes_m),
2320            };
2321
2322            let envelope = TransactionV1Envelope {
2323                tx,
2324                signatures: vec![sig].try_into().unwrap(),
2325            };
2326
2327            let tx_envelope = TransactionEnvelope::Tx(envelope);
2328            tx_envelope.to_xdr_base64(Limits::none()).unwrap()
2329        };
2330        let stellar_request = StellarTransactionRequest {
2331            source_account: None,
2332            network: "testnet".to_string(),
2333            operations: Some(vec![]),
2334            memo: None,
2335            valid_until: None,
2336            transaction_xdr: Some(signed_xdr.to_string()),
2337            fee_bump: Some(true),
2338            max_fee: Some(20000000),
2339        };
2340
2341        let request = NetworkTransactionRequest::Stellar(stellar_request);
2342        let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2343        assert!(result.is_ok());
2344
2345        let tx_model = result.unwrap();
2346        if let NetworkTransactionData::Stellar(ref stellar_data) = tx_model.network_data {
2347            match &stellar_data.transaction_input {
2348                TransactionInput::SignedXdr { xdr, max_fee } => {
2349                    assert_eq!(xdr, &signed_xdr);
2350                    assert_eq!(*max_fee, 20000000);
2351                }
2352                _ => panic!("Expected SignedXdr transaction input"),
2353            }
2354        } else {
2355            panic!("Expected Stellar transaction data");
2356        }
2357
2358        // Test case 4: Signed XDR without fee_bump should fail
2359        let stellar_request = StellarTransactionRequest {
2360            source_account: None,
2361            network: "testnet".to_string(),
2362            operations: Some(vec![]),
2363            memo: None,
2364            valid_until: None,
2365            transaction_xdr: Some(signed_xdr.clone()),
2366            fee_bump: None,
2367            max_fee: None,
2368        };
2369
2370        let request = NetworkTransactionRequest::Stellar(stellar_request);
2371        let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2372        assert!(result.is_err());
2373        assert!(result
2374            .unwrap_err()
2375            .to_string()
2376            .contains("Expected unsigned XDR but received signed XDR"));
2377
2378        // Test case 5: Operations with fee_bump should fail
2379        let stellar_request = StellarTransactionRequest {
2380            source_account: Some(
2381                "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2382            ),
2383            network: "testnet".to_string(),
2384            operations: Some(vec![OperationSpec::Payment {
2385                destination: "GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB".to_string(),
2386                amount: 1000000,
2387                asset: AssetSpec::Native,
2388            }]),
2389            memo: None,
2390            valid_until: None,
2391            transaction_xdr: None,
2392            fee_bump: Some(true),
2393            max_fee: None,
2394        };
2395
2396        let request = NetworkTransactionRequest::Stellar(stellar_request);
2397        let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2398        assert!(result.is_err());
2399        assert!(result
2400            .unwrap_err()
2401            .to_string()
2402            .contains("Cannot request fee_bump with operations mode"));
2403    }
2404
2405    #[test]
2406    fn test_invoke_host_function_must_be_exclusive() {
2407        let (network_model, relayer_model) = test_models();
2408
2409        // Test case 1: Single InvokeHostFunction - should succeed
2410        let stellar_request = StellarTransactionRequest {
2411            source_account: Some(
2412                "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2413            ),
2414            network: "testnet".to_string(),
2415            operations: Some(vec![OperationSpec::InvokeContract {
2416                contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2417                    .to_string(),
2418                function_name: "transfer".to_string(),
2419                args: vec![],
2420                auth: None,
2421            }]),
2422            memo: None,
2423            valid_until: None,
2424            transaction_xdr: None,
2425            fee_bump: None,
2426            max_fee: None,
2427        };
2428
2429        let request = NetworkTransactionRequest::Stellar(stellar_request);
2430        let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2431        assert!(result.is_ok(), "Single InvokeHostFunction should succeed");
2432
2433        // Test case 2: InvokeHostFunction mixed with Payment - should fail
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::InvokeContract {
2447                    contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2448                        .to_string(),
2449                    function_name: "transfer".to_string(),
2450                    args: vec![],
2451                    auth: None,
2452                },
2453            ]),
2454            memo: None,
2455            valid_until: None,
2456            transaction_xdr: None,
2457            fee_bump: None,
2458            max_fee: None,
2459        };
2460
2461        let request = NetworkTransactionRequest::Stellar(stellar_request);
2462        let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2463
2464        match result {
2465            Ok(_) => panic!("Expected Soroban operation mixed with Payment to fail"),
2466            Err(err) => {
2467                let err_str = err.to_string();
2468                assert!(
2469                    err_str.contains("Soroban operations must be exclusive"),
2470                    "Expected error about Soroban operation exclusivity, got: {}",
2471                    err_str
2472                );
2473            }
2474        }
2475
2476        // Test case 3: Multiple InvokeHostFunction operations - should fail
2477        let stellar_request = StellarTransactionRequest {
2478            source_account: Some(
2479                "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2480            ),
2481            network: "testnet".to_string(),
2482            operations: Some(vec![
2483                OperationSpec::InvokeContract {
2484                    contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2485                        .to_string(),
2486                    function_name: "transfer".to_string(),
2487                    args: vec![],
2488                    auth: None,
2489                },
2490                OperationSpec::InvokeContract {
2491                    contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2492                        .to_string(),
2493                    function_name: "approve".to_string(),
2494                    args: vec![],
2495                    auth: None,
2496                },
2497            ]),
2498            memo: None,
2499            valid_until: None,
2500            transaction_xdr: None,
2501            fee_bump: None,
2502            max_fee: None,
2503        };
2504
2505        let request = NetworkTransactionRequest::Stellar(stellar_request);
2506        let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2507
2508        match result {
2509            Ok(_) => panic!("Expected multiple Soroban operations to fail"),
2510            Err(err) => {
2511                let err_str = err.to_string();
2512                assert!(
2513                    err_str.contains("Transaction can contain at most one Soroban operation"),
2514                    "Expected error about multiple Soroban operations, got: {}",
2515                    err_str
2516                );
2517            }
2518        }
2519
2520        // Test case 4: Multiple Payment operations - should succeed
2521        let stellar_request = StellarTransactionRequest {
2522            source_account: Some(
2523                "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2524            ),
2525            network: "testnet".to_string(),
2526            operations: Some(vec![
2527                OperationSpec::Payment {
2528                    destination: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
2529                        .to_string(),
2530                    amount: 1000,
2531                    asset: AssetSpec::Native,
2532                },
2533                OperationSpec::Payment {
2534                    destination: "GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"
2535                        .to_string(),
2536                    amount: 2000,
2537                    asset: AssetSpec::Native,
2538                },
2539            ]),
2540            memo: None,
2541            valid_until: None,
2542            transaction_xdr: None,
2543            fee_bump: None,
2544            max_fee: None,
2545        };
2546
2547        let request = NetworkTransactionRequest::Stellar(stellar_request);
2548        let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2549        assert!(result.is_ok(), "Multiple Payment operations should succeed");
2550
2551        // Test case 5: InvokeHostFunction with non-None memo - should fail
2552        let stellar_request = StellarTransactionRequest {
2553            source_account: Some(
2554                "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2555            ),
2556            network: "testnet".to_string(),
2557            operations: Some(vec![OperationSpec::InvokeContract {
2558                contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2559                    .to_string(),
2560                function_name: "transfer".to_string(),
2561                args: vec![],
2562                auth: None,
2563            }]),
2564            memo: Some(MemoSpec::Text {
2565                value: "This should fail".to_string(),
2566            }),
2567            valid_until: None,
2568            transaction_xdr: None,
2569            fee_bump: None,
2570            max_fee: None,
2571        };
2572
2573        let request = NetworkTransactionRequest::Stellar(stellar_request);
2574        let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2575
2576        match result {
2577            Ok(_) => panic!("Expected InvokeHostFunction with non-None memo to fail"),
2578            Err(err) => {
2579                let err_str = err.to_string();
2580                assert!(
2581                    err_str.contains("Soroban operations cannot have a memo"),
2582                    "Expected error about memo restriction, got: {}",
2583                    err_str
2584                );
2585            }
2586        }
2587
2588        // Test case 6: InvokeHostFunction with memo None - should succeed
2589        let stellar_request = StellarTransactionRequest {
2590            source_account: Some(
2591                "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2592            ),
2593            network: "testnet".to_string(),
2594            operations: Some(vec![OperationSpec::InvokeContract {
2595                contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2596                    .to_string(),
2597                function_name: "transfer".to_string(),
2598                args: vec![],
2599                auth: None,
2600            }]),
2601            memo: Some(MemoSpec::None),
2602            valid_until: None,
2603            transaction_xdr: None,
2604            fee_bump: None,
2605            max_fee: None,
2606        };
2607
2608        let request = NetworkTransactionRequest::Stellar(stellar_request);
2609        let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2610        assert!(
2611            result.is_ok(),
2612            "InvokeHostFunction with MemoSpec::None should succeed"
2613        );
2614
2615        // Test case 7: InvokeHostFunction with no memo field - should succeed
2616        let stellar_request = StellarTransactionRequest {
2617            source_account: Some(
2618                "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2619            ),
2620            network: "testnet".to_string(),
2621            operations: Some(vec![OperationSpec::InvokeContract {
2622                contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2623                    .to_string(),
2624                function_name: "transfer".to_string(),
2625                args: vec![],
2626                auth: None,
2627            }]),
2628            memo: None,
2629            valid_until: None,
2630            transaction_xdr: None,
2631            fee_bump: None,
2632            max_fee: None,
2633        };
2634
2635        let request = NetworkTransactionRequest::Stellar(stellar_request);
2636        let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2637        assert!(
2638            result.is_ok(),
2639            "InvokeHostFunction with no memo should succeed"
2640        );
2641
2642        // Test case 8: Payment operation with memo - should succeed
2643        let stellar_request = StellarTransactionRequest {
2644            source_account: Some(
2645                "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2646            ),
2647            network: "testnet".to_string(),
2648            operations: Some(vec![OperationSpec::Payment {
2649                destination: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2650                amount: 1000,
2651                asset: AssetSpec::Native,
2652            }]),
2653            memo: Some(MemoSpec::Text {
2654                value: "Payment memo is allowed".to_string(),
2655            }),
2656            valid_until: None,
2657            transaction_xdr: None,
2658            fee_bump: None,
2659            max_fee: None,
2660        };
2661
2662        let request = NetworkTransactionRequest::Stellar(stellar_request);
2663        let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2664        assert!(result.is_ok(), "Payment operation with memo should succeed");
2665    }
2666
2667    #[test]
2668    fn test_update_delete_at_if_final_status_does_not_update_when_delete_at_already_set() {
2669        let _lock = match ENV_MUTEX.lock() {
2670            Ok(guard) => guard,
2671            Err(poisoned) => poisoned.into_inner(),
2672        };
2673
2674        use std::env;
2675
2676        // Set custom expiration hours for test
2677        env::set_var("TRANSACTION_EXPIRATION_HOURS", "6");
2678
2679        let mut transaction = create_test_transaction();
2680        transaction.delete_at = Some("2024-01-01T00:00:00Z".to_string());
2681        transaction.status = TransactionStatus::Confirmed; // Final status
2682
2683        let original_delete_at = transaction.delete_at.clone();
2684
2685        transaction.update_delete_at_if_final_status();
2686
2687        // Should not change delete_at when it's already set
2688        assert_eq!(transaction.delete_at, original_delete_at);
2689
2690        // Cleanup
2691        env::remove_var("TRANSACTION_EXPIRATION_HOURS");
2692    }
2693
2694    #[test]
2695    fn test_update_delete_at_if_final_status_does_not_update_when_status_not_final() {
2696        let _lock = match ENV_MUTEX.lock() {
2697            Ok(guard) => guard,
2698            Err(poisoned) => poisoned.into_inner(),
2699        };
2700
2701        use std::env;
2702
2703        // Set custom expiration hours for test
2704        env::set_var("TRANSACTION_EXPIRATION_HOURS", "6");
2705
2706        let mut transaction = create_test_transaction();
2707        transaction.delete_at = None;
2708        transaction.status = TransactionStatus::Pending; // Non-final status
2709
2710        transaction.update_delete_at_if_final_status();
2711
2712        // Should not set delete_at for non-final status
2713        assert!(transaction.delete_at.is_none());
2714
2715        // Cleanup
2716        env::remove_var("TRANSACTION_EXPIRATION_HOURS");
2717    }
2718
2719    #[test]
2720    fn test_update_delete_at_if_final_status_sets_delete_at_for_final_statuses() {
2721        let _lock = match ENV_MUTEX.lock() {
2722            Ok(guard) => guard,
2723            Err(poisoned) => poisoned.into_inner(),
2724        };
2725
2726        use crate::config::ServerConfig;
2727        use chrono::{DateTime, Duration, Utc};
2728        use std::env;
2729
2730        // Set custom expiration hours for test
2731        env::set_var("TRANSACTION_EXPIRATION_HOURS", "3"); // Use 3 hours for this test
2732
2733        // Verify the env var is actually set correctly
2734        let actual_hours = ServerConfig::get_transaction_expiration_hours();
2735        assert_eq!(
2736            actual_hours, 3,
2737            "Environment variable should be set to 3 hours"
2738        );
2739
2740        let final_statuses = vec![
2741            TransactionStatus::Canceled,
2742            TransactionStatus::Confirmed,
2743            TransactionStatus::Failed,
2744            TransactionStatus::Expired,
2745        ];
2746
2747        for status in final_statuses {
2748            let mut transaction = create_test_transaction();
2749            transaction.delete_at = None;
2750            transaction.status = status.clone();
2751
2752            let before_update = Utc::now();
2753            transaction.update_delete_at_if_final_status();
2754
2755            // Should set delete_at for final status
2756            assert!(
2757                transaction.delete_at.is_some(),
2758                "delete_at should be set for status: {:?}",
2759                status
2760            );
2761
2762            // Verify the timestamp is reasonable
2763            let delete_at_str = transaction.delete_at.unwrap();
2764            let delete_at = DateTime::parse_from_rfc3339(&delete_at_str)
2765                .expect("delete_at should be valid RFC3339")
2766                .with_timezone(&Utc);
2767
2768            // Should be approximately 3 hours from before_update
2769            let duration_from_before = delete_at.signed_duration_since(before_update);
2770            let expected_duration = Duration::hours(3);
2771            let tolerance = Duration::minutes(5); // Allow 5 minutes tolerance
2772
2773            // Debug information
2774            let actual_hours_at_runtime = ServerConfig::get_transaction_expiration_hours();
2775
2776            assert!(
2777                duration_from_before >= expected_duration - tolerance &&
2778                duration_from_before <= expected_duration + tolerance,
2779                "delete_at should be approximately 3 hours from now for status: {:?}. Duration from start: {:?}, Expected: {:?}, Config hours at runtime: {}",
2780                status, duration_from_before, expected_duration, actual_hours_at_runtime
2781            );
2782        }
2783
2784        // Cleanup
2785        env::remove_var("TRANSACTION_EXPIRATION_HOURS");
2786    }
2787
2788    #[test]
2789    fn test_update_delete_at_if_final_status_uses_default_expiration_hours() {
2790        let _lock = match ENV_MUTEX.lock() {
2791            Ok(guard) => guard,
2792            Err(poisoned) => poisoned.into_inner(),
2793        };
2794
2795        use chrono::{DateTime, Duration, Utc};
2796        use std::env;
2797
2798        // Remove env var to test default behavior
2799        env::remove_var("TRANSACTION_EXPIRATION_HOURS");
2800
2801        let mut transaction = create_test_transaction();
2802        transaction.delete_at = None;
2803        transaction.status = TransactionStatus::Confirmed;
2804
2805        let before_update = Utc::now();
2806        transaction.update_delete_at_if_final_status();
2807
2808        // Should set delete_at using default value (4 hours)
2809        assert!(transaction.delete_at.is_some());
2810
2811        let delete_at_str = transaction.delete_at.unwrap();
2812        let delete_at = DateTime::parse_from_rfc3339(&delete_at_str)
2813            .expect("delete_at should be valid RFC3339")
2814            .with_timezone(&Utc);
2815
2816        // Should be approximately 4 hours from before_update (default value)
2817        let duration_from_before = delete_at.signed_duration_since(before_update);
2818        let expected_duration = Duration::hours(4);
2819        let tolerance = Duration::minutes(5); // Allow 5 minutes tolerance
2820
2821        assert!(
2822            duration_from_before >= expected_duration - tolerance &&
2823            duration_from_before <= expected_duration + tolerance,
2824            "delete_at should be approximately 4 hours from now (default). Duration from start: {:?}, Expected: {:?}",
2825            duration_from_before, expected_duration
2826        );
2827    }
2828
2829    #[test]
2830    fn test_update_delete_at_if_final_status_with_custom_expiration_hours() {
2831        let _lock = match ENV_MUTEX.lock() {
2832            Ok(guard) => guard,
2833            Err(poisoned) => poisoned.into_inner(),
2834        };
2835
2836        use chrono::{DateTime, Duration, Utc};
2837        use std::env;
2838
2839        // Test with various custom expiration hours
2840        let test_cases = vec![1, 2, 6, 12]; // 1 hour, 2 hours, 6 hours, 12 hours
2841
2842        for expiration_hours in test_cases {
2843            env::set_var("TRANSACTION_EXPIRATION_HOURS", expiration_hours.to_string());
2844
2845            let mut transaction = create_test_transaction();
2846            transaction.delete_at = None;
2847            transaction.status = TransactionStatus::Failed;
2848
2849            let before_update = Utc::now();
2850            transaction.update_delete_at_if_final_status();
2851
2852            assert!(
2853                transaction.delete_at.is_some(),
2854                "delete_at should be set for {} hours",
2855                expiration_hours
2856            );
2857
2858            let delete_at_str = transaction.delete_at.unwrap();
2859            let delete_at = DateTime::parse_from_rfc3339(&delete_at_str)
2860                .expect("delete_at should be valid RFC3339")
2861                .with_timezone(&Utc);
2862
2863            let duration_from_before = delete_at.signed_duration_since(before_update);
2864            let expected_duration = Duration::hours(expiration_hours as i64);
2865            let tolerance = Duration::minutes(5); // Allow 5 minutes tolerance
2866
2867            assert!(
2868                duration_from_before >= expected_duration - tolerance &&
2869                duration_from_before <= expected_duration + tolerance,
2870                "delete_at should be approximately {} hours from now. Duration from start: {:?}, Expected: {:?}",
2871                expiration_hours, duration_from_before, expected_duration
2872            );
2873        }
2874
2875        // Cleanup
2876        env::remove_var("TRANSACTION_EXPIRATION_HOURS");
2877    }
2878
2879    #[test]
2880    fn test_calculate_delete_at_with_various_hours() {
2881        use chrono::{DateTime, Utc};
2882
2883        let test_cases = vec![0, 1, 6, 12, 24, 48];
2884
2885        for hours in test_cases {
2886            let before_calc = Utc::now();
2887            let result = TransactionRepoModel::calculate_delete_at(hours);
2888            let after_calc = Utc::now();
2889
2890            assert!(
2891                result.is_some(),
2892                "calculate_delete_at should return Some for {} hours",
2893                hours
2894            );
2895
2896            let delete_at_str = result.unwrap();
2897            let delete_at = DateTime::parse_from_rfc3339(&delete_at_str)
2898                .expect("Result should be valid RFC3339")
2899                .with_timezone(&Utc);
2900
2901            let expected_min =
2902                before_calc + chrono::Duration::hours(hours as i64) - chrono::Duration::seconds(1);
2903            let expected_max =
2904                after_calc + chrono::Duration::hours(hours as i64) + chrono::Duration::seconds(1);
2905
2906            assert!(
2907                delete_at >= expected_min && delete_at <= expected_max,
2908                "Calculated delete_at should be approximately {} hours from now. Got: {}, Expected between: {} and {}",
2909                hours, delete_at, expected_min, expected_max
2910            );
2911        }
2912    }
2913
2914    #[test]
2915    fn test_update_delete_at_if_final_status_idempotent() {
2916        let _lock = match ENV_MUTEX.lock() {
2917            Ok(guard) => guard,
2918            Err(poisoned) => poisoned.into_inner(),
2919        };
2920
2921        use std::env;
2922
2923        env::set_var("TRANSACTION_EXPIRATION_HOURS", "8");
2924
2925        let mut transaction = create_test_transaction();
2926        transaction.delete_at = None;
2927        transaction.status = TransactionStatus::Confirmed;
2928
2929        // First call should set delete_at
2930        transaction.update_delete_at_if_final_status();
2931        let first_delete_at = transaction.delete_at.clone();
2932        assert!(first_delete_at.is_some());
2933
2934        // Second call should not change delete_at (idempotent)
2935        transaction.update_delete_at_if_final_status();
2936        assert_eq!(transaction.delete_at, first_delete_at);
2937
2938        // Third call should not change delete_at (idempotent)
2939        transaction.update_delete_at_if_final_status();
2940        assert_eq!(transaction.delete_at, first_delete_at);
2941
2942        // Cleanup
2943        env::remove_var("TRANSACTION_EXPIRATION_HOURS");
2944    }
2945
2946    /// Helper function to create a test transaction for testing delete_at functionality
2947    fn create_test_transaction() -> TransactionRepoModel {
2948        TransactionRepoModel {
2949            id: "test-transaction-id".to_string(),
2950            relayer_id: "test-relayer-id".to_string(),
2951            status: TransactionStatus::Pending,
2952            status_reason: None,
2953            created_at: "2024-01-01T00:00:00Z".to_string(),
2954            sent_at: None,
2955            confirmed_at: None,
2956            valid_until: None,
2957            delete_at: None,
2958            network_data: NetworkTransactionData::Evm(EvmTransactionData {
2959                gas_price: None,
2960                gas_limit: Some(21000),
2961                nonce: Some(0),
2962                value: U256::from(0),
2963                data: None,
2964                from: "0x1234567890123456789012345678901234567890".to_string(),
2965                to: Some("0x0987654321098765432109876543210987654321".to_string()),
2966                chain_id: 1,
2967                hash: None,
2968                signature: None,
2969                speed: None,
2970                max_fee_per_gas: None,
2971                max_priority_fee_per_gas: None,
2972                raw: None,
2973            }),
2974            priced_at: None,
2975            hashes: vec![],
2976            network_type: NetworkType::Evm,
2977            noop_count: None,
2978            is_canceled: None,
2979        }
2980    }
2981
2982    #[test]
2983    fn test_apply_partial_update() {
2984        // Create a test transaction
2985        let mut transaction = create_test_transaction();
2986
2987        // Create a partial update request
2988        let update = TransactionUpdateRequest {
2989            status: Some(TransactionStatus::Confirmed),
2990            status_reason: Some("Transaction confirmed".to_string()),
2991            sent_at: Some("2023-01-01T12:00:00Z".to_string()),
2992            confirmed_at: Some("2023-01-01T12:05:00Z".to_string()),
2993            hashes: Some(vec!["0x123".to_string(), "0x456".to_string()]),
2994            is_canceled: Some(false),
2995            ..Default::default()
2996        };
2997
2998        // Apply the partial update
2999        transaction.apply_partial_update(update);
3000
3001        // Verify the updates were applied
3002        assert_eq!(transaction.status, TransactionStatus::Confirmed);
3003        assert_eq!(
3004            transaction.status_reason,
3005            Some("Transaction confirmed".to_string())
3006        );
3007        assert_eq!(
3008            transaction.sent_at,
3009            Some("2023-01-01T12:00:00Z".to_string())
3010        );
3011        assert_eq!(
3012            transaction.confirmed_at,
3013            Some("2023-01-01T12:05:00Z".to_string())
3014        );
3015        assert_eq!(
3016            transaction.hashes,
3017            vec!["0x123".to_string(), "0x456".to_string()]
3018        );
3019        assert_eq!(transaction.is_canceled, Some(false));
3020
3021        // Verify that delete_at was set because status changed to final
3022        assert!(transaction.delete_at.is_some());
3023    }
3024
3025    #[test]
3026    fn test_apply_partial_update_preserves_unchanged_fields() {
3027        // Create a test transaction with initial values
3028        let mut transaction = TransactionRepoModel {
3029            id: "test-tx".to_string(),
3030            relayer_id: "test-relayer".to_string(),
3031            status: TransactionStatus::Pending,
3032            status_reason: Some("Initial reason".to_string()),
3033            created_at: Utc::now().to_rfc3339(),
3034            sent_at: Some("2023-01-01T10:00:00Z".to_string()),
3035            confirmed_at: None,
3036            valid_until: None,
3037            delete_at: None,
3038            network_data: NetworkTransactionData::Evm(EvmTransactionData::default()),
3039            priced_at: None,
3040            hashes: vec!["0xoriginal".to_string()],
3041            network_type: NetworkType::Evm,
3042            noop_count: Some(5),
3043            is_canceled: Some(true),
3044        };
3045
3046        // Create a partial update that only changes status
3047        let update = TransactionUpdateRequest {
3048            status: Some(TransactionStatus::Sent),
3049            ..Default::default()
3050        };
3051
3052        // Apply the partial update
3053        transaction.apply_partial_update(update);
3054
3055        // Verify only status changed, other fields preserved
3056        assert_eq!(transaction.status, TransactionStatus::Sent);
3057        assert_eq!(
3058            transaction.status_reason,
3059            Some("Initial reason".to_string())
3060        );
3061        assert_eq!(
3062            transaction.sent_at,
3063            Some("2023-01-01T10:00:00Z".to_string())
3064        );
3065        assert_eq!(transaction.confirmed_at, None);
3066        assert_eq!(transaction.hashes, vec!["0xoriginal".to_string()]);
3067        assert_eq!(transaction.noop_count, Some(5));
3068        assert_eq!(transaction.is_canceled, Some(true));
3069
3070        // Status is not final, so delete_at should remain None
3071        assert!(transaction.delete_at.is_none());
3072    }
3073
3074    #[test]
3075    fn test_apply_partial_update_empty_update() {
3076        // Create a test transaction
3077        let mut transaction = create_test_transaction();
3078        let original_transaction = transaction.clone();
3079
3080        // Apply an empty update
3081        let update = TransactionUpdateRequest::default();
3082        transaction.apply_partial_update(update);
3083
3084        // Verify nothing changed
3085        assert_eq!(transaction.id, original_transaction.id);
3086        assert_eq!(transaction.status, original_transaction.status);
3087        assert_eq!(
3088            transaction.status_reason,
3089            original_transaction.status_reason
3090        );
3091        assert_eq!(transaction.sent_at, original_transaction.sent_at);
3092        assert_eq!(transaction.confirmed_at, original_transaction.confirmed_at);
3093        assert_eq!(transaction.hashes, original_transaction.hashes);
3094        assert_eq!(transaction.noop_count, original_transaction.noop_count);
3095        assert_eq!(transaction.is_canceled, original_transaction.is_canceled);
3096        assert_eq!(transaction.delete_at, original_transaction.delete_at);
3097    }
3098}