openzeppelin_relayer/models/transaction/
repository.rs

1use super::evm::Speed;
2use crate::{
3    constants::DEFAULT_TRANSACTION_SPEED,
4    domain::{
5        evm::PriceParams,
6        stellar::validation::{validate_operations, validate_soroban_memo_restriction},
7        xdr_utils::{is_signed, parse_transaction_xdr},
8        SignTransactionResponseEvm,
9    },
10    models::{
11        transaction::{
12            request::{evm::EvmTransactionRequest, stellar::StellarTransactionRequest},
13            stellar::{DecoratedSignature, MemoSpec, OperationSpec},
14        },
15        AddressError, EvmNetwork, NetworkRepoModel, NetworkTransactionRequest, NetworkType,
16        RelayerError, RelayerRepoModel, SignerError, StellarNetwork, StellarValidationError,
17        TransactionError, U256,
18    },
19};
20use alloy::{
21    consensus::{TxEip1559, TxLegacy},
22    primitives::{Address as AlloyAddress, Bytes, TxKind},
23    rpc::types::AccessList,
24};
25
26use chrono::Utc;
27use serde::{Deserialize, Serialize};
28use std::{convert::TryFrom, str::FromStr};
29
30use utoipa::ToSchema;
31use uuid::Uuid;
32
33use crate::constants::{STELLAR_DEFAULT_MAX_FEE, STELLAR_DEFAULT_TRANSACTION_FEE};
34use soroban_rs::xdr::{
35    Transaction as SorobanTransaction, TransactionEnvelope, TransactionV1Envelope, VecM,
36};
37
38#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema)]
39#[serde(rename_all = "lowercase")]
40pub enum TransactionStatus {
41    Canceled,
42    Pending,
43    Sent,
44    Submitted,
45    Mined,
46    Confirmed,
47    Failed,
48    Expired,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize, Default)]
52pub struct TransactionUpdateRequest {
53    pub status: Option<TransactionStatus>,
54    pub status_reason: Option<String>,
55    pub sent_at: Option<String>,
56    pub confirmed_at: Option<String>,
57    pub network_data: Option<NetworkTransactionData>,
58    /// Timestamp when gas price was determined
59    pub priced_at: Option<String>,
60    /// History of transaction hashes
61    pub hashes: Option<Vec<String>>,
62    /// Number of no-ops in the transaction
63    pub noop_count: Option<u32>,
64    /// Whether the transaction is canceled
65    pub is_canceled: Option<bool>,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct TransactionRepoModel {
70    pub id: String,
71    pub relayer_id: String,
72    pub status: TransactionStatus,
73    pub status_reason: Option<String>,
74    pub created_at: String,
75    pub sent_at: Option<String>,
76    pub confirmed_at: Option<String>,
77    pub valid_until: Option<String>,
78    pub network_data: NetworkTransactionData,
79    /// Timestamp when gas price was determined
80    pub priced_at: Option<String>,
81    /// History of transaction hashes
82    pub hashes: Vec<String>,
83    pub network_type: NetworkType,
84    pub noop_count: Option<u32>,
85    pub is_canceled: Option<bool>,
86}
87
88impl TransactionRepoModel {
89    /// Validates the transaction repository model
90    ///
91    /// # Returns
92    /// * `Ok(())` if the transaction is valid
93    /// * `Err(TransactionError)` if validation fails
94    pub fn validate(&self) -> Result<(), TransactionError> {
95        Ok(())
96    }
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
100#[serde(tag = "network_data", content = "data")]
101#[allow(clippy::large_enum_variant)]
102pub enum NetworkTransactionData {
103    Evm(EvmTransactionData),
104    Solana(SolanaTransactionData),
105    Stellar(StellarTransactionData),
106}
107
108impl NetworkTransactionData {
109    pub fn get_evm_transaction_data(&self) -> Result<EvmTransactionData, TransactionError> {
110        match self {
111            NetworkTransactionData::Evm(data) => Ok(data.clone()),
112            _ => Err(TransactionError::InvalidType(
113                "Expected EVM transaction".to_string(),
114            )),
115        }
116    }
117
118    pub fn get_solana_transaction_data(&self) -> Result<SolanaTransactionData, TransactionError> {
119        match self {
120            NetworkTransactionData::Solana(data) => Ok(data.clone()),
121            _ => Err(TransactionError::InvalidType(
122                "Expected Solana transaction".to_string(),
123            )),
124        }
125    }
126
127    pub fn get_stellar_transaction_data(&self) -> Result<StellarTransactionData, TransactionError> {
128        match self {
129            NetworkTransactionData::Stellar(data) => Ok(data.clone()),
130            _ => Err(TransactionError::InvalidType(
131                "Expected Stellar transaction".to_string(),
132            )),
133        }
134    }
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
138pub struct EvmTransactionDataSignature {
139    pub r: String,
140    pub s: String,
141    pub v: u8,
142    pub sig: String,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
146
147pub struct EvmTransactionData {
148    pub gas_price: Option<u128>,
149    pub gas_limit: u64,
150    pub nonce: Option<u64>,
151    pub value: U256,
152    pub data: Option<String>,
153    pub from: String,
154    pub to: Option<String>,
155    pub chain_id: u64,
156    pub hash: Option<String>,
157    pub signature: Option<EvmTransactionDataSignature>,
158    pub speed: Option<Speed>,
159    pub max_fee_per_gas: Option<u128>,
160    pub max_priority_fee_per_gas: Option<u128>,
161    pub raw: Option<Vec<u8>>,
162}
163
164impl EvmTransactionData {
165    /// Creates transaction data for replacement by combining existing transaction data with new request data.
166    ///
167    /// Preserves critical fields like chain_id, from address, and nonce while applying new transaction parameters.
168    /// Pricing fields are cleared and must be calculated separately.
169    ///
170    /// # Arguments
171    /// * `old_data` - The existing transaction data to preserve core fields from
172    /// * `request` - The new transaction request containing updated parameters
173    ///
174    /// # Returns
175    /// New `EvmTransactionData` configured for replacement transaction
176    pub fn for_replacement(old_data: &EvmTransactionData, request: &EvmTransactionRequest) -> Self {
177        Self {
178            // Preserve existing fields from old transaction
179            chain_id: old_data.chain_id,
180            from: old_data.from.clone(),
181            nonce: old_data.nonce, // Preserve original nonce for replacement
182
183            // Apply new fields from request
184            to: request.to.clone(),
185            value: request.value,
186            data: request.data.clone(),
187            gas_limit: request.gas_limit,
188            speed: request
189                .speed
190                .clone()
191                .or_else(|| old_data.speed.clone())
192                .or(Some(DEFAULT_TRANSACTION_SPEED)),
193
194            // Clear pricing fields - these will be calculated later
195            gas_price: None,
196            max_fee_per_gas: None,
197            max_priority_fee_per_gas: None,
198
199            // Reset signing fields
200            signature: None,
201            hash: None,
202            raw: None,
203        }
204    }
205
206    /// Updates the transaction data with calculated price parameters.
207    ///
208    /// # Arguments
209    /// * `price_params` - Calculated pricing parameters containing gas price and EIP-1559 fees
210    ///
211    /// # Returns
212    /// The updated `EvmTransactionData` with pricing information applied
213    pub fn with_price_params(mut self, price_params: PriceParams) -> Self {
214        self.gas_price = price_params.gas_price;
215        self.max_fee_per_gas = price_params.max_fee_per_gas;
216        self.max_priority_fee_per_gas = price_params.max_priority_fee_per_gas;
217
218        self
219    }
220
221    /// Updates the transaction data with an estimated gas limit.
222    ///
223    /// # Arguments
224    /// * `gas_limit` - The estimated gas limit for the transaction
225    ///
226    /// # Returns
227    /// The updated `EvmTransactionData` with the new gas limit
228    pub fn with_gas_estimate(mut self, gas_limit: u64) -> Self {
229        self.gas_limit = gas_limit;
230        self
231    }
232
233    /// Updates the transaction data with a specific nonce value.
234    ///
235    /// # Arguments
236    /// * `nonce` - The nonce value to set for the transaction
237    ///
238    /// # Returns
239    /// The updated `EvmTransactionData` with the specified nonce
240    pub fn with_nonce(mut self, nonce: u64) -> Self {
241        self.nonce = Some(nonce);
242        self
243    }
244
245    /// Updates the transaction data with signature information from a signed transaction response.
246    ///
247    /// # Arguments
248    /// * `sig` - The signed transaction response containing signature, hash, and raw transaction data
249    ///
250    /// # Returns
251    /// The updated `EvmTransactionData` with signature information applied
252    pub fn with_signed_transaction_data(mut self, sig: SignTransactionResponseEvm) -> Self {
253        self.signature = Some(sig.signature);
254        self.hash = Some(sig.hash);
255        self.raw = Some(sig.raw);
256        self
257    }
258}
259
260#[cfg(test)]
261impl Default for EvmTransactionData {
262    fn default() -> Self {
263        Self {
264            from: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266".to_string(), // Standard Hardhat test address
265            to: Some("0x70997970C51812dc3A010C7d01b50e0d17dc79C8".to_string()), // Standard Hardhat test address
266            gas_price: Some(20000000000),
267            value: U256::from(1000000000000000000u128), // 1 ETH
268            data: Some("0x".to_string()),
269            nonce: Some(1),
270            chain_id: 1,
271            gas_limit: 21000,
272            hash: None,
273            signature: None,
274            speed: None,
275            max_fee_per_gas: None,
276            max_priority_fee_per_gas: None,
277            raw: None,
278        }
279    }
280}
281
282#[cfg(test)]
283impl Default for TransactionRepoModel {
284    fn default() -> Self {
285        Self {
286            id: "00000000-0000-0000-0000-000000000001".to_string(),
287            relayer_id: "00000000-0000-0000-0000-000000000002".to_string(),
288            status: TransactionStatus::Pending,
289            created_at: "2023-01-01T00:00:00Z".to_string(),
290            status_reason: None,
291            sent_at: None,
292            confirmed_at: None,
293            valid_until: None,
294            network_data: NetworkTransactionData::Evm(EvmTransactionData::default()),
295            network_type: NetworkType::Evm,
296            priced_at: None,
297            hashes: Vec::new(),
298            noop_count: None,
299            is_canceled: Some(false),
300        }
301    }
302}
303
304pub trait EvmTransactionDataTrait {
305    fn is_legacy(&self) -> bool;
306    fn is_eip1559(&self) -> bool;
307    fn is_speed(&self) -> bool;
308}
309
310impl EvmTransactionDataTrait for EvmTransactionData {
311    fn is_legacy(&self) -> bool {
312        self.gas_price.is_some()
313    }
314
315    fn is_eip1559(&self) -> bool {
316        self.max_fee_per_gas.is_some() && self.max_priority_fee_per_gas.is_some()
317    }
318
319    fn is_speed(&self) -> bool {
320        self.speed.is_some()
321    }
322}
323
324#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct SolanaTransactionData {
326    pub recent_blockhash: Option<String>,
327    pub fee_payer: String,
328    pub instructions: Vec<String>,
329    pub hash: Option<String>,
330}
331
332/// Represents different input types for Stellar transactions
333#[derive(Debug, Clone, Serialize, Deserialize)]
334pub enum TransactionInput {
335    /// Operations to be built into a transaction
336    Operations(Vec<OperationSpec>),
337    /// Pre-built unsigned XDR that needs signing
338    UnsignedXdr(String),
339    /// Pre-built signed XDR that needs fee-bumping
340    SignedXdr { xdr: String, max_fee: i64 },
341}
342
343impl Default for TransactionInput {
344    fn default() -> Self {
345        TransactionInput::Operations(vec![])
346    }
347}
348
349impl TransactionInput {
350    /// Create a TransactionInput from a StellarTransactionRequest
351    pub fn from_stellar_request(
352        request: &StellarTransactionRequest,
353    ) -> Result<Self, TransactionError> {
354        // Handle XDR mode
355        if let Some(xdr) = &request.transaction_xdr {
356            let envelope = parse_transaction_xdr(xdr, false)
357                .map_err(|e| TransactionError::ValidationError(e.to_string()))?;
358
359            return if request.fee_bump == Some(true) {
360                // Fee bump requires signed XDR
361                if !is_signed(&envelope) {
362                    Err(TransactionError::ValidationError(
363                        "Cannot request fee_bump with unsigned XDR".to_string(),
364                    ))
365                } else {
366                    let max_fee = request.max_fee.unwrap_or(STELLAR_DEFAULT_MAX_FEE);
367                    Ok(TransactionInput::SignedXdr {
368                        xdr: xdr.clone(),
369                        max_fee,
370                    })
371                }
372            } else {
373                // No fee bump - must be unsigned
374                if is_signed(&envelope) {
375                    Err(TransactionError::ValidationError(
376                        StellarValidationError::UnexpectedSignedXdr.to_string(),
377                    ))
378                } else {
379                    Ok(TransactionInput::UnsignedXdr(xdr.clone()))
380                }
381            };
382        }
383
384        // Handle operations mode
385        if let Some(operations) = &request.operations {
386            if operations.is_empty() {
387                return Err(TransactionError::ValidationError(
388                    "Operations must not be empty".to_string(),
389                ));
390            }
391
392            if request.fee_bump == Some(true) {
393                return Err(TransactionError::ValidationError(
394                    "Cannot request fee_bump with operations mode".to_string(),
395                ));
396            }
397
398            // Validate operations
399            validate_operations(operations)
400                .map_err(|e| TransactionError::ValidationError(e.to_string()))?;
401
402            // Validate Soroban memo restriction
403            validate_soroban_memo_restriction(operations, &request.memo)
404                .map_err(|e| TransactionError::ValidationError(e.to_string()))?;
405
406            return Ok(TransactionInput::Operations(operations.clone()));
407        }
408
409        // Neither XDR nor operations provided
410        Err(TransactionError::ValidationError(
411            "Must provide either operations or transaction_xdr".to_string(),
412        ))
413    }
414}
415
416#[derive(Debug, Clone, Serialize, Deserialize)]
417pub struct StellarTransactionData {
418    pub source_account: String,
419    pub fee: Option<u32>,
420    pub sequence_number: Option<i64>,
421    pub memo: Option<MemoSpec>,
422    pub valid_until: Option<String>,
423    pub network_passphrase: String,
424    #[serde(skip_serializing, skip_deserializing)]
425    pub signatures: Vec<DecoratedSignature>,
426    pub hash: Option<String>,
427    #[serde(skip_serializing, skip_deserializing)]
428    pub simulation_transaction_data: Option<String>,
429    #[serde(skip)]
430    pub transaction_input: TransactionInput,
431    #[serde(skip_serializing, skip_deserializing)]
432    pub signed_envelope_xdr: Option<String>,
433}
434
435impl StellarTransactionData {
436    /// Updates the Stellar transaction data with a specific sequence number.
437    ///
438    /// # Arguments
439    /// * `sequence_number` - The sequence number for the Stellar account
440    ///
441    /// # Returns
442    /// The updated `StellarTransactionData` with the specified sequence number
443    pub fn with_sequence_number(mut self, sequence_number: i64) -> Self {
444        self.sequence_number = Some(sequence_number);
445        self
446    }
447
448    /// Builds an unsigned envelope from any transaction input.
449    ///
450    /// Returns an envelope without signatures, suitable for simulation and fee calculation.
451    ///
452    /// # Returns
453    /// * `Ok(TransactionEnvelope)` containing the unsigned transaction
454    /// * `Err(SignerError)` if the transaction data cannot be converted
455    pub fn build_unsigned_envelope(&self) -> Result<TransactionEnvelope, SignerError> {
456        match &self.transaction_input {
457            TransactionInput::Operations(_) => {
458                // Build from operations without signatures
459                self.build_envelope_from_operations_unsigned()
460            }
461            TransactionInput::UnsignedXdr(xdr) => {
462                // Parse the XDR as-is (already unsigned)
463                self.parse_xdr_envelope(xdr)
464            }
465            TransactionInput::SignedXdr { xdr, .. } => {
466                // Parse the inner transaction (for fee-bump cases)
467                self.parse_xdr_envelope(xdr)
468            }
469        }
470    }
471
472    /// Gets the transaction envelope for simulation purposes.
473    ///
474    /// Convenience method that delegates to build_unsigned_envelope().
475    ///
476    /// # Returns
477    /// * `Ok(TransactionEnvelope)` containing the unsigned transaction
478    /// * `Err(SignerError)` if the transaction data cannot be converted
479    pub fn get_envelope_for_simulation(&self) -> Result<TransactionEnvelope, SignerError> {
480        self.build_unsigned_envelope()
481    }
482
483    /// Builds a signed envelope ready for submission to the network.
484    ///
485    /// Uses cached signed_envelope_xdr if available, otherwise builds from components.
486    ///
487    /// # Returns
488    /// * `Ok(TransactionEnvelope)` containing the signed transaction
489    /// * `Err(SignerError)` if the transaction data cannot be converted
490    pub fn build_signed_envelope(&self) -> Result<TransactionEnvelope, SignerError> {
491        // If we have a cached signed envelope, use it
492        if let Some(ref xdr) = self.signed_envelope_xdr {
493            return self.parse_xdr_envelope(xdr);
494        }
495
496        // Otherwise, build from components
497        match &self.transaction_input {
498            TransactionInput::Operations(_) => {
499                // Build from operations with signatures
500                self.build_envelope_from_operations_signed()
501            }
502            TransactionInput::UnsignedXdr(xdr) => {
503                // Parse and attach signatures
504                let envelope = self.parse_xdr_envelope(xdr)?;
505                self.attach_signatures_to_envelope(envelope)
506            }
507            TransactionInput::SignedXdr { xdr, .. } => {
508                // Already signed
509                self.parse_xdr_envelope(xdr)
510            }
511        }
512    }
513
514    /// Gets the transaction envelope for submission to the network.
515    ///
516    /// Convenience method that delegates to build_signed_envelope().
517    ///
518    /// # Returns
519    /// * `Ok(TransactionEnvelope)` containing the signed transaction
520    /// * `Err(SignerError)` if the transaction data cannot be converted
521    pub fn get_envelope_for_submission(&self) -> Result<TransactionEnvelope, SignerError> {
522        self.build_signed_envelope()
523    }
524
525    // Helper method to build unsigned envelope from operations
526    fn build_envelope_from_operations_unsigned(&self) -> Result<TransactionEnvelope, SignerError> {
527        let tx = SorobanTransaction::try_from(self.clone())?;
528        Ok(TransactionEnvelope::Tx(TransactionV1Envelope {
529            tx,
530            signatures: VecM::default(),
531        }))
532    }
533
534    // Helper method to build signed envelope from operations
535    fn build_envelope_from_operations_signed(&self) -> Result<TransactionEnvelope, SignerError> {
536        let tx = SorobanTransaction::try_from(self.clone())?;
537        let signatures = VecM::try_from(self.signatures.clone())
538            .map_err(|_| SignerError::ConversionError("too many signatures".into()))?;
539        Ok(TransactionEnvelope::Tx(TransactionV1Envelope {
540            tx,
541            signatures,
542        }))
543    }
544
545    // Helper method to parse XDR envelope
546    fn parse_xdr_envelope(&self, xdr: &str) -> Result<TransactionEnvelope, SignerError> {
547        use soroban_rs::xdr::{Limits, ReadXdr};
548        TransactionEnvelope::from_xdr_base64(xdr, Limits::none())
549            .map_err(|e| SignerError::ConversionError(format!("Invalid XDR: {}", e)))
550    }
551
552    // Helper method to attach signatures to an envelope
553    fn attach_signatures_to_envelope(
554        &self,
555        envelope: TransactionEnvelope,
556    ) -> Result<TransactionEnvelope, SignerError> {
557        use soroban_rs::xdr::{Limits, ReadXdr, WriteXdr};
558
559        // Serialize and re-parse to get a mutable version
560        let envelope_xdr = envelope.to_xdr_base64(Limits::none()).map_err(|e| {
561            SignerError::ConversionError(format!("Failed to serialize envelope: {}", e))
562        })?;
563
564        let mut envelope = TransactionEnvelope::from_xdr_base64(&envelope_xdr, Limits::none())
565            .map_err(|e| {
566                SignerError::ConversionError(format!("Failed to parse envelope: {}", e))
567            })?;
568
569        let sigs = VecM::try_from(self.signatures.clone())
570            .map_err(|_| SignerError::ConversionError("too many signatures".into()))?;
571
572        match &mut envelope {
573            TransactionEnvelope::Tx(ref mut v1) => v1.signatures = sigs,
574            TransactionEnvelope::TxV0(ref mut v0) => v0.signatures = sigs,
575            TransactionEnvelope::TxFeeBump(_) => {
576                return Err(SignerError::ConversionError(
577                    "Cannot attach signatures to fee-bump transaction directly".into(),
578                ));
579            }
580        }
581
582        Ok(envelope)
583    }
584
585    /// Updates instance with the given signature appended to the signatures list.
586    ///
587    /// # Arguments
588    /// * `sig` - The decorated signature to append
589    ///
590    /// # Returns
591    /// The updated `StellarTransactionData` with the new signature added
592    pub fn attach_signature(mut self, sig: DecoratedSignature) -> Self {
593        self.signatures.push(sig);
594        self
595    }
596
597    /// Updates instance with the transaction hash populated.
598    ///
599    /// # Arguments
600    /// * `hash` - The transaction hash to set
601    ///
602    /// # Returns
603    /// The updated `StellarTransactionData` with the hash field set
604    pub fn with_hash(mut self, hash: String) -> Self {
605        self.hash = Some(hash);
606        self
607    }
608
609    /// Return a new instance with simulation data applied (fees and transaction extension).
610    pub fn with_simulation_data(
611        mut self,
612        sim_response: soroban_rs::stellar_rpc_client::SimulateTransactionResponse,
613        operations_count: u64,
614    ) -> Result<Self, SignerError> {
615        use log::info;
616
617        // Update fee based on simulation (using soroban-helpers formula)
618        let inclusion_fee = operations_count * STELLAR_DEFAULT_TRANSACTION_FEE as u64;
619        let resource_fee = sim_response.min_resource_fee;
620
621        let updated_fee = u32::try_from(inclusion_fee + resource_fee)
622            .map_err(|_| SignerError::ConversionError("Fee too high".to_string()))?
623            .max(STELLAR_DEFAULT_TRANSACTION_FEE);
624        self.fee = Some(updated_fee);
625
626        // Store simulation transaction data for TransactionExt::V1
627        self.simulation_transaction_data = Some(sim_response.transaction_data);
628
629        info!(
630            "Applied simulation fee: {} stroops and stored transaction extension data",
631            updated_fee
632        );
633        Ok(self)
634    }
635}
636
637impl
638    TryFrom<(
639        &NetworkTransactionRequest,
640        &RelayerRepoModel,
641        &NetworkRepoModel,
642    )> for TransactionRepoModel
643{
644    type Error = RelayerError;
645
646    fn try_from(
647        (request, relayer_model, network_model): (
648            &NetworkTransactionRequest,
649            &RelayerRepoModel,
650            &NetworkRepoModel,
651        ),
652    ) -> Result<Self, Self::Error> {
653        let now = Utc::now().to_rfc3339();
654
655        match request {
656            NetworkTransactionRequest::Evm(evm_request) => {
657                let network = EvmNetwork::try_from(network_model.clone())?;
658                Ok(Self {
659                    id: Uuid::new_v4().to_string(),
660                    relayer_id: relayer_model.id.clone(),
661                    status: TransactionStatus::Pending,
662                    status_reason: None,
663                    created_at: now,
664                    sent_at: None,
665                    confirmed_at: None,
666                    valid_until: evm_request.valid_until.clone(),
667                    network_type: NetworkType::Evm,
668                    network_data: NetworkTransactionData::Evm(EvmTransactionData {
669                        gas_price: evm_request.gas_price,
670                        gas_limit: evm_request.gas_limit,
671                        nonce: None,
672                        value: evm_request.value,
673                        data: evm_request.data.clone(),
674                        from: relayer_model.address.clone(),
675                        to: evm_request.to.clone(),
676                        chain_id: network.id(),
677                        hash: None,
678                        signature: None,
679                        speed: evm_request.speed.clone(),
680                        max_fee_per_gas: evm_request.max_fee_per_gas,
681                        max_priority_fee_per_gas: evm_request.max_priority_fee_per_gas,
682                        raw: None,
683                    }),
684                    priced_at: None,
685                    hashes: Vec::new(),
686                    noop_count: None,
687                    is_canceled: Some(false),
688                })
689            }
690            NetworkTransactionRequest::Solana(solana_request) => Ok(Self {
691                id: Uuid::new_v4().to_string(),
692                relayer_id: relayer_model.id.clone(),
693                status: TransactionStatus::Pending,
694                status_reason: None,
695                created_at: now,
696                sent_at: None,
697                confirmed_at: None,
698                valid_until: None,
699                network_type: NetworkType::Solana,
700                network_data: NetworkTransactionData::Solana(SolanaTransactionData {
701                    recent_blockhash: None,
702                    fee_payer: solana_request.fee_payer.clone(),
703                    instructions: solana_request.instructions.clone(),
704                    hash: None,
705                }),
706                priced_at: None,
707                hashes: Vec::new(),
708                noop_count: None,
709                is_canceled: Some(false),
710            }),
711            NetworkTransactionRequest::Stellar(stellar_request) => {
712                // Store the source account before consuming the request
713                let source_account = stellar_request.source_account.clone();
714
715                // Create the TransactionData before consuming the request
716                let stellar_data = StellarTransactionData {
717                    source_account: source_account.unwrap_or_else(|| relayer_model.address.clone()),
718                    memo: stellar_request.memo.clone(),
719                    valid_until: stellar_request.valid_until.clone(),
720                    network_passphrase: StellarNetwork::try_from(network_model.clone())?.passphrase,
721                    signatures: Vec::new(),
722                    hash: None,
723                    fee: None,
724                    sequence_number: None,
725                    simulation_transaction_data: None,
726                    transaction_input: TransactionInput::from_stellar_request(stellar_request)
727                        .map_err(|e| RelayerError::ValidationError(e.to_string()))?,
728                    signed_envelope_xdr: None,
729                };
730
731                Ok(Self {
732                    id: Uuid::new_v4().to_string(),
733                    relayer_id: relayer_model.id.clone(),
734                    status: TransactionStatus::Pending,
735                    status_reason: None,
736                    created_at: now,
737                    sent_at: None,
738                    confirmed_at: None,
739                    valid_until: None,
740                    network_type: NetworkType::Stellar,
741                    network_data: NetworkTransactionData::Stellar(stellar_data),
742                    priced_at: None,
743                    hashes: Vec::new(),
744                    noop_count: None,
745                    is_canceled: Some(false),
746                })
747            }
748        }
749    }
750}
751
752impl EvmTransactionData {
753    /// Converts the transaction's 'to' field to an Alloy Address.
754    ///
755    /// # Returns
756    /// * `Ok(Some(AlloyAddress))` if the 'to' field contains a valid address
757    /// * `Ok(None)` if the 'to' field is None or empty (contract creation)
758    /// * `Err(SignerError)` if the address format is invalid
759    pub fn to_address(&self) -> Result<Option<AlloyAddress>, SignerError> {
760        Ok(match self.to.as_deref().filter(|s| !s.is_empty()) {
761            Some(addr_str) => Some(AlloyAddress::from_str(addr_str).map_err(|e| {
762                AddressError::ConversionError(format!("Invalid 'to' address: {}", e))
763            })?),
764            None => None,
765        })
766    }
767
768    /// Converts the transaction's data field from hex string to bytes.
769    ///
770    /// # Returns
771    /// * `Ok(Bytes)` containing the decoded transaction data
772    /// * `Err(SignerError)` if the hex string is invalid
773    pub fn data_to_bytes(&self) -> Result<Bytes, SignerError> {
774        Bytes::from_str(self.data.as_deref().unwrap_or(""))
775            .map_err(|e| SignerError::SigningError(format!("Invalid transaction data: {}", e)))
776    }
777}
778
779impl TryFrom<NetworkTransactionData> for TxLegacy {
780    type Error = SignerError;
781
782    fn try_from(tx: NetworkTransactionData) -> Result<Self, Self::Error> {
783        match tx {
784            NetworkTransactionData::Evm(tx) => {
785                let tx_kind = match tx.to_address()? {
786                    Some(addr) => TxKind::Call(addr),
787                    None => TxKind::Create,
788                };
789
790                Ok(Self {
791                    chain_id: Some(tx.chain_id),
792                    nonce: tx.nonce.unwrap_or(0),
793                    gas_limit: tx.gas_limit,
794                    gas_price: tx.gas_price.unwrap_or(0),
795                    to: tx_kind,
796                    value: tx.value,
797                    input: tx.data_to_bytes()?,
798                })
799            }
800            _ => Err(SignerError::SigningError(
801                "Not an EVM transaction".to_string(),
802            )),
803        }
804    }
805}
806
807impl TryFrom<NetworkTransactionData> for TxEip1559 {
808    type Error = SignerError;
809
810    fn try_from(tx: NetworkTransactionData) -> Result<Self, Self::Error> {
811        match tx {
812            NetworkTransactionData::Evm(tx) => {
813                let tx_kind = match tx.to_address()? {
814                    Some(addr) => TxKind::Call(addr),
815                    None => TxKind::Create,
816                };
817
818                Ok(Self {
819                    chain_id: tx.chain_id,
820                    nonce: tx.nonce.unwrap_or(0),
821                    gas_limit: tx.gas_limit,
822                    max_fee_per_gas: tx.max_fee_per_gas.unwrap_or(0),
823                    max_priority_fee_per_gas: tx.max_priority_fee_per_gas.unwrap_or(0),
824                    to: tx_kind,
825                    value: tx.value,
826                    access_list: AccessList::default(),
827                    input: tx.data_to_bytes()?,
828                })
829            }
830            _ => Err(SignerError::SigningError(
831                "Not an EVM transaction".to_string(),
832            )),
833        }
834    }
835}
836
837impl TryFrom<&EvmTransactionData> for TxLegacy {
838    type Error = SignerError;
839
840    fn try_from(tx: &EvmTransactionData) -> Result<Self, Self::Error> {
841        let tx_kind = match tx.to_address()? {
842            Some(addr) => TxKind::Call(addr),
843            None => TxKind::Create,
844        };
845
846        Ok(Self {
847            chain_id: Some(tx.chain_id),
848            nonce: tx.nonce.unwrap_or(0),
849            gas_limit: tx.gas_limit,
850            gas_price: tx.gas_price.unwrap_or(0),
851            to: tx_kind,
852            value: tx.value,
853            input: tx.data_to_bytes()?,
854        })
855    }
856}
857
858impl TryFrom<EvmTransactionData> for TxLegacy {
859    type Error = SignerError;
860
861    fn try_from(tx: EvmTransactionData) -> Result<Self, Self::Error> {
862        Self::try_from(&tx)
863    }
864}
865
866impl TryFrom<&EvmTransactionData> for TxEip1559 {
867    type Error = SignerError;
868
869    fn try_from(tx: &EvmTransactionData) -> Result<Self, Self::Error> {
870        let tx_kind = match tx.to_address()? {
871            Some(addr) => TxKind::Call(addr),
872            None => TxKind::Create,
873        };
874
875        Ok(Self {
876            chain_id: tx.chain_id,
877            nonce: tx.nonce.unwrap_or(0),
878            gas_limit: tx.gas_limit,
879            max_fee_per_gas: tx.max_fee_per_gas.unwrap_or(0),
880            max_priority_fee_per_gas: tx.max_priority_fee_per_gas.unwrap_or(0),
881            to: tx_kind,
882            value: tx.value,
883            access_list: AccessList::default(),
884            input: tx.data_to_bytes()?,
885        })
886    }
887}
888
889impl TryFrom<EvmTransactionData> for TxEip1559 {
890    type Error = SignerError;
891
892    fn try_from(tx: EvmTransactionData) -> Result<Self, Self::Error> {
893        Self::try_from(&tx)
894    }
895}
896
897impl From<&[u8; 65]> for EvmTransactionDataSignature {
898    fn from(bytes: &[u8; 65]) -> Self {
899        Self {
900            r: hex::encode(&bytes[0..32]),
901            s: hex::encode(&bytes[32..64]),
902            v: bytes[64],
903            sig: hex::encode(bytes),
904        }
905    }
906}
907
908#[cfg(test)]
909mod tests {
910    use soroban_rs::xdr::{BytesM, Signature, SignatureHint};
911
912    use super::*;
913    use crate::{
914        config::{
915            EvmNetworkConfig, NetworkConfigCommon, SolanaNetworkConfig, StellarNetworkConfig,
916        },
917        models::{
918            network::NetworkConfigData,
919            relayer::{
920                RelayerEvmPolicy, RelayerNetworkPolicy, RelayerSolanaPolicy, RelayerStellarPolicy,
921            },
922            transaction::stellar::AssetSpec,
923        },
924    };
925
926    #[test]
927    fn test_signature_from_bytes() {
928        let test_bytes: [u8; 65] = [
929            1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
930            25, 26, 27, 28, 29, 30, 31, 32, // r (32 bytes)
931            33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54,
932            55, 56, 57, 58, 59, 60, 61, 62, 63, 64, // s (32 bytes)
933            27, // v (1 byte)
934        ];
935
936        let signature = EvmTransactionDataSignature::from(&test_bytes);
937
938        assert_eq!(signature.r.len(), 64); // 32 bytes in hex
939        assert_eq!(signature.s.len(), 64); // 32 bytes in hex
940        assert_eq!(signature.v, 27);
941        assert_eq!(signature.sig.len(), 130); // 65 bytes in hex
942    }
943
944    // Create a helper function to generate a sample EvmTransactionData for testing
945    fn create_sample_evm_tx_data() -> EvmTransactionData {
946        EvmTransactionData {
947            gas_price: Some(20_000_000_000),
948            gas_limit: 21000,
949            nonce: Some(5),
950            value: U256::from(1000000000000000000u128), // 1 ETH
951            data: Some("0x".to_string()),
952            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
953            to: Some("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_string()),
954            chain_id: 1,
955            hash: None,
956            signature: None,
957            speed: None,
958            max_fee_per_gas: None,
959            max_priority_fee_per_gas: None,
960            raw: None,
961        }
962    }
963
964    // Tests for EvmTransactionData methods
965    #[test]
966    fn test_evm_tx_with_price_params() {
967        let tx_data = create_sample_evm_tx_data();
968        let price_params = PriceParams {
969            gas_price: None,
970            max_fee_per_gas: Some(30_000_000_000),
971            max_priority_fee_per_gas: Some(2_000_000_000),
972            is_min_bumped: None,
973            extra_fee: None,
974            total_cost: U256::ZERO,
975        };
976
977        let updated_tx = tx_data.with_price_params(price_params);
978
979        assert_eq!(updated_tx.max_fee_per_gas, Some(30_000_000_000));
980        assert_eq!(updated_tx.max_priority_fee_per_gas, Some(2_000_000_000));
981    }
982
983    #[test]
984    fn test_evm_tx_with_gas_estimate() {
985        let tx_data = create_sample_evm_tx_data();
986        let new_gas_limit = 30000;
987
988        let updated_tx = tx_data.with_gas_estimate(new_gas_limit);
989
990        assert_eq!(updated_tx.gas_limit, new_gas_limit);
991    }
992
993    #[test]
994    fn test_evm_tx_with_nonce() {
995        let tx_data = create_sample_evm_tx_data();
996        let new_nonce = 10;
997
998        let updated_tx = tx_data.with_nonce(new_nonce);
999
1000        assert_eq!(updated_tx.nonce, Some(new_nonce));
1001    }
1002
1003    #[test]
1004    fn test_evm_tx_with_signed_transaction_data() {
1005        let tx_data = create_sample_evm_tx_data();
1006
1007        let signature = EvmTransactionDataSignature {
1008            r: "r_value".to_string(),
1009            s: "s_value".to_string(),
1010            v: 27,
1011            sig: "signature_value".to_string(),
1012        };
1013
1014        let signed_tx_response = SignTransactionResponseEvm {
1015            signature,
1016            hash: "0xabcdef1234567890".to_string(),
1017            raw: vec![1, 2, 3, 4, 5],
1018        };
1019
1020        let updated_tx = tx_data.with_signed_transaction_data(signed_tx_response);
1021
1022        assert_eq!(updated_tx.signature.as_ref().unwrap().r, "r_value");
1023        assert_eq!(updated_tx.signature.as_ref().unwrap().s, "s_value");
1024        assert_eq!(updated_tx.signature.as_ref().unwrap().v, 27);
1025        assert_eq!(updated_tx.hash, Some("0xabcdef1234567890".to_string()));
1026        assert_eq!(updated_tx.raw, Some(vec![1, 2, 3, 4, 5]));
1027    }
1028
1029    #[test]
1030    fn test_evm_tx_to_address() {
1031        // Test with valid address
1032        let tx_data = create_sample_evm_tx_data();
1033        let address_result = tx_data.to_address();
1034        assert!(address_result.is_ok());
1035        let address_option = address_result.unwrap();
1036        assert!(address_option.is_some());
1037        assert_eq!(
1038            address_option.unwrap().to_string().to_lowercase(),
1039            "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed".to_lowercase()
1040        );
1041
1042        // Test with None address (contract creation)
1043        let mut contract_creation_tx = create_sample_evm_tx_data();
1044        contract_creation_tx.to = None;
1045        let address_result = contract_creation_tx.to_address();
1046        assert!(address_result.is_ok());
1047        assert!(address_result.unwrap().is_none());
1048
1049        // Test with empty address string
1050        let mut empty_address_tx = create_sample_evm_tx_data();
1051        empty_address_tx.to = Some("".to_string());
1052        let address_result = empty_address_tx.to_address();
1053        assert!(address_result.is_ok());
1054        assert!(address_result.unwrap().is_none());
1055
1056        // Test with invalid address
1057        let mut invalid_address_tx = create_sample_evm_tx_data();
1058        invalid_address_tx.to = Some("0xINVALID".to_string());
1059        let address_result = invalid_address_tx.to_address();
1060        assert!(address_result.is_err());
1061    }
1062
1063    #[test]
1064    fn test_evm_tx_data_to_bytes() {
1065        // Test with valid hex data
1066        let mut tx_data = create_sample_evm_tx_data();
1067        tx_data.data = Some("0x1234".to_string());
1068        let bytes_result = tx_data.data_to_bytes();
1069        assert!(bytes_result.is_ok());
1070        assert_eq!(bytes_result.unwrap().as_ref(), &[0x12, 0x34]);
1071
1072        // Test with empty data
1073        tx_data.data = Some("".to_string());
1074        assert!(tx_data.data_to_bytes().is_ok());
1075
1076        // Test with None data
1077        tx_data.data = None;
1078        assert!(tx_data.data_to_bytes().is_ok());
1079
1080        // Test with invalid hex data
1081        tx_data.data = Some("0xZZ".to_string());
1082        assert!(tx_data.data_to_bytes().is_err());
1083    }
1084
1085    // Tests for EvmTransactionDataTrait implementation
1086    #[test]
1087    fn test_evm_tx_is_legacy() {
1088        let mut tx_data = create_sample_evm_tx_data();
1089
1090        // Legacy transaction has gas_price
1091        assert!(tx_data.is_legacy());
1092
1093        // Not legacy if gas_price is None
1094        tx_data.gas_price = None;
1095        assert!(!tx_data.is_legacy());
1096    }
1097
1098    #[test]
1099    fn test_evm_tx_is_eip1559() {
1100        let mut tx_data = create_sample_evm_tx_data();
1101
1102        // Not EIP-1559 initially
1103        assert!(!tx_data.is_eip1559());
1104
1105        // Set EIP-1559 fields
1106        tx_data.max_fee_per_gas = Some(30_000_000_000);
1107        tx_data.max_priority_fee_per_gas = Some(2_000_000_000);
1108        assert!(tx_data.is_eip1559());
1109
1110        // Not EIP-1559 if one field is missing
1111        tx_data.max_priority_fee_per_gas = None;
1112        assert!(!tx_data.is_eip1559());
1113    }
1114
1115    #[test]
1116    fn test_evm_tx_is_speed() {
1117        let mut tx_data = create_sample_evm_tx_data();
1118
1119        // No speed initially
1120        assert!(!tx_data.is_speed());
1121
1122        // Set speed
1123        tx_data.speed = Some(Speed::Fast);
1124        assert!(tx_data.is_speed());
1125    }
1126
1127    // Tests for NetworkTransactionData methods
1128    #[test]
1129    fn test_network_tx_data_get_evm_transaction_data() {
1130        let evm_tx_data = create_sample_evm_tx_data();
1131        let network_data = NetworkTransactionData::Evm(evm_tx_data.clone());
1132
1133        // Should succeed for EVM data
1134        let result = network_data.get_evm_transaction_data();
1135        assert!(result.is_ok());
1136        assert_eq!(result.unwrap().chain_id, evm_tx_data.chain_id);
1137
1138        // Should fail for non-EVM data
1139        let solana_data = NetworkTransactionData::Solana(SolanaTransactionData {
1140            recent_blockhash: None,
1141            fee_payer: "test".to_string(),
1142            instructions: vec![],
1143            hash: None,
1144        });
1145        assert!(solana_data.get_evm_transaction_data().is_err());
1146    }
1147
1148    #[test]
1149    fn test_network_tx_data_get_solana_transaction_data() {
1150        let solana_tx_data = SolanaTransactionData {
1151            recent_blockhash: Some("hash123".to_string()),
1152            fee_payer: "payer123".to_string(),
1153            instructions: vec!["instruction1".to_string()],
1154            hash: None,
1155        };
1156        let network_data = NetworkTransactionData::Solana(solana_tx_data.clone());
1157
1158        // Should succeed for Solana data
1159        let result = network_data.get_solana_transaction_data();
1160        assert!(result.is_ok());
1161        assert_eq!(result.unwrap().fee_payer, solana_tx_data.fee_payer);
1162
1163        // Should fail for non-Solana data
1164        let evm_data = NetworkTransactionData::Evm(create_sample_evm_tx_data());
1165        assert!(evm_data.get_solana_transaction_data().is_err());
1166    }
1167
1168    #[test]
1169    fn test_network_tx_data_get_stellar_transaction_data() {
1170        let stellar_tx_data = StellarTransactionData {
1171            source_account: "account123".to_string(),
1172            fee: Some(100),
1173            sequence_number: Some(5),
1174            memo: Some(MemoSpec::Text {
1175                value: "Test memo".to_string(),
1176            }),
1177            valid_until: Some("2025-01-01T00:00:00Z".to_string()),
1178            network_passphrase: "Test SDF Network ; September 2015".to_string(),
1179            signatures: Vec::new(),
1180            hash: Some("hash123".to_string()),
1181            simulation_transaction_data: None,
1182            transaction_input: TransactionInput::Operations(vec![OperationSpec::Payment {
1183                destination: "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ".to_string(),
1184                amount: 100000000, // 10 XLM in stroops
1185                asset: AssetSpec::Native,
1186            }]),
1187            signed_envelope_xdr: None,
1188        };
1189        let network_data = NetworkTransactionData::Stellar(stellar_tx_data.clone());
1190
1191        // Should succeed for Stellar data
1192        let result = network_data.get_stellar_transaction_data();
1193        assert!(result.is_ok());
1194        assert_eq!(
1195            result.unwrap().source_account,
1196            stellar_tx_data.source_account
1197        );
1198
1199        // Should fail for non-Stellar data
1200        let evm_data = NetworkTransactionData::Evm(create_sample_evm_tx_data());
1201        assert!(evm_data.get_stellar_transaction_data().is_err());
1202    }
1203
1204    // Test for TryFrom<NetworkTransactionData> for TxLegacy
1205    #[test]
1206    fn test_try_from_network_tx_data_for_tx_legacy() {
1207        // Create a valid EVM transaction
1208        let evm_tx_data = create_sample_evm_tx_data();
1209        let network_data = NetworkTransactionData::Evm(evm_tx_data.clone());
1210
1211        // Should convert successfully
1212        let result = TxLegacy::try_from(network_data);
1213        assert!(result.is_ok());
1214        let tx_legacy = result.unwrap();
1215
1216        // Verify fields
1217        assert_eq!(tx_legacy.chain_id, Some(evm_tx_data.chain_id));
1218        assert_eq!(tx_legacy.nonce, evm_tx_data.nonce.unwrap());
1219        assert_eq!(tx_legacy.gas_limit, evm_tx_data.gas_limit);
1220        assert_eq!(tx_legacy.gas_price, evm_tx_data.gas_price.unwrap());
1221        assert_eq!(tx_legacy.value, evm_tx_data.value);
1222
1223        // Should fail for non-EVM data
1224        let solana_data = NetworkTransactionData::Solana(SolanaTransactionData {
1225            recent_blockhash: None,
1226            fee_payer: "test".to_string(),
1227            instructions: vec![],
1228            hash: None,
1229        });
1230        assert!(TxLegacy::try_from(solana_data).is_err());
1231    }
1232
1233    #[test]
1234    fn test_try_from_evm_tx_data_for_tx_legacy() {
1235        // Create a valid EVM transaction with legacy fields
1236        let evm_tx_data = create_sample_evm_tx_data();
1237
1238        // Should convert successfully
1239        let result = TxLegacy::try_from(evm_tx_data.clone());
1240        assert!(result.is_ok());
1241        let tx_legacy = result.unwrap();
1242
1243        // Verify fields
1244        assert_eq!(tx_legacy.chain_id, Some(evm_tx_data.chain_id));
1245        assert_eq!(tx_legacy.nonce, evm_tx_data.nonce.unwrap());
1246        assert_eq!(tx_legacy.gas_limit, evm_tx_data.gas_limit);
1247        assert_eq!(tx_legacy.gas_price, evm_tx_data.gas_price.unwrap());
1248        assert_eq!(tx_legacy.value, evm_tx_data.value);
1249    }
1250
1251    fn dummy_signature() -> DecoratedSignature {
1252        let hint = SignatureHint([0; 4]);
1253        let bytes: Vec<u8> = vec![0u8; 64];
1254        let bytes_m: BytesM<64> = bytes.try_into().expect("BytesM conversion");
1255        DecoratedSignature {
1256            hint,
1257            signature: Signature(bytes_m),
1258        }
1259    }
1260
1261    fn test_stellar_tx_data() -> StellarTransactionData {
1262        StellarTransactionData {
1263            source_account: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
1264            fee: Some(100),
1265            sequence_number: Some(1),
1266            memo: None,
1267            valid_until: None,
1268            network_passphrase: "Test SDF Network ; September 2015".to_string(),
1269            signatures: Vec::new(),
1270            hash: None,
1271            simulation_transaction_data: None,
1272            transaction_input: TransactionInput::Operations(vec![OperationSpec::Payment {
1273                destination: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
1274                amount: 1000,
1275                asset: AssetSpec::Native,
1276            }]),
1277            signed_envelope_xdr: None,
1278        }
1279    }
1280
1281    #[test]
1282    fn test_with_sequence_number() {
1283        let tx = test_stellar_tx_data();
1284        let updated = tx.with_sequence_number(42);
1285        assert_eq!(updated.sequence_number, Some(42));
1286    }
1287
1288    #[test]
1289    fn test_get_envelope_for_simulation() {
1290        let tx = test_stellar_tx_data();
1291        let env = tx.get_envelope_for_simulation();
1292        assert!(env.is_ok());
1293        let env = env.unwrap();
1294        // Should be a TransactionV1Envelope with no signatures
1295        match env {
1296            soroban_rs::xdr::TransactionEnvelope::Tx(tx_env) => {
1297                assert_eq!(tx_env.signatures.len(), 0);
1298            }
1299            _ => {
1300                panic!("Expected TransactionEnvelope::Tx variant");
1301            }
1302        }
1303    }
1304
1305    #[test]
1306    fn test_get_envelope_for_submission() {
1307        let mut tx = test_stellar_tx_data();
1308        tx.signatures.push(dummy_signature());
1309        let env = tx.get_envelope_for_submission();
1310        assert!(env.is_ok());
1311        let env = env.unwrap();
1312        match env {
1313            soroban_rs::xdr::TransactionEnvelope::Tx(tx_env) => {
1314                assert_eq!(tx_env.signatures.len(), 1);
1315            }
1316            _ => {
1317                panic!("Expected TransactionEnvelope::Tx variant");
1318            }
1319        }
1320    }
1321
1322    #[test]
1323    fn test_attach_signature() {
1324        let tx = test_stellar_tx_data();
1325        let sig = dummy_signature();
1326        let updated = tx.attach_signature(sig.clone());
1327        assert_eq!(updated.signatures.len(), 1);
1328        assert_eq!(updated.signatures[0], sig);
1329    }
1330
1331    #[test]
1332    fn test_with_hash() {
1333        let tx = test_stellar_tx_data();
1334        let updated = tx.with_hash("hash123".to_string());
1335        assert_eq!(updated.hash, Some("hash123".to_string()));
1336    }
1337
1338    #[test]
1339    fn test_evm_tx_for_replacement() {
1340        let old_data = create_sample_evm_tx_data();
1341        let new_request = EvmTransactionRequest {
1342            to: Some("0xNewRecipient".to_string()),
1343            value: U256::from(2000000000000000000u64), // 2 ETH
1344            data: Some("0xNewData".to_string()),
1345            gas_limit: 25000,
1346            gas_price: Some(30000000000), // 30 Gwei (should be ignored)
1347            max_fee_per_gas: Some(40000000000), // Should be ignored
1348            max_priority_fee_per_gas: Some(2000000000), // Should be ignored
1349            speed: Some(Speed::Fast),
1350            valid_until: None,
1351        };
1352
1353        let result = EvmTransactionData::for_replacement(&old_data, &new_request);
1354
1355        // Should preserve old data fields
1356        assert_eq!(result.chain_id, old_data.chain_id);
1357        assert_eq!(result.from, old_data.from);
1358        assert_eq!(result.nonce, old_data.nonce);
1359
1360        // Should use new request fields
1361        assert_eq!(result.to, new_request.to);
1362        assert_eq!(result.value, new_request.value);
1363        assert_eq!(result.data, new_request.data);
1364        assert_eq!(result.gas_limit, new_request.gas_limit);
1365        assert_eq!(result.speed, new_request.speed);
1366
1367        // Should clear all pricing fields (regardless of what's in the request)
1368        assert_eq!(result.gas_price, None);
1369        assert_eq!(result.max_fee_per_gas, None);
1370        assert_eq!(result.max_priority_fee_per_gas, None);
1371
1372        // Should reset signing fields
1373        assert_eq!(result.signature, None);
1374        assert_eq!(result.hash, None);
1375        assert_eq!(result.raw, None);
1376    }
1377
1378    #[test]
1379    fn test_transaction_repo_model_validate() {
1380        let transaction = TransactionRepoModel::default();
1381        let result = transaction.validate();
1382        assert!(result.is_ok());
1383    }
1384
1385    #[test]
1386    fn test_try_from_network_transaction_request_evm() {
1387        use crate::models::{NetworkRepoModel, NetworkType, RelayerRepoModel};
1388
1389        let evm_request = NetworkTransactionRequest::Evm(EvmTransactionRequest {
1390            to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
1391            value: U256::from(1000000000000000000u128),
1392            data: Some("0x1234".to_string()),
1393            gas_limit: 21000,
1394            gas_price: Some(20000000000),
1395            max_fee_per_gas: None,
1396            max_priority_fee_per_gas: None,
1397            speed: Some(Speed::Fast),
1398            valid_until: Some("2024-12-31T23:59:59Z".to_string()),
1399        });
1400
1401        let relayer_model = RelayerRepoModel {
1402            id: "relayer-id".to_string(),
1403            name: "Test Relayer".to_string(),
1404            network: "network-id".to_string(),
1405            paused: false,
1406            network_type: NetworkType::Evm,
1407            signer_id: "signer-id".to_string(),
1408            policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
1409            address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
1410            notification_id: None,
1411            system_disabled: false,
1412            custom_rpc_urls: None,
1413        };
1414
1415        let network_model = NetworkRepoModel {
1416            id: "evm:ethereum".to_string(),
1417            name: "ethereum".to_string(),
1418            network_type: NetworkType::Evm,
1419            config: NetworkConfigData::Evm(EvmNetworkConfig {
1420                common: NetworkConfigCommon {
1421                    network: "ethereum".to_string(),
1422                    from: None,
1423                    rpc_urls: Some(vec!["https://mainnet.infura.io".to_string()]),
1424                    explorer_urls: Some(vec!["https://etherscan.io".to_string()]),
1425                    average_blocktime_ms: Some(12000),
1426                    is_testnet: Some(false),
1427                    tags: Some(vec!["mainnet".to_string()]),
1428                },
1429                chain_id: Some(1),
1430                required_confirmations: Some(12),
1431                features: None,
1432                symbol: Some("ETH".to_string()),
1433            }),
1434        };
1435
1436        let result = TransactionRepoModel::try_from((&evm_request, &relayer_model, &network_model));
1437        assert!(result.is_ok());
1438        let transaction = result.unwrap();
1439
1440        assert_eq!(transaction.relayer_id, relayer_model.id);
1441        assert_eq!(transaction.status, TransactionStatus::Pending);
1442        assert_eq!(transaction.network_type, NetworkType::Evm);
1443        assert_eq!(
1444            transaction.valid_until,
1445            Some("2024-12-31T23:59:59Z".to_string())
1446        );
1447        assert!(transaction.is_canceled == Some(false));
1448
1449        if let NetworkTransactionData::Evm(evm_data) = transaction.network_data {
1450            assert_eq!(evm_data.from, relayer_model.address);
1451            assert_eq!(
1452                evm_data.to,
1453                Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string())
1454            );
1455            assert_eq!(evm_data.value, U256::from(1000000000000000000u128));
1456            assert_eq!(evm_data.chain_id, 1);
1457            assert_eq!(evm_data.gas_limit, 21000);
1458            assert_eq!(evm_data.gas_price, Some(20000000000));
1459            assert_eq!(evm_data.speed, Some(Speed::Fast));
1460        } else {
1461            panic!("Expected EVM transaction data");
1462        }
1463    }
1464
1465    #[test]
1466    fn test_try_from_network_transaction_request_solana() {
1467        use crate::models::{
1468            NetworkRepoModel, NetworkTransactionRequest, NetworkType, RelayerRepoModel,
1469        };
1470
1471        let solana_request = NetworkTransactionRequest::Solana(
1472            crate::models::transaction::request::solana::SolanaTransactionRequest {
1473                fee_payer: "fee_payer_address".to_string(),
1474                instructions: vec!["instruction1".to_string(), "instruction2".to_string()],
1475            },
1476        );
1477
1478        let relayer_model = RelayerRepoModel {
1479            id: "relayer-id".to_string(),
1480            name: "Test Solana Relayer".to_string(),
1481            network: "network-id".to_string(),
1482            paused: false,
1483            network_type: NetworkType::Solana,
1484            signer_id: "signer-id".to_string(),
1485            policies: RelayerNetworkPolicy::Solana(RelayerSolanaPolicy::default()),
1486            address: "solana_address".to_string(),
1487            notification_id: None,
1488            system_disabled: false,
1489            custom_rpc_urls: None,
1490        };
1491
1492        let network_model = NetworkRepoModel {
1493            id: "solana:mainnet".to_string(),
1494            name: "mainnet".to_string(),
1495            network_type: NetworkType::Solana,
1496            config: NetworkConfigData::Solana(SolanaNetworkConfig {
1497                common: NetworkConfigCommon {
1498                    network: "mainnet".to_string(),
1499                    from: None,
1500                    rpc_urls: Some(vec!["https://api.mainnet-beta.solana.com".to_string()]),
1501                    explorer_urls: Some(vec!["https://explorer.solana.com".to_string()]),
1502                    average_blocktime_ms: Some(400),
1503                    is_testnet: Some(false),
1504                    tags: Some(vec!["mainnet".to_string()]),
1505                },
1506            }),
1507        };
1508
1509        let result =
1510            TransactionRepoModel::try_from((&solana_request, &relayer_model, &network_model));
1511        assert!(result.is_ok());
1512        let transaction = result.unwrap();
1513
1514        assert_eq!(transaction.relayer_id, relayer_model.id);
1515        assert_eq!(transaction.status, TransactionStatus::Pending);
1516        assert_eq!(transaction.network_type, NetworkType::Solana);
1517        assert_eq!(transaction.valid_until, None);
1518
1519        if let NetworkTransactionData::Solana(solana_data) = transaction.network_data {
1520            assert_eq!(solana_data.fee_payer, "fee_payer_address");
1521            assert_eq!(solana_data.instructions.len(), 2);
1522            assert_eq!(solana_data.instructions[0], "instruction1");
1523            assert_eq!(solana_data.instructions[1], "instruction2");
1524            assert_eq!(solana_data.recent_blockhash, None);
1525            assert_eq!(solana_data.hash, None);
1526        } else {
1527            panic!("Expected Solana transaction data");
1528        }
1529    }
1530
1531    #[test]
1532    fn test_try_from_network_transaction_request_stellar() {
1533        use crate::models::transaction::request::stellar::StellarTransactionRequest;
1534        use crate::models::{
1535            NetworkRepoModel, NetworkTransactionRequest, NetworkType, RelayerRepoModel,
1536        };
1537
1538        let stellar_request = NetworkTransactionRequest::Stellar(StellarTransactionRequest {
1539            source_account: Some(
1540                "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
1541            ),
1542            network: "mainnet".to_string(),
1543            operations: Some(vec![OperationSpec::Payment {
1544                destination: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
1545                amount: 1000000,
1546                asset: AssetSpec::Native,
1547            }]),
1548            memo: Some(MemoSpec::Text {
1549                value: "Test memo".to_string(),
1550            }),
1551            valid_until: Some("2024-12-31T23:59:59Z".to_string()),
1552            transaction_xdr: None,
1553            fee_bump: None,
1554            max_fee: None,
1555        });
1556
1557        let relayer_model = RelayerRepoModel {
1558            id: "relayer-id".to_string(),
1559            name: "Test Stellar Relayer".to_string(),
1560            network: "network-id".to_string(),
1561            paused: false,
1562            network_type: NetworkType::Stellar,
1563            signer_id: "signer-id".to_string(),
1564            policies: RelayerNetworkPolicy::Stellar(RelayerStellarPolicy::default()),
1565            address: "stellar_address".to_string(),
1566            notification_id: None,
1567            system_disabled: false,
1568            custom_rpc_urls: None,
1569        };
1570
1571        let network_model = NetworkRepoModel {
1572            id: "stellar:mainnet".to_string(),
1573            name: "mainnet".to_string(),
1574            network_type: NetworkType::Stellar,
1575            config: NetworkConfigData::Stellar(StellarNetworkConfig {
1576                common: NetworkConfigCommon {
1577                    network: "mainnet".to_string(),
1578                    from: None,
1579                    rpc_urls: Some(vec!["https://horizon.stellar.org".to_string()]),
1580                    explorer_urls: Some(vec!["https://stellarchain.io".to_string()]),
1581                    average_blocktime_ms: Some(5000),
1582                    is_testnet: Some(false),
1583                    tags: Some(vec!["mainnet".to_string()]),
1584                },
1585                passphrase: Some("Public Global Stellar Network ; September 2015".to_string()),
1586            }),
1587        };
1588
1589        let result =
1590            TransactionRepoModel::try_from((&stellar_request, &relayer_model, &network_model));
1591        assert!(result.is_ok());
1592        let transaction = result.unwrap();
1593
1594        assert_eq!(transaction.relayer_id, relayer_model.id);
1595        assert_eq!(transaction.status, TransactionStatus::Pending);
1596        assert_eq!(transaction.network_type, NetworkType::Stellar);
1597        assert_eq!(transaction.valid_until, None);
1598
1599        if let NetworkTransactionData::Stellar(stellar_data) = transaction.network_data {
1600            assert_eq!(
1601                stellar_data.source_account,
1602                "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
1603            );
1604            // Check that transaction_input contains the operations
1605            if let TransactionInput::Operations(ops) = &stellar_data.transaction_input {
1606                assert_eq!(ops.len(), 1);
1607                if let OperationSpec::Payment {
1608                    destination,
1609                    amount,
1610                    asset,
1611                } = &ops[0]
1612                {
1613                    assert_eq!(
1614                        destination,
1615                        "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
1616                    );
1617                    assert_eq!(amount, &1000000);
1618                    assert_eq!(asset, &AssetSpec::Native);
1619                } else {
1620                    panic!("Expected Payment operation");
1621                }
1622            } else {
1623                panic!("Expected Operations transaction input");
1624            }
1625            assert_eq!(
1626                stellar_data.memo,
1627                Some(MemoSpec::Text {
1628                    value: "Test memo".to_string()
1629                })
1630            );
1631            assert_eq!(
1632                stellar_data.valid_until,
1633                Some("2024-12-31T23:59:59Z".to_string())
1634            );
1635            assert_eq!(stellar_data.signatures.len(), 0);
1636            assert_eq!(stellar_data.hash, None);
1637            assert_eq!(stellar_data.fee, None);
1638            assert_eq!(stellar_data.sequence_number, None);
1639        } else {
1640            panic!("Expected Stellar transaction data");
1641        }
1642    }
1643
1644    #[test]
1645    fn test_try_from_network_transaction_data_for_tx_eip1559() {
1646        // Create a valid EVM transaction with EIP-1559 fields
1647        let mut evm_tx_data = create_sample_evm_tx_data();
1648        evm_tx_data.max_fee_per_gas = Some(30_000_000_000);
1649        evm_tx_data.max_priority_fee_per_gas = Some(2_000_000_000);
1650        let network_data = NetworkTransactionData::Evm(evm_tx_data.clone());
1651
1652        // Should convert successfully
1653        let result = TxEip1559::try_from(network_data);
1654        assert!(result.is_ok());
1655        let tx_eip1559 = result.unwrap();
1656
1657        // Verify fields
1658        assert_eq!(tx_eip1559.chain_id, evm_tx_data.chain_id);
1659        assert_eq!(tx_eip1559.nonce, evm_tx_data.nonce.unwrap());
1660        assert_eq!(tx_eip1559.gas_limit, evm_tx_data.gas_limit);
1661        assert_eq!(
1662            tx_eip1559.max_fee_per_gas,
1663            evm_tx_data.max_fee_per_gas.unwrap()
1664        );
1665        assert_eq!(
1666            tx_eip1559.max_priority_fee_per_gas,
1667            evm_tx_data.max_priority_fee_per_gas.unwrap()
1668        );
1669        assert_eq!(tx_eip1559.value, evm_tx_data.value);
1670        assert!(tx_eip1559.access_list.0.is_empty());
1671
1672        // Should fail for non-EVM data
1673        let solana_data = NetworkTransactionData::Solana(SolanaTransactionData {
1674            recent_blockhash: None,
1675            fee_payer: "test".to_string(),
1676            instructions: vec![],
1677            hash: None,
1678        });
1679        assert!(TxEip1559::try_from(solana_data).is_err());
1680    }
1681
1682    #[test]
1683    fn test_evm_transaction_data_defaults() {
1684        let default_data = EvmTransactionData::default();
1685
1686        assert_eq!(
1687            default_data.from,
1688            "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
1689        );
1690        assert_eq!(
1691            default_data.to,
1692            Some("0x70997970C51812dc3A010C7d01b50e0d17dc79C8".to_string())
1693        );
1694        assert_eq!(default_data.gas_price, Some(20000000000));
1695        assert_eq!(default_data.value, U256::from(1000000000000000000u128));
1696        assert_eq!(default_data.data, Some("0x".to_string()));
1697        assert_eq!(default_data.nonce, Some(1));
1698        assert_eq!(default_data.chain_id, 1);
1699        assert_eq!(default_data.gas_limit, 21000);
1700        assert_eq!(default_data.hash, None);
1701        assert_eq!(default_data.signature, None);
1702        assert_eq!(default_data.speed, None);
1703        assert_eq!(default_data.max_fee_per_gas, None);
1704        assert_eq!(default_data.max_priority_fee_per_gas, None);
1705        assert_eq!(default_data.raw, None);
1706    }
1707
1708    #[test]
1709    fn test_transaction_repo_model_defaults() {
1710        let default_model = TransactionRepoModel::default();
1711
1712        assert_eq!(default_model.id, "00000000-0000-0000-0000-000000000001");
1713        assert_eq!(
1714            default_model.relayer_id,
1715            "00000000-0000-0000-0000-000000000002"
1716        );
1717        assert_eq!(default_model.status, TransactionStatus::Pending);
1718        assert_eq!(default_model.created_at, "2023-01-01T00:00:00Z");
1719        assert_eq!(default_model.status_reason, None);
1720        assert_eq!(default_model.sent_at, None);
1721        assert_eq!(default_model.confirmed_at, None);
1722        assert_eq!(default_model.valid_until, None);
1723        assert_eq!(default_model.network_type, NetworkType::Evm);
1724        assert_eq!(default_model.priced_at, None);
1725        assert_eq!(default_model.hashes.len(), 0);
1726        assert_eq!(default_model.noop_count, None);
1727        assert_eq!(default_model.is_canceled, Some(false));
1728    }
1729
1730    #[test]
1731    fn test_evm_tx_for_replacement_with_speed_fallback() {
1732        let mut old_data = create_sample_evm_tx_data();
1733        old_data.speed = Some(Speed::SafeLow);
1734
1735        // Request with no speed - should use old data's speed
1736        let new_request = EvmTransactionRequest {
1737            to: Some("0xNewRecipient".to_string()),
1738            value: U256::from(2000000000000000000u64),
1739            data: Some("0xNewData".to_string()),
1740            gas_limit: 25000,
1741            gas_price: None,
1742            max_fee_per_gas: None,
1743            max_priority_fee_per_gas: None,
1744            speed: None,
1745            valid_until: None,
1746        };
1747
1748        let result = EvmTransactionData::for_replacement(&old_data, &new_request);
1749        assert_eq!(result.speed, Some(Speed::SafeLow));
1750
1751        // Old data with no speed - should use default
1752        let mut old_data_no_speed = create_sample_evm_tx_data();
1753        old_data_no_speed.speed = None;
1754
1755        let result2 = EvmTransactionData::for_replacement(&old_data_no_speed, &new_request);
1756        assert_eq!(result2.speed, Some(DEFAULT_TRANSACTION_SPEED));
1757    }
1758
1759    #[test]
1760    fn test_transaction_status_serialization() {
1761        use serde_json;
1762
1763        // Test serialization of different status values
1764        assert_eq!(
1765            serde_json::to_string(&TransactionStatus::Pending).unwrap(),
1766            "\"pending\""
1767        );
1768        assert_eq!(
1769            serde_json::to_string(&TransactionStatus::Sent).unwrap(),
1770            "\"sent\""
1771        );
1772        assert_eq!(
1773            serde_json::to_string(&TransactionStatus::Mined).unwrap(),
1774            "\"mined\""
1775        );
1776        assert_eq!(
1777            serde_json::to_string(&TransactionStatus::Failed).unwrap(),
1778            "\"failed\""
1779        );
1780        assert_eq!(
1781            serde_json::to_string(&TransactionStatus::Confirmed).unwrap(),
1782            "\"confirmed\""
1783        );
1784        assert_eq!(
1785            serde_json::to_string(&TransactionStatus::Canceled).unwrap(),
1786            "\"canceled\""
1787        );
1788        assert_eq!(
1789            serde_json::to_string(&TransactionStatus::Submitted).unwrap(),
1790            "\"submitted\""
1791        );
1792        assert_eq!(
1793            serde_json::to_string(&TransactionStatus::Expired).unwrap(),
1794            "\"expired\""
1795        );
1796    }
1797
1798    #[test]
1799    fn test_evm_tx_contract_creation() {
1800        // Test transaction data for contract creation (no 'to' address)
1801        let mut tx_data = create_sample_evm_tx_data();
1802        tx_data.to = None;
1803
1804        let tx_legacy = TxLegacy::try_from(&tx_data).unwrap();
1805        assert_eq!(tx_legacy.to, TxKind::Create);
1806
1807        let tx_eip1559 = TxEip1559::try_from(&tx_data).unwrap();
1808        assert_eq!(tx_eip1559.to, TxKind::Create);
1809    }
1810
1811    #[test]
1812    fn test_evm_tx_default_values_in_conversion() {
1813        // Test conversion with missing nonce and gas price
1814        let mut tx_data = create_sample_evm_tx_data();
1815        tx_data.nonce = None;
1816        tx_data.gas_price = None;
1817        tx_data.max_fee_per_gas = None;
1818        tx_data.max_priority_fee_per_gas = None;
1819
1820        let tx_legacy = TxLegacy::try_from(&tx_data).unwrap();
1821        assert_eq!(tx_legacy.nonce, 0); // Default nonce
1822        assert_eq!(tx_legacy.gas_price, 0); // Default gas price
1823
1824        let tx_eip1559 = TxEip1559::try_from(&tx_data).unwrap();
1825        assert_eq!(tx_eip1559.nonce, 0); // Default nonce
1826        assert_eq!(tx_eip1559.max_fee_per_gas, 0); // Default max fee
1827        assert_eq!(tx_eip1559.max_priority_fee_per_gas, 0); // Default max priority fee
1828    }
1829
1830    // Helper function to create test network and relayer models
1831    fn test_models() -> (NetworkRepoModel, RelayerRepoModel) {
1832        use crate::config::{NetworkConfigCommon, StellarNetworkConfig};
1833        use crate::constants::DEFAULT_STELLAR_MIN_BALANCE;
1834
1835        let network_config = NetworkConfigData::Stellar(StellarNetworkConfig {
1836            common: NetworkConfigCommon {
1837                network: "testnet".to_string(),
1838                from: None,
1839                rpc_urls: Some(vec!["https://test.stellar.org".to_string()]),
1840                explorer_urls: None,
1841                average_blocktime_ms: Some(5000), // 5 seconds for Stellar
1842                is_testnet: Some(true),
1843                tags: None,
1844            },
1845            passphrase: Some("Test SDF Network ; September 2015".to_string()),
1846        });
1847
1848        let network_model = NetworkRepoModel {
1849            id: "stellar:testnet".to_string(),
1850            name: "testnet".to_string(),
1851            network_type: NetworkType::Stellar,
1852            config: network_config,
1853        };
1854
1855        let relayer_model = RelayerRepoModel {
1856            id: "test-relayer".to_string(),
1857            name: "Test Relayer".to_string(),
1858            network: "stellar:testnet".to_string(),
1859            paused: false,
1860            network_type: NetworkType::Stellar,
1861            signer_id: "test-signer".to_string(),
1862            policies: RelayerNetworkPolicy::Stellar(RelayerStellarPolicy {
1863                max_fee: None,
1864                timeout_seconds: None,
1865                min_balance: DEFAULT_STELLAR_MIN_BALANCE,
1866            }),
1867            address: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
1868            notification_id: None,
1869            system_disabled: false,
1870            custom_rpc_urls: None,
1871        };
1872
1873        (network_model, relayer_model)
1874    }
1875
1876    #[test]
1877    fn test_stellar_xdr_transaction_input_conversion() {
1878        let (network_model, relayer_model) = test_models();
1879
1880        // Test case 1: Operations mode (existing behavior)
1881        let stellar_request = StellarTransactionRequest {
1882            source_account: Some(
1883                "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
1884            ),
1885            network: "testnet".to_string(),
1886            operations: Some(vec![OperationSpec::Payment {
1887                destination: "GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB".to_string(),
1888                amount: 1000000,
1889                asset: AssetSpec::Native,
1890            }]),
1891            memo: None,
1892            valid_until: None,
1893            transaction_xdr: None,
1894            fee_bump: None,
1895            max_fee: None,
1896        };
1897
1898        let request = NetworkTransactionRequest::Stellar(stellar_request);
1899        let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
1900        assert!(result.is_ok());
1901
1902        let tx_model = result.unwrap();
1903        if let NetworkTransactionData::Stellar(ref stellar_data) = tx_model.network_data {
1904            assert!(matches!(
1905                stellar_data.transaction_input,
1906                TransactionInput::Operations(_)
1907            ));
1908        } else {
1909            panic!("Expected Stellar transaction data");
1910        }
1911
1912        // Test case 2: Unsigned XDR mode
1913        // This is a valid unsigned transaction created with stellar CLI
1914        let unsigned_xdr = "AAAAAgAAAACige4lTdwSB/sto4SniEdJ2kOa2X65s5bqkd40J4DjSwAAAGQAAHAkAAAADgAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAKKB7iVN3BIH+y2jhKeIR0naQ5rZfrmzluqR3jQngONLAAAAAAAAAAAAD0JAAAAAAAAAAAA=";
1915        let stellar_request = StellarTransactionRequest {
1916            source_account: None,
1917            network: "testnet".to_string(),
1918            operations: Some(vec![]),
1919            memo: None,
1920            valid_until: None,
1921            transaction_xdr: Some(unsigned_xdr.to_string()),
1922            fee_bump: None,
1923            max_fee: None,
1924        };
1925
1926        let request = NetworkTransactionRequest::Stellar(stellar_request);
1927        let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
1928        assert!(result.is_ok());
1929
1930        let tx_model = result.unwrap();
1931        if let NetworkTransactionData::Stellar(ref stellar_data) = tx_model.network_data {
1932            assert!(matches!(
1933                stellar_data.transaction_input,
1934                TransactionInput::UnsignedXdr(_)
1935            ));
1936        } else {
1937            panic!("Expected Stellar transaction data");
1938        }
1939
1940        // Test case 3: Signed XDR with fee_bump
1941        // Create a signed XDR by duplicating the test logic from xdr_tests
1942        let signed_xdr = {
1943            use soroban_rs::xdr::{Limits, TransactionEnvelope, TransactionV1Envelope, WriteXdr};
1944            use stellar_strkey::ed25519::PublicKey;
1945
1946            // Use the same transaction structure but add a dummy signature
1947            let source_pk =
1948                PublicKey::from_string("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
1949                    .unwrap();
1950            let dest_pk =
1951                PublicKey::from_string("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ")
1952                    .unwrap();
1953
1954            let payment_op = soroban_rs::xdr::PaymentOp {
1955                destination: soroban_rs::xdr::MuxedAccount::Ed25519(soroban_rs::xdr::Uint256(
1956                    dest_pk.0,
1957                )),
1958                asset: soroban_rs::xdr::Asset::Native,
1959                amount: 1000000,
1960            };
1961
1962            let operation = soroban_rs::xdr::Operation {
1963                source_account: None,
1964                body: soroban_rs::xdr::OperationBody::Payment(payment_op),
1965            };
1966
1967            let operations: soroban_rs::xdr::VecM<soroban_rs::xdr::Operation, 100> =
1968                vec![operation].try_into().unwrap();
1969
1970            let tx = soroban_rs::xdr::Transaction {
1971                source_account: soroban_rs::xdr::MuxedAccount::Ed25519(soroban_rs::xdr::Uint256(
1972                    source_pk.0,
1973                )),
1974                fee: 100,
1975                seq_num: soroban_rs::xdr::SequenceNumber(1),
1976                cond: soroban_rs::xdr::Preconditions::None,
1977                memo: soroban_rs::xdr::Memo::None,
1978                operations,
1979                ext: soroban_rs::xdr::TransactionExt::V0,
1980            };
1981
1982            // Add a dummy signature
1983            let hint = soroban_rs::xdr::SignatureHint([0; 4]);
1984            let sig_bytes: Vec<u8> = vec![0u8; 64];
1985            let sig_bytes_m: soroban_rs::xdr::BytesM<64> = sig_bytes.try_into().unwrap();
1986            let sig = soroban_rs::xdr::DecoratedSignature {
1987                hint,
1988                signature: soroban_rs::xdr::Signature(sig_bytes_m),
1989            };
1990
1991            let envelope = TransactionV1Envelope {
1992                tx,
1993                signatures: vec![sig].try_into().unwrap(),
1994            };
1995
1996            let tx_envelope = TransactionEnvelope::Tx(envelope);
1997            tx_envelope.to_xdr_base64(Limits::none()).unwrap()
1998        };
1999        let stellar_request = StellarTransactionRequest {
2000            source_account: None,
2001            network: "testnet".to_string(),
2002            operations: Some(vec![]),
2003            memo: None,
2004            valid_until: None,
2005            transaction_xdr: Some(signed_xdr.to_string()),
2006            fee_bump: Some(true),
2007            max_fee: Some(20000000),
2008        };
2009
2010        let request = NetworkTransactionRequest::Stellar(stellar_request);
2011        let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2012        assert!(result.is_ok());
2013
2014        let tx_model = result.unwrap();
2015        if let NetworkTransactionData::Stellar(ref stellar_data) = tx_model.network_data {
2016            match &stellar_data.transaction_input {
2017                TransactionInput::SignedXdr { xdr, max_fee } => {
2018                    assert_eq!(xdr, &signed_xdr);
2019                    assert_eq!(*max_fee, 20000000);
2020                }
2021                _ => panic!("Expected SignedXdr transaction input"),
2022            }
2023        } else {
2024            panic!("Expected Stellar transaction data");
2025        }
2026
2027        // Test case 4: Signed XDR without fee_bump should fail
2028        let stellar_request = StellarTransactionRequest {
2029            source_account: None,
2030            network: "testnet".to_string(),
2031            operations: Some(vec![]),
2032            memo: None,
2033            valid_until: None,
2034            transaction_xdr: Some(signed_xdr.clone()),
2035            fee_bump: None,
2036            max_fee: None,
2037        };
2038
2039        let request = NetworkTransactionRequest::Stellar(stellar_request);
2040        let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2041        assert!(result.is_err());
2042        assert!(result
2043            .unwrap_err()
2044            .to_string()
2045            .contains("Expected unsigned XDR but received signed XDR"));
2046
2047        // Test case 5: Operations with fee_bump should fail
2048        let stellar_request = StellarTransactionRequest {
2049            source_account: Some(
2050                "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2051            ),
2052            network: "testnet".to_string(),
2053            operations: Some(vec![OperationSpec::Payment {
2054                destination: "GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB".to_string(),
2055                amount: 1000000,
2056                asset: AssetSpec::Native,
2057            }]),
2058            memo: None,
2059            valid_until: None,
2060            transaction_xdr: None,
2061            fee_bump: Some(true),
2062            max_fee: None,
2063        };
2064
2065        let request = NetworkTransactionRequest::Stellar(stellar_request);
2066        let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2067        assert!(result.is_err());
2068        assert!(result
2069            .unwrap_err()
2070            .to_string()
2071            .contains("Cannot request fee_bump with operations mode"));
2072    }
2073
2074    #[test]
2075    fn test_invoke_host_function_must_be_exclusive() {
2076        let (network_model, relayer_model) = test_models();
2077
2078        // Test case 1: Single InvokeHostFunction - should succeed
2079        let stellar_request = StellarTransactionRequest {
2080            source_account: Some(
2081                "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2082            ),
2083            network: "testnet".to_string(),
2084            operations: Some(vec![OperationSpec::InvokeContract {
2085                contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2086                    .to_string(),
2087                function_name: "transfer".to_string(),
2088                args: vec![],
2089                auth: None,
2090            }]),
2091            memo: None,
2092            valid_until: None,
2093            transaction_xdr: None,
2094            fee_bump: None,
2095            max_fee: None,
2096        };
2097
2098        let request = NetworkTransactionRequest::Stellar(stellar_request);
2099        let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2100        assert!(result.is_ok(), "Single InvokeHostFunction should succeed");
2101
2102        // Test case 2: InvokeHostFunction mixed with Payment - should fail
2103        let stellar_request = StellarTransactionRequest {
2104            source_account: Some(
2105                "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2106            ),
2107            network: "testnet".to_string(),
2108            operations: Some(vec![
2109                OperationSpec::Payment {
2110                    destination: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
2111                        .to_string(),
2112                    amount: 1000,
2113                    asset: AssetSpec::Native,
2114                },
2115                OperationSpec::InvokeContract {
2116                    contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2117                        .to_string(),
2118                    function_name: "transfer".to_string(),
2119                    args: vec![],
2120                    auth: None,
2121                },
2122            ]),
2123            memo: None,
2124            valid_until: None,
2125            transaction_xdr: None,
2126            fee_bump: None,
2127            max_fee: None,
2128        };
2129
2130        let request = NetworkTransactionRequest::Stellar(stellar_request);
2131        let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2132
2133        match result {
2134            Ok(_) => panic!("Expected Soroban operation mixed with Payment to fail"),
2135            Err(err) => {
2136                let err_str = err.to_string();
2137                assert!(
2138                    err_str.contains("Soroban operations must be exclusive"),
2139                    "Expected error about Soroban operation exclusivity, got: {}",
2140                    err_str
2141                );
2142            }
2143        }
2144
2145        // Test case 3: Multiple InvokeHostFunction operations - should fail
2146        let stellar_request = StellarTransactionRequest {
2147            source_account: Some(
2148                "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2149            ),
2150            network: "testnet".to_string(),
2151            operations: Some(vec![
2152                OperationSpec::InvokeContract {
2153                    contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2154                        .to_string(),
2155                    function_name: "transfer".to_string(),
2156                    args: vec![],
2157                    auth: None,
2158                },
2159                OperationSpec::InvokeContract {
2160                    contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2161                        .to_string(),
2162                    function_name: "approve".to_string(),
2163                    args: vec![],
2164                    auth: None,
2165                },
2166            ]),
2167            memo: None,
2168            valid_until: None,
2169            transaction_xdr: None,
2170            fee_bump: None,
2171            max_fee: None,
2172        };
2173
2174        let request = NetworkTransactionRequest::Stellar(stellar_request);
2175        let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2176
2177        match result {
2178            Ok(_) => panic!("Expected multiple Soroban operations to fail"),
2179            Err(err) => {
2180                let err_str = err.to_string();
2181                assert!(
2182                    err_str.contains("Transaction can contain at most one Soroban operation"),
2183                    "Expected error about multiple Soroban operations, got: {}",
2184                    err_str
2185                );
2186            }
2187        }
2188
2189        // Test case 4: Multiple Payment operations - should succeed
2190        let stellar_request = StellarTransactionRequest {
2191            source_account: Some(
2192                "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2193            ),
2194            network: "testnet".to_string(),
2195            operations: Some(vec![
2196                OperationSpec::Payment {
2197                    destination: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
2198                        .to_string(),
2199                    amount: 1000,
2200                    asset: AssetSpec::Native,
2201                },
2202                OperationSpec::Payment {
2203                    destination: "GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"
2204                        .to_string(),
2205                    amount: 2000,
2206                    asset: AssetSpec::Native,
2207                },
2208            ]),
2209            memo: None,
2210            valid_until: None,
2211            transaction_xdr: None,
2212            fee_bump: None,
2213            max_fee: None,
2214        };
2215
2216        let request = NetworkTransactionRequest::Stellar(stellar_request);
2217        let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2218        assert!(result.is_ok(), "Multiple Payment operations should succeed");
2219
2220        // Test case 5: InvokeHostFunction with non-None memo - should fail
2221        let stellar_request = StellarTransactionRequest {
2222            source_account: Some(
2223                "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2224            ),
2225            network: "testnet".to_string(),
2226            operations: Some(vec![OperationSpec::InvokeContract {
2227                contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2228                    .to_string(),
2229                function_name: "transfer".to_string(),
2230                args: vec![],
2231                auth: None,
2232            }]),
2233            memo: Some(MemoSpec::Text {
2234                value: "This should fail".to_string(),
2235            }),
2236            valid_until: None,
2237            transaction_xdr: None,
2238            fee_bump: None,
2239            max_fee: None,
2240        };
2241
2242        let request = NetworkTransactionRequest::Stellar(stellar_request);
2243        let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2244
2245        match result {
2246            Ok(_) => panic!("Expected InvokeHostFunction with non-None memo to fail"),
2247            Err(err) => {
2248                let err_str = err.to_string();
2249                assert!(
2250                    err_str.contains("Soroban operations cannot have a memo"),
2251                    "Expected error about memo restriction, got: {}",
2252                    err_str
2253                );
2254            }
2255        }
2256
2257        // Test case 6: InvokeHostFunction with memo None - should succeed
2258        let stellar_request = StellarTransactionRequest {
2259            source_account: Some(
2260                "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2261            ),
2262            network: "testnet".to_string(),
2263            operations: Some(vec![OperationSpec::InvokeContract {
2264                contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2265                    .to_string(),
2266                function_name: "transfer".to_string(),
2267                args: vec![],
2268                auth: None,
2269            }]),
2270            memo: Some(MemoSpec::None),
2271            valid_until: None,
2272            transaction_xdr: None,
2273            fee_bump: None,
2274            max_fee: None,
2275        };
2276
2277        let request = NetworkTransactionRequest::Stellar(stellar_request);
2278        let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2279        assert!(
2280            result.is_ok(),
2281            "InvokeHostFunction with MemoSpec::None should succeed"
2282        );
2283
2284        // Test case 7: InvokeHostFunction with no memo field - should succeed
2285        let stellar_request = StellarTransactionRequest {
2286            source_account: Some(
2287                "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2288            ),
2289            network: "testnet".to_string(),
2290            operations: Some(vec![OperationSpec::InvokeContract {
2291                contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
2292                    .to_string(),
2293                function_name: "transfer".to_string(),
2294                args: vec![],
2295                auth: None,
2296            }]),
2297            memo: None,
2298            valid_until: None,
2299            transaction_xdr: None,
2300            fee_bump: None,
2301            max_fee: None,
2302        };
2303
2304        let request = NetworkTransactionRequest::Stellar(stellar_request);
2305        let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2306        assert!(
2307            result.is_ok(),
2308            "InvokeHostFunction with no memo should succeed"
2309        );
2310
2311        // Test case 8: Payment operation with memo - should succeed
2312        let stellar_request = StellarTransactionRequest {
2313            source_account: Some(
2314                "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2315            ),
2316            network: "testnet".to_string(),
2317            operations: Some(vec![OperationSpec::Payment {
2318                destination: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
2319                amount: 1000,
2320                asset: AssetSpec::Native,
2321            }]),
2322            memo: Some(MemoSpec::Text {
2323                value: "Payment memo is allowed".to_string(),
2324            }),
2325            valid_until: None,
2326            transaction_xdr: None,
2327            fee_bump: None,
2328            max_fee: None,
2329        };
2330
2331        let request = NetworkTransactionRequest::Stellar(stellar_request);
2332        let result = TransactionRepoModel::try_from((&request, &relayer_model, &network_model));
2333        assert!(result.is_ok(), "Payment operation with memo should succeed");
2334    }
2335}