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