openzeppelin_relayer/domain/transaction/solana/
solana_transaction.rs

1//! Solana transaction implementation
2//!
3//! This module provides the main SolanaRelayerTransaction struct and
4//! implements the Transaction trait for Solana transactions.
5
6use async_trait::async_trait;
7use chrono::Utc;
8use eyre::Result;
9use solana_sdk::{pubkey::Pubkey, transaction::Transaction as SolanaTransaction};
10use std::str::FromStr;
11use std::sync::Arc;
12use tracing::{debug, error, info, warn};
13
14use crate::{
15    domain::transaction::{
16        solana::{
17            utils::{
18                build_transaction_from_instructions, decode_solana_transaction,
19                decode_solana_transaction_from_string, is_resubmitable,
20            },
21            validation::SolanaTransactionValidator,
22        },
23        Transaction,
24    },
25    jobs::{JobProducer, JobProducerTrait, TransactionSend},
26    models::{
27        produce_transaction_update_notification_payload, EncodedSerializedTransaction,
28        NetworkTransactionData, NetworkTransactionRequest, RelayerRepoModel, SolanaTransactionData,
29        TransactionError, TransactionRepoModel, TransactionStatus, TransactionUpdateRequest,
30    },
31    repositories::{
32        RelayerRepository, RelayerRepositoryStorage, Repository, TransactionRepository,
33        TransactionRepositoryStorage,
34    },
35    services::{
36        provider::{SolanaProvider, SolanaProviderError, SolanaProviderTrait},
37        signer::{SolanaSignTrait, SolanaSigner},
38    },
39};
40
41#[allow(dead_code)]
42pub struct SolanaRelayerTransaction<P, RR, TR, J, S>
43where
44    P: SolanaProviderTrait + Send + Sync + 'static,
45    RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
46    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
47    J: JobProducerTrait + Send + Sync + 'static,
48    S: SolanaSignTrait + Send + Sync + 'static,
49{
50    relayer: RelayerRepoModel,
51    relayer_repository: Arc<RR>,
52    provider: Arc<P>,
53    job_producer: Arc<J>,
54    transaction_repository: Arc<TR>,
55    signer: Arc<S>,
56}
57
58pub type DefaultSolanaTransaction = SolanaRelayerTransaction<
59    SolanaProvider,
60    RelayerRepositoryStorage,
61    TransactionRepositoryStorage,
62    JobProducer,
63    SolanaSigner,
64>;
65
66#[allow(dead_code)]
67impl<P, RR, TR, J, S> SolanaRelayerTransaction<P, RR, TR, J, S>
68where
69    P: SolanaProviderTrait + Send + Sync + 'static,
70    RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
71    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
72    J: JobProducerTrait + Send + Sync + 'static,
73    S: SolanaSignTrait + Send + Sync + 'static,
74{
75    pub fn new(
76        relayer: RelayerRepoModel,
77        relayer_repository: Arc<RR>,
78        provider: Arc<P>,
79        transaction_repository: Arc<TR>,
80        job_producer: Arc<J>,
81        signer: Arc<S>,
82    ) -> Result<Self, TransactionError> {
83        Ok(Self {
84            relayer,
85            relayer_repository,
86            provider,
87            transaction_repository,
88            job_producer,
89            signer,
90        })
91    }
92
93    pub(super) fn provider(&self) -> &P {
94        &self.provider
95    }
96
97    pub(super) fn transaction_repository(&self) -> &TR {
98        &self.transaction_repository
99    }
100
101    pub(super) fn relayer(&self) -> &RelayerRepoModel {
102        &self.relayer
103    }
104
105    pub(super) fn job_producer(&self) -> &J {
106        &self.job_producer
107    }
108
109    pub(super) fn signer(&self) -> &S {
110        &self.signer
111    }
112
113    /// Prepare transaction - validate and sign
114    async fn prepare_transaction_impl(
115        &self,
116        tx: TransactionRepoModel,
117    ) -> Result<TransactionRepoModel, TransactionError> {
118        debug!(tx_id = %tx.id, status = ?tx.status, "preparing Solana transaction");
119
120        // If transaction is not in Pending status, return Ok to avoid wasteful retries
121        // (e.g., if it's already Sent, Failed, or in another state)
122        if tx.status != TransactionStatus::Pending {
123            debug!(
124                tx_id = %tx.id,
125                status = ?tx.status,
126                expected_status = ?TransactionStatus::Pending,
127                "transaction not in Pending status, skipping preparation"
128            );
129            return Ok(tx);
130        }
131
132        let solana_data = tx.network_data.get_solana_transaction_data()?;
133
134        // Build or decode transaction based on input mode
135        let mut transaction = if let Some(transaction_str) = &solana_data.transaction {
136            // Transaction mode: decode pre-built transaction
137            // Use the provided blockhash from user - resubmit logic will handle expiration if needed
138            debug!(
139                tx_id = %tx.id,
140                "transaction mode: using pre-built transaction with provided blockhash"
141            );
142            decode_solana_transaction_from_string(transaction_str)?
143        } else if let Some(instructions) = &solana_data.instructions {
144            // Instructions mode: build transaction from instructions with fresh blockhash
145            debug!(
146                tx_id = %tx.id,
147                "instructions mode: building transaction with fresh blockhash"
148            );
149
150            let payer = Pubkey::from_str(&self.relayer.address).map_err(|e| {
151                TransactionError::ValidationError(format!("Invalid relayer address: {e}"))
152            })?;
153
154            // Fetch fresh blockhash for instructions mode
155            let latest_blockhash = self.provider.get_latest_blockhash().await?;
156
157            build_transaction_from_instructions(instructions, &payer, latest_blockhash)?
158        } else {
159            // Neither transaction nor instructions provided - permanent validation error
160            let validation_error = TransactionError::ValidationError(
161                "Must provide either transaction or instructions".to_string(),
162            );
163
164            let updated_tx = self
165                .fail_transaction_with_notification(&tx, &validation_error)
166                .await?;
167
168            // Return Ok since transaction is in final Failed state - no retry needed
169            return Ok(updated_tx);
170        };
171
172        // Validate transaction before signing
173        // Distinguish between transient errors (RPC issues) and permanent errors (policy violations)
174        if let Err(validation_error) = self.validate_transaction_impl(&transaction).await {
175            // Determine if the error is transient
176            let is_transient = validation_error.is_transient();
177
178            if is_transient {
179                warn!(
180                    tx_id = %tx.id,
181                    error = %validation_error,
182                    "transient validation error (likely RPC/network issue), will retry"
183                );
184                return Err(validation_error);
185            } else {
186                // Permanent validation error (policy violation, insufficient balance, etc.) - mark as failed
187                warn!(
188                    tx_id = %tx.id,
189                    error = %validation_error,
190                    "permanent validation error, marking transaction as failed"
191                );
192
193                let updated_tx = self
194                    .fail_transaction_with_notification(&tx, &validation_error)
195                    .await?;
196
197                // Return Ok since transaction is in final Failed state - no retry needed
198                return Ok(updated_tx);
199            }
200        }
201
202        // Sign transaction
203        let signature = self
204            .signer
205            .sign(&transaction.message_data())
206            .await
207            .map_err(|e| TransactionError::SignerError(e.to_string()))?;
208
209        transaction.signatures[0] = signature;
210
211        // Update transaction with signature
212        let update = TransactionUpdateRequest {
213            status: Some(TransactionStatus::Sent),
214            network_data: Some(NetworkTransactionData::Solana(SolanaTransactionData {
215                signature: Some(signature.to_string()),
216                transaction: Some(
217                    EncodedSerializedTransaction::try_from(&transaction)
218                        .map_err(|e| {
219                            TransactionError::ValidationError(format!(
220                                "Failed to encode transaction: {e}"
221                            ))
222                        })?
223                        .into_inner(),
224                ),
225                instructions: solana_data.instructions,
226            })),
227            ..Default::default()
228        };
229
230        let updated_tx = self
231            .transaction_repository
232            .partial_update(tx.id.clone(), update)
233            .await?;
234
235        // After preparing the transaction, produce a submit job to send it to the blockchain
236        self.job_producer
237            .produce_submit_transaction_job(
238                TransactionSend::submit(updated_tx.id.clone(), updated_tx.relayer_id.clone()),
239                None,
240            )
241            .await?;
242
243        // Send notification as best-effort (errors logged but not propagated)
244        if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
245            error!(
246                tx_id = %updated_tx.id,
247                status = ?TransactionStatus::Sent,
248                "sending transaction update notification failed after prepare: {:?}",
249                e
250            );
251        }
252
253        Ok(updated_tx)
254    }
255
256    /// Submit transaction to blockchain
257    async fn submit_transaction_impl(
258        &self,
259        tx: TransactionRepoModel,
260    ) -> Result<TransactionRepoModel, TransactionError> {
261        debug!(tx_id = %tx.id, status = ?tx.status, "submitting Solana transaction to blockchain");
262
263        if tx.status != TransactionStatus::Sent && tx.status != TransactionStatus::Submitted {
264            debug!(
265                tx_id = %tx.id,
266                status = ?tx.status,
267                "transaction not in expected status for submission, skipping"
268            );
269            return Ok(tx);
270        }
271
272        // Extract Solana transaction data and decode
273        let solana_data = tx.network_data.get_solana_transaction_data()?;
274        let transaction = decode_solana_transaction(&tx)?;
275
276        // Send to blockchain
277        match self.provider.send_transaction(&transaction).await {
278            Ok(sig) => sig,
279            Err(provider_error) => {
280                // Special case: AlreadyProcessed means transaction is already on-chain
281                if matches!(provider_error, SolanaProviderError::AlreadyProcessed(_)) {
282                    debug!(
283                        tx_id = %tx.id,
284                        signature = ?solana_data.signature,
285                        "transaction already processed on-chain"
286                    );
287
288                    // Transaction is already on-chain with existing signature.
289                    // Return as-is - the status check job will query and update to the actual on-chain status.
290                    return Ok(tx);
291                }
292
293                // Special case: BlockhashNotFound handling depends on signature requirements
294                if matches!(provider_error, SolanaProviderError::BlockhashNotFound(_))
295                    && is_resubmitable(&transaction)
296                {
297                    // Single-signer: Can update blockhash via resubmit
298                    // Return Ok to allow status check to detect expiration and trigger resubmit
299                    // The resubmit logic will fetch fresh blockhash, re-sign, and resubmit
300                    debug!(
301                        tx_id = %tx.id,
302                        error = %provider_error,
303                        "blockhash expired for single-signer transaction, status check will trigger resubmit"
304                    );
305                    return Ok(tx);
306                }
307
308                error!(
309                    tx_id = %tx.id,
310                    error = %provider_error,
311                    "failed to send transaction to blockchain"
312                );
313
314                // Check if error is transient or permanent
315                if provider_error.is_transient() {
316                    // Transient error - propagate so job can retry
317                    return Err(TransactionError::UnderlyingSolanaProvider(provider_error));
318                } else {
319                    // Non-transient error - mark as failed and send notification
320                    let error = TransactionError::UnderlyingSolanaProvider(provider_error);
321                    let updated_tx = self.fail_transaction_with_notification(&tx, &error).await?;
322
323                    // Return Ok with failed transaction since it's in final state
324                    return Ok(updated_tx);
325                }
326            }
327        };
328
329        debug!(tx_id = %tx.id, "transaction submitted successfully to blockchain");
330
331        // Transaction is now on-chain - update status and timestamp
332        // Append signature to hashes array to track attempts
333        let signature_str = transaction.signatures[0].to_string();
334        let mut updated_hashes = tx.hashes.clone();
335        updated_hashes.push(signature_str.clone());
336
337        let update = TransactionUpdateRequest {
338            status: Some(TransactionStatus::Submitted),
339            sent_at: Some(Utc::now().to_rfc3339()),
340            hashes: Some(updated_hashes),
341            ..Default::default()
342        };
343
344        let updated_tx = match self
345            .transaction_repository
346            .partial_update(tx.id.clone(), update)
347            .await
348        {
349            Ok(tx) => tx,
350            Err(e) => {
351                error!(
352                    error = %e,
353                    tx_id = %tx.id,
354                    "CRITICAL: transaction sent to blockchain but failed to update database - transaction may not be tracked correctly"
355                );
356                // Transaction is on-chain - don't propagate error to avoid wasteful retries
357                // Return the original transaction data
358                tx
359            }
360        };
361
362        // Send notification as best-effort (errors logged but not propagated)
363        if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
364            error!(
365                tx_id = %updated_tx.id,
366                status = ?TransactionStatus::Submitted,
367                "sending transaction update notification failed after submit: {:?}",
368                e
369            );
370        }
371
372        Ok(updated_tx)
373    }
374
375    /// Resubmit transaction
376    async fn resubmit_transaction_impl(
377        &self,
378        tx: TransactionRepoModel,
379    ) -> Result<TransactionRepoModel, TransactionError> {
380        debug!(tx_id = %tx.id, "resubmitting Solana transaction");
381
382        // Validate transaction is in correct status for resubmission
383        if !matches!(
384            tx.status,
385            TransactionStatus::Sent | TransactionStatus::Submitted
386        ) {
387            warn!(
388                tx_id = %tx.id,
389                status = ?tx.status,
390                "transaction not in expected status for resubmission, skipping"
391            );
392            return Ok(tx);
393        }
394
395        // Decode current transaction
396        let mut transaction = decode_solana_transaction(&tx)?;
397
398        info!(
399            tx_id = %tx.id,
400            old_blockhash = %transaction.message.recent_blockhash,
401            "fetching fresh blockhash for resubmission"
402        );
403
404        // Fetch fresh blockhash
405        // SolanaProviderError automatically converts to TransactionError::UnderlyingSolanaProvider
406        let fresh_blockhash = self.provider.get_latest_blockhash().await?;
407
408        // Update transaction with fresh blockhash
409        transaction.message.recent_blockhash = fresh_blockhash;
410
411        // Re-sign the transaction with the updated message
412        // SignerError automatically converts to TransactionError::SignerError
413        let signature = self.signer.sign(&transaction.message_data()).await?;
414
415        // Update transaction signature
416        transaction.signatures[0] = signature;
417
418        // Append new signature to hashes array to track resubmission attempts
419        let mut updated_hashes = tx.hashes.clone();
420        updated_hashes.push(signature.to_string());
421
422        // Update in repository with Submitted status and new sent_at
423        let update_request = TransactionUpdateRequest {
424            status: Some(TransactionStatus::Submitted),
425            network_data: Some(NetworkTransactionData::Solana(SolanaTransactionData {
426                signature: Some(signature.to_string()),
427                transaction: Some(
428                    EncodedSerializedTransaction::try_from(&transaction)
429                        .map_err(|e| {
430                            TransactionError::ValidationError(format!(
431                                "Failed to encode transaction: {e}"
432                            ))
433                        })?
434                        .into_inner(),
435                ),
436                ..Default::default()
437            })),
438            sent_at: Some(Utc::now().to_rfc3339()),
439            hashes: Some(updated_hashes),
440            ..Default::default()
441        };
442
443        // Send resubmitted transaction to blockchain directly - this is the critical operation
444        let was_already_processed = match self.provider.send_transaction(&transaction).await {
445            Ok(sig) => {
446                info!(
447                    tx_id = %tx.id,
448                    signature = %sig,
449                    new_blockhash = %fresh_blockhash,
450                    "transaction resubmitted successfully with fresh blockhash"
451                );
452                false
453            }
454            Err(e) => {
455                // Special case: AlreadyProcessed means transaction is already on-chain
456                if matches!(e, SolanaProviderError::AlreadyProcessed(_)) {
457                    warn!(
458                        tx_id = %tx.id,
459                        error = %e,
460                        "resubmission indicates transaction already on-chain - keeping original signature"
461                    );
462                    // Don't update with new signature - the original transaction is what's on-chain
463                    true
464                } else if e.is_transient() {
465                    // Transient error (network, RPC) - return for retry
466                    warn!(
467                        tx_id = %tx.id,
468                        error = %e,
469                        "transient error during resubmission, will retry"
470                    );
471                    return Err(TransactionError::UnderlyingSolanaProvider(e));
472                } else {
473                    // Permanent error (invalid tx, insufficient funds) - mark as failed
474                    warn!(
475                        tx_id = %tx.id,
476                        error = %e,
477                        "permanent error during resubmission, marking transaction as failed"
478                    );
479                    let updated_tx = self
480                        .fail_transaction_with_notification(
481                            &tx,
482                            &TransactionError::UnderlyingSolanaProvider(e),
483                        )
484                        .await?;
485                    return Ok(updated_tx);
486                }
487            }
488        };
489
490        // If transaction was already processed, don't update anything - status check will handle it
491        let updated_tx = if was_already_processed {
492            // Transaction already on-chain - return as-is, status check job will update to Confirmed/Mined
493            info!(
494                tx_id = %tx.id,
495                "transaction already on-chain, no update needed - status check will handle confirmation"
496            );
497            tx
498        } else {
499            // Transaction resubmitted successfully - update with new signature and blockhash
500            let tx = match self
501                .transaction_repository
502                .partial_update(tx.id.clone(), update_request)
503                .await
504            {
505                Ok(tx) => tx,
506                Err(e) => {
507                    error!(
508                        error = %e,
509                        tx_id = %tx.id,
510                        "CRITICAL: resubmitted transaction sent to blockchain but failed to update database"
511                    );
512                    // Transaction is on-chain - return original tx data to avoid wasteful retries
513                    tx
514                }
515            };
516
517            info!(
518                tx_id = %tx.id,
519                new_signature = %signature,
520                new_blockhash = %fresh_blockhash,
521                "transaction resubmitted with fresh blockhash"
522            );
523
524            tx
525        };
526
527        Ok(updated_tx)
528    }
529
530    /// Helper method to send transaction update notification.
531    ///
532    /// This is a best-effort operation that logs errors but does not propagate them,
533    /// as notification failures should not affect the transaction lifecycle.
534    pub(super) async fn send_transaction_update_notification(
535        &self,
536        tx: &TransactionRepoModel,
537    ) -> Result<(), eyre::Report> {
538        if let Some(notification_id) = &self.relayer.notification_id {
539            self.job_producer
540                .produce_send_notification_job(
541                    produce_transaction_update_notification_payload(notification_id, tx),
542                    None,
543                )
544                .await?;
545        }
546        Ok(())
547    }
548
549    /// Marks a transaction as failed, updates the database, and sends notification.
550    ///
551    /// This is a convenience method that combines:
552    /// 1. Marking transaction as Failed
553    /// 2. Sending notification (best-effort, errors logged but not propagated)
554    async fn fail_transaction_with_notification(
555        &self,
556        tx: &TransactionRepoModel,
557        error: &TransactionError,
558    ) -> Result<TransactionRepoModel, TransactionError> {
559        let updated_tx = self.mark_transaction_as_failed(tx, error).await?;
560
561        // Send notification as best-effort (errors logged but not propagated)
562        if let Err(e) = self.send_transaction_update_notification(&updated_tx).await {
563            error!(
564                tx_id = %updated_tx.id,
565                status = ?TransactionStatus::Failed,
566                error = %error,
567                notification_error = %e,
568                "failed to send notification for failed transaction"
569            );
570        }
571
572        Ok(updated_tx)
573    }
574
575    /// Marks a transaction as failed and updates the database.
576    async fn mark_transaction_as_failed(
577        &self,
578        tx: &TransactionRepoModel,
579        error: &TransactionError,
580    ) -> Result<TransactionRepoModel, TransactionError> {
581        warn!(
582            tx_id = %tx.id,
583            error = %error,
584            "marking transaction as Failed"
585        );
586
587        let update = TransactionUpdateRequest {
588            status: Some(TransactionStatus::Failed),
589            status_reason: Some(error.to_string()),
590            ..Default::default()
591        };
592
593        let updated_tx = self
594            .transaction_repository
595            .partial_update(tx.id.clone(), update)
596            .await?;
597
598        Ok(updated_tx)
599    }
600
601    async fn validate_transaction_impl(
602        &self,
603        tx: &SolanaTransaction,
604    ) -> Result<(), TransactionError> {
605        use futures::{try_join, TryFutureExt};
606
607        let policy = self.relayer.policies.get_solana_policy();
608        let relayer_pubkey = Pubkey::from_str(&self.relayer.address).map_err(|e| {
609            TransactionError::ValidationError(format!("Invalid relayer address: {e}"))
610        })?;
611
612        // Group all synchronous policy validations together
613        let sync_validations = async {
614            SolanaTransactionValidator::validate_tx_allowed_accounts(tx, &policy)?;
615            SolanaTransactionValidator::validate_tx_disallowed_accounts(tx, &policy)?;
616            SolanaTransactionValidator::validate_allowed_programs(tx, &policy)?;
617            SolanaTransactionValidator::validate_max_signatures(tx, &policy)?;
618            SolanaTransactionValidator::validate_fee_payer(tx, &relayer_pubkey)?;
619            SolanaTransactionValidator::validate_data_size(tx, &policy)?;
620            Ok::<(), TransactionError>(())
621        };
622
623        // Fee calculation and validation (async - needs RPC calls)
624        let fee_validations = async {
625            let fee = self
626                .provider
627                .calculate_total_fee(&tx.message)
628                .await
629                .map_err(TransactionError::from)?;
630
631            SolanaTransactionValidator::validate_max_fee(fee, &policy)?;
632
633            SolanaTransactionValidator::validate_sufficient_relayer_balance(
634                fee,
635                &self.relayer.address,
636                &policy,
637                self.provider.as_ref(),
638            )
639            .await?;
640
641            Ok::<(), TransactionError>(())
642        };
643
644        // Run all validations in parallel for optimal performance
645        // Use map_err to convert SolanaTransactionValidationError to TransactionError
646        try_join!(
647            sync_validations,
648            SolanaTransactionValidator::validate_blockhash(tx, self.provider.as_ref())
649                .map_err(TransactionError::from),
650            SolanaTransactionValidator::simulate_transaction(tx, self.provider.as_ref())
651                .map_ok(|_| ()) // Discard simulation result, we only care about errors
652                .map_err(TransactionError::from),
653            SolanaTransactionValidator::validate_token_transfers(
654                tx,
655                &policy,
656                self.provider.as_ref(),
657                &relayer_pubkey,
658            )
659            .map_err(TransactionError::from),
660            fee_validations,
661        )?;
662
663        Ok(())
664    }
665}
666
667#[async_trait]
668impl<P, RR, TR, J, S> Transaction for SolanaRelayerTransaction<P, RR, TR, J, S>
669where
670    P: SolanaProviderTrait,
671    RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
672    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
673    J: JobProducerTrait + Send + Sync + 'static,
674    S: SolanaSignTrait + Send + Sync + 'static,
675{
676    async fn prepare_transaction(
677        &self,
678        tx: TransactionRepoModel,
679    ) -> Result<TransactionRepoModel, TransactionError> {
680        self.prepare_transaction_impl(tx).await
681    }
682
683    async fn submit_transaction(
684        &self,
685        tx: TransactionRepoModel,
686    ) -> Result<TransactionRepoModel, TransactionError> {
687        self.submit_transaction_impl(tx).await
688    }
689
690    async fn resubmit_transaction(
691        &self,
692        tx: TransactionRepoModel,
693    ) -> Result<TransactionRepoModel, TransactionError> {
694        self.resubmit_transaction_impl(tx).await
695    }
696
697    /// Main entry point for transaction status handling
698    async fn handle_transaction_status(
699        &self,
700        tx: TransactionRepoModel,
701    ) -> Result<TransactionRepoModel, TransactionError> {
702        self.handle_transaction_status_impl(tx).await
703    }
704
705    async fn cancel_transaction(
706        &self,
707        _tx: TransactionRepoModel,
708    ) -> Result<TransactionRepoModel, TransactionError> {
709        Err(TransactionError::NotSupported(
710            "Transaction cancellation is not supported for Solana".to_string(),
711        ))
712    }
713
714    async fn replace_transaction(
715        &self,
716        _old_tx: TransactionRepoModel,
717        _new_tx_request: NetworkTransactionRequest,
718    ) -> Result<TransactionRepoModel, TransactionError> {
719        Err(TransactionError::NotSupported(
720            "Transaction replacement is not supported for Solana".to_string(),
721        ))
722    }
723
724    async fn sign_transaction(
725        &self,
726        _tx: TransactionRepoModel,
727    ) -> Result<TransactionRepoModel, TransactionError> {
728        Err(TransactionError::NotSupported(
729            "Standalone transaction signing is not supported for Solana - signing happens during prepare_transaction".to_string(),
730        ))
731    }
732
733    async fn validate_transaction(
734        &self,
735        tx: TransactionRepoModel,
736    ) -> Result<bool, TransactionError> {
737        debug!(tx_id = %tx.id, "validating Solana transaction");
738
739        // Decode transaction
740        let transaction = decode_solana_transaction(&tx)?;
741
742        // Run validation logic
743        self.validate_transaction_impl(&transaction).await?;
744
745        Ok(true)
746    }
747}
748
749#[cfg(test)]
750mod tests {
751    use super::*;
752    use crate::{
753        jobs::MockJobProducerTrait,
754        models::{
755            Address, NetworkTransactionData, SignerError, SolanaTransactionData, TransactionStatus,
756        },
757        repositories::{MockRelayerRepository, MockTransactionRepository},
758        services::{
759            provider::{MockSolanaProviderTrait, SolanaProviderError},
760            signer::MockSolanaSignTrait,
761        },
762        utils::mocks::mockutils::{create_mock_solana_relayer, create_mock_solana_transaction},
763    };
764    use solana_sdk::{hash::Hash, message::Message, pubkey::Pubkey, signature::Signature};
765    use std::sync::Arc;
766
767    #[tokio::test]
768    async fn test_solana_transaction_creation() {
769        let relayer = create_mock_solana_relayer("test-solana-relayer".to_string(), false);
770        let relayer_repository = Arc::new(MockRelayerRepository::new());
771        let provider = Arc::new(MockSolanaProviderTrait::new());
772        let transaction_repository = Arc::new(MockTransactionRepository::new());
773        let job_producer = Arc::new(MockJobProducerTrait::new());
774        let signer = Arc::new(MockSolanaSignTrait::new());
775
776        let transaction = SolanaRelayerTransaction::new(
777            relayer,
778            relayer_repository,
779            provider,
780            transaction_repository,
781            job_producer,
782            signer,
783        );
784
785        assert!(transaction.is_ok());
786    }
787
788    #[tokio::test]
789    async fn test_prepare_transaction_transaction_mode_success() {
790        let mut provider = MockSolanaProviderTrait::new();
791        let relayer_repo = Arc::new(MockRelayerRepository::new());
792        let mut tx_repo = MockTransactionRepository::new();
793        let mut job_producer = MockJobProducerTrait::new();
794        let mut signer = MockSolanaSignTrait::new();
795
796        let relayer = create_mock_solana_relayer("test-relayer".to_string(), false);
797        let mut tx = create_mock_solana_transaction();
798        tx.status = TransactionStatus::Pending;
799
800        // Create a valid base64-encoded transaction
801        let signer_pubkey = Pubkey::from_str("11111111111111111111111111111112").unwrap();
802        let recipient = Pubkey::new_unique();
803        let message = Message::new(
804            &[solana_system_interface::instruction::transfer(
805                &signer_pubkey,
806                &recipient,
807                1000,
808            )],
809            Some(&signer_pubkey),
810        );
811        let transaction = solana_sdk::transaction::Transaction::new_unsigned(message);
812        let encoded_tx = EncodedSerializedTransaction::try_from(&transaction).unwrap();
813
814        // Set up transaction with pre-built transaction data
815        tx.network_data = NetworkTransactionData::Solana(SolanaTransactionData {
816            transaction: Some(encoded_tx.into_inner()),
817            ..Default::default()
818        });
819
820        let tx_id = tx.id.clone();
821        let tx_id_clone = tx_id.clone();
822        let tx_clone = tx.clone();
823
824        // Mock validation calls
825        provider
826            .expect_calculate_total_fee()
827            .returning(|_| Box::pin(async { Ok(5000) }));
828        provider
829            .expect_get_balance()
830            .returning(|_| Box::pin(async { Ok(1000000) }));
831        provider
832            .expect_is_blockhash_valid()
833            .returning(|_, _| Box::pin(async { Ok(true) }));
834        provider.expect_simulate_transaction().returning(|_| {
835            Box::pin(async {
836                Ok(solana_client::rpc_response::RpcSimulateTransactionResult {
837                    err: None,
838                    logs: Some(vec![]),
839                    accounts: None,
840                    units_consumed: Some(0),
841                    return_data: None,
842                    fee: Some(0),
843                    inner_instructions: None,
844                    loaded_accounts_data_size: Some(0),
845                    replacement_blockhash: None,
846                    pre_balances: Some(vec![]),
847                    post_balances: Some(vec![]),
848                    pre_token_balances: None,
849                    post_token_balances: None,
850                    loaded_addresses: None,
851                })
852            })
853        });
854
855        // Mock signer
856        let signer_pubkey_str = signer_pubkey.to_string();
857        signer.expect_pubkey().returning(move || {
858            let value = signer_pubkey_str.clone();
859            Box::pin(async move { Ok(Address::Solana(value)) })
860        });
861        signer
862            .expect_sign()
863            .returning(|_| Box::pin(async { Ok(Signature::new_unique()) }));
864
865        // Mock repository update
866        tx_repo
867            .expect_partial_update()
868            .withf(move |id, update| {
869                id == &tx_id_clone && matches!(update.status, Some(TransactionStatus::Sent))
870            })
871            .times(1)
872            .returning(move |_, _| {
873                let mut updated_tx = tx_clone.clone();
874                updated_tx.status = TransactionStatus::Sent;
875                Ok(updated_tx)
876            });
877
878        // Mock job producer
879        job_producer
880            .expect_produce_submit_transaction_job()
881            .times(1)
882            .returning(|_, _| Box::pin(async { Ok(()) }));
883
884        let handler = SolanaRelayerTransaction {
885            relayer,
886            relayer_repository: relayer_repo,
887            provider: Arc::new(provider),
888            transaction_repository: Arc::new(tx_repo),
889            job_producer: Arc::new(job_producer),
890            signer: Arc::new(signer),
891        };
892
893        let tx_for_test = tx.clone();
894        let result = handler.prepare_transaction_impl(tx_for_test).await;
895        assert!(result.is_ok());
896        let updated_tx = result.unwrap();
897        assert_eq!(updated_tx.status, TransactionStatus::Sent);
898    }
899
900    #[tokio::test]
901    async fn test_prepare_transaction_instructions_mode_success() {
902        let mut provider = MockSolanaProviderTrait::new();
903        let relayer_repo = Arc::new(MockRelayerRepository::new());
904        let mut tx_repo = MockTransactionRepository::new();
905        let mut job_producer = MockJobProducerTrait::new();
906        let mut signer = MockSolanaSignTrait::new();
907
908        let relayer = create_mock_solana_relayer("test-relayer".to_string(), false);
909        let mut tx = create_mock_solana_transaction();
910        tx.status = TransactionStatus::Pending;
911
912        // Set up transaction with instructions data
913        let instructions = vec![crate::models::SolanaInstructionSpec {
914            program_id: "11111111111111111111111111111112".to_string(),
915            accounts: vec![crate::models::SolanaAccountMeta {
916                pubkey: "11111111111111111111111111111112".to_string(),
917                is_signer: false,
918                is_writable: true,
919            }],
920            data: "AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_string(),
921        }];
922
923        tx.network_data = NetworkTransactionData::Solana(SolanaTransactionData {
924            instructions: Some(instructions),
925            ..Default::default()
926        });
927
928        let tx_id = tx.id.clone();
929        let tx_id_clone = tx_id.clone();
930        let tx_clone = tx.clone();
931
932        // Mock blockhash fetch
933        provider
934            .expect_get_latest_blockhash()
935            .returning(|| Box::pin(async { Ok(Hash::new_unique()) }));
936
937        // Mock validation calls
938        provider
939            .expect_calculate_total_fee()
940            .returning(|_| Box::pin(async { Ok(5000) }));
941        provider
942            .expect_get_balance()
943            .returning(|_| Box::pin(async { Ok(1000000) }));
944        provider
945            .expect_is_blockhash_valid()
946            .returning(|_, _| Box::pin(async { Ok(true) }));
947        provider.expect_simulate_transaction().returning(|_| {
948            Box::pin(async {
949                Ok(solana_client::rpc_response::RpcSimulateTransactionResult {
950                    err: None,
951                    logs: Some(vec![]),
952                    accounts: None,
953                    units_consumed: Some(0),
954                    return_data: None,
955                    fee: Some(0),
956                    inner_instructions: None,
957                    loaded_accounts_data_size: Some(0),
958                    replacement_blockhash: None,
959                    pre_balances: Some(vec![]),
960                    post_balances: Some(vec![]),
961                    pre_token_balances: None,
962                    post_token_balances: None,
963                    loaded_addresses: None,
964                })
965            })
966        });
967
968        // Mock signer
969        let signer_pubkey = Pubkey::from_str("11111111111111111111111111111112").unwrap();
970        signer.expect_pubkey().returning(move || {
971            Box::pin(async move { Ok(Address::Solana(signer_pubkey.to_string())) })
972        });
973        signer
974            .expect_sign()
975            .returning(|_| Box::pin(async { Ok(Signature::new_unique()) }));
976
977        // Mock repository update
978        tx_repo
979            .expect_partial_update()
980            .withf(move |id, update| {
981                id == &tx_id_clone && matches!(update.status, Some(TransactionStatus::Sent))
982            })
983            .times(1)
984            .returning(move |_, _| {
985                let mut updated_tx = tx_clone.clone();
986                updated_tx.status = TransactionStatus::Sent;
987                Ok(updated_tx)
988            });
989
990        // Mock job producer
991        job_producer
992            .expect_produce_submit_transaction_job()
993            .times(1)
994            .returning(|_, _| Box::pin(async { Ok(()) }));
995
996        let handler = SolanaRelayerTransaction {
997            relayer,
998            relayer_repository: relayer_repo,
999            provider: Arc::new(provider),
1000            transaction_repository: Arc::new(tx_repo),
1001            job_producer: Arc::new(job_producer),
1002            signer: Arc::new(signer),
1003        };
1004
1005        let tx_for_test = tx.clone();
1006        let result = handler.prepare_transaction_impl(tx_for_test).await;
1007        assert!(result.is_ok());
1008        let updated_tx = result.unwrap();
1009        assert_eq!(updated_tx.status, TransactionStatus::Sent);
1010    }
1011
1012    #[tokio::test]
1013    async fn test_prepare_transaction_validation_failure() {
1014        let provider = MockSolanaProviderTrait::new();
1015        let relayer_repo = Arc::new(MockRelayerRepository::new());
1016        let mut tx_repo = MockTransactionRepository::new();
1017        let job_producer = Arc::new(MockJobProducerTrait::new());
1018        let signer = MockSolanaSignTrait::new();
1019
1020        let relayer = create_mock_solana_relayer("test-relayer".to_string(), false);
1021        let mut tx = create_mock_solana_transaction();
1022        tx.status = TransactionStatus::Pending;
1023
1024        // Create transaction with invalid data (missing both transaction and instructions)
1025        tx.network_data = NetworkTransactionData::Solana(SolanaTransactionData::default());
1026
1027        let tx_id = tx.id.clone();
1028
1029        // Mock repository update
1030        let tx_for_closure = tx.clone();
1031        tx_repo
1032            .expect_partial_update()
1033            .withf(move |id, update| {
1034                id == &tx_id && matches!(update.status, Some(TransactionStatus::Failed))
1035            })
1036            .times(1)
1037            .returning(move |_, _| {
1038                let mut updated_tx = tx_for_closure.clone();
1039                updated_tx.status = TransactionStatus::Failed;
1040                Ok(updated_tx)
1041            });
1042
1043        let handler = SolanaRelayerTransaction {
1044            relayer,
1045            relayer_repository: relayer_repo,
1046            provider: Arc::new(provider),
1047            transaction_repository: Arc::new(tx_repo),
1048            job_producer,
1049            signer: Arc::new(signer),
1050        };
1051
1052        let tx_for_test = tx.clone();
1053        let result = handler.prepare_transaction_impl(tx_for_test).await;
1054        assert!(result.is_ok()); // Returns Ok with failed transaction
1055        let updated_tx = result.unwrap();
1056        assert_eq!(updated_tx.status, TransactionStatus::Failed);
1057    }
1058
1059    #[tokio::test]
1060    async fn test_prepare_transaction_signer_error() {
1061        let mut provider = MockSolanaProviderTrait::new();
1062        let relayer_repo = Arc::new(MockRelayerRepository::new());
1063        let tx_repo = Arc::new(MockTransactionRepository::new());
1064        let job_producer = Arc::new(MockJobProducerTrait::new());
1065        let mut signer = MockSolanaSignTrait::new();
1066
1067        let relayer = create_mock_solana_relayer("test-relayer".to_string(), false);
1068        let mut tx = create_mock_solana_transaction();
1069        tx.status = TransactionStatus::Pending;
1070
1071        // Create a valid transaction
1072        let signer_pubkey = Pubkey::from_str("11111111111111111111111111111112").unwrap();
1073        let recipient = Pubkey::new_unique();
1074        let message = Message::new(
1075            &[solana_system_interface::instruction::transfer(
1076                &signer_pubkey,
1077                &recipient,
1078                1000,
1079            )],
1080            Some(&signer_pubkey),
1081        );
1082        let transaction = solana_sdk::transaction::Transaction::new_unsigned(message);
1083        let encoded_tx = EncodedSerializedTransaction::try_from(&transaction).unwrap();
1084
1085        tx.network_data = NetworkTransactionData::Solana(SolanaTransactionData {
1086            transaction: Some(encoded_tx.into_inner()),
1087            ..Default::default()
1088        });
1089
1090        // Mock validation calls (needed before signer is called)
1091        provider
1092            .expect_calculate_total_fee()
1093            .returning(|_| Box::pin(async { Ok(5000) }));
1094        provider
1095            .expect_get_balance()
1096            .returning(|_| Box::pin(async { Ok(1000000) }));
1097        provider
1098            .expect_is_blockhash_valid()
1099            .returning(|_, _| Box::pin(async { Ok(true) }));
1100        provider.expect_simulate_transaction().returning(|_| {
1101            Box::pin(async {
1102                Ok(solana_client::rpc_response::RpcSimulateTransactionResult {
1103                    err: None,
1104                    logs: Some(vec![]),
1105                    accounts: None,
1106                    units_consumed: Some(0),
1107                    return_data: None,
1108                    fee: Some(0),
1109                    inner_instructions: None,
1110                    loaded_accounts_data_size: Some(0),
1111                    replacement_blockhash: None,
1112                    pre_balances: Some(vec![]),
1113                    post_balances: Some(vec![]),
1114                    pre_token_balances: None,
1115                    post_token_balances: None,
1116                    loaded_addresses: None,
1117                })
1118            })
1119        });
1120
1121        // Mock signer to return error
1122        let signer_pubkey_str = signer_pubkey.to_string();
1123        signer.expect_pubkey().returning(move || {
1124            let value = signer_pubkey_str.clone();
1125            Box::pin(async move { Ok(Address::Solana(value)) })
1126        });
1127        signer.expect_sign().returning(|_| {
1128            Box::pin(async { Err(SignerError::SigningError("Signer failed".to_string())) })
1129        });
1130
1131        let handler = SolanaRelayerTransaction {
1132            relayer,
1133            relayer_repository: relayer_repo,
1134            provider: Arc::new(provider),
1135            transaction_repository: tx_repo,
1136            job_producer,
1137            signer: Arc::new(signer),
1138        };
1139
1140        let tx_for_test = tx.clone();
1141        let result = handler.prepare_transaction_impl(tx_for_test).await;
1142        assert!(result.is_err());
1143        let error = result.unwrap_err();
1144        match error {
1145            TransactionError::SignerError(msg) => assert!(msg.contains("Signer failed")),
1146            _ => panic!("Expected SignerError"),
1147        }
1148    }
1149
1150    #[tokio::test]
1151    async fn test_submit_transaction_success() {
1152        let mut provider = MockSolanaProviderTrait::new();
1153        let relayer_repo = Arc::new(MockRelayerRepository::new());
1154        let mut tx_repo = MockTransactionRepository::new();
1155        let job_producer = Arc::new(MockJobProducerTrait::new());
1156        let signer = MockSolanaSignTrait::new();
1157
1158        let relayer = create_mock_solana_relayer("test-relayer".to_string(), false);
1159        let mut tx = create_mock_solana_transaction();
1160        tx.status = TransactionStatus::Sent;
1161
1162        // Create a valid transaction with signature
1163        let signer_pubkey = Pubkey::from_str("11111111111111111111111111111112").unwrap();
1164        let recipient = Pubkey::new_unique();
1165        let message = Message::new(
1166            &[solana_system_interface::instruction::transfer(
1167                &signer_pubkey,
1168                &recipient,
1169                1000,
1170            )],
1171            Some(&signer_pubkey),
1172        );
1173        let mut transaction = solana_sdk::transaction::Transaction::new_unsigned(message);
1174        let signature = Signature::new_unique();
1175        transaction.signatures = vec![signature];
1176
1177        let encoded_tx = EncodedSerializedTransaction::try_from(&transaction).unwrap();
1178
1179        tx.network_data = NetworkTransactionData::Solana(SolanaTransactionData {
1180            transaction: Some(encoded_tx.into_inner()),
1181            signature: Some(signature.to_string()),
1182            ..Default::default()
1183        });
1184
1185        let tx_id = tx.id.clone();
1186        let tx_id_clone = tx_id.clone();
1187        let tx_clone = tx.clone();
1188
1189        // Mock successful send
1190        provider
1191            .expect_send_transaction()
1192            .returning(|_| Box::pin(async { Ok(Signature::new_unique()) }));
1193
1194        // Mock repository update
1195        tx_repo
1196            .expect_partial_update()
1197            .withf(move |id, update| {
1198                id == &tx_id_clone && matches!(update.status, Some(TransactionStatus::Submitted))
1199            })
1200            .times(1)
1201            .returning(move |_, _| {
1202                let mut updated_tx = tx_clone.clone();
1203                updated_tx.status = TransactionStatus::Submitted;
1204                Ok(updated_tx)
1205            });
1206
1207        let handler = SolanaRelayerTransaction {
1208            relayer,
1209            relayer_repository: relayer_repo,
1210            provider: Arc::new(provider),
1211            transaction_repository: Arc::new(tx_repo),
1212            job_producer,
1213            signer: Arc::new(signer),
1214        };
1215
1216        let tx_for_test = tx.clone();
1217        let result = handler.submit_transaction_impl(tx_for_test).await;
1218        assert!(result.is_ok());
1219        let updated_tx = result.unwrap();
1220        assert_eq!(updated_tx.status, TransactionStatus::Submitted);
1221    }
1222
1223    #[tokio::test]
1224    async fn test_submit_transaction_already_processed() {
1225        let mut provider = MockSolanaProviderTrait::new();
1226        let relayer_repo = Arc::new(MockRelayerRepository::new());
1227        let tx_repo = Arc::new(MockTransactionRepository::new());
1228        let job_producer = Arc::new(MockJobProducerTrait::new());
1229        let signer = MockSolanaSignTrait::new();
1230
1231        let relayer = create_mock_solana_relayer("test-relayer".to_string(), false);
1232        let mut tx = create_mock_solana_transaction();
1233        tx.status = TransactionStatus::Sent;
1234
1235        // Create a valid transaction
1236        let signer_pubkey = Pubkey::from_str("11111111111111111111111111111112").unwrap();
1237        let recipient = Pubkey::new_unique();
1238        let message = Message::new(
1239            &[solana_system_interface::instruction::transfer(
1240                &signer_pubkey,
1241                &recipient,
1242                1000,
1243            )],
1244            Some(&signer_pubkey),
1245        );
1246        let mut transaction = solana_sdk::transaction::Transaction::new_unsigned(message);
1247        let signature = Signature::new_unique();
1248        transaction.signatures = vec![signature];
1249
1250        let encoded_tx = EncodedSerializedTransaction::try_from(&transaction).unwrap();
1251
1252        tx.network_data = NetworkTransactionData::Solana(SolanaTransactionData {
1253            transaction: Some(encoded_tx.into_inner()),
1254            signature: Some(signature.to_string()),
1255            ..Default::default()
1256        });
1257
1258        // Mock provider to return AlreadyProcessed
1259        provider.expect_send_transaction().returning(|_| {
1260            Box::pin(async {
1261                Err(SolanaProviderError::AlreadyProcessed(
1262                    "Already processed".to_string(),
1263                ))
1264            })
1265        });
1266
1267        let handler = SolanaRelayerTransaction {
1268            relayer,
1269            relayer_repository: relayer_repo,
1270            provider: Arc::new(provider),
1271            transaction_repository: tx_repo,
1272            job_producer,
1273            signer: Arc::new(signer),
1274        };
1275
1276        let result = handler.submit_transaction_impl(tx.clone()).await;
1277        assert!(result.is_ok());
1278        let updated_tx = result.unwrap();
1279        assert_eq!(updated_tx.status, tx.status); // Status unchanged
1280    }
1281
1282    #[tokio::test]
1283    async fn test_submit_transaction_blockhash_expired_resubmitable() {
1284        let mut provider = MockSolanaProviderTrait::new();
1285        let relayer_repo = Arc::new(MockRelayerRepository::new());
1286        let tx_repo = Arc::new(MockTransactionRepository::new());
1287        let job_producer = Arc::new(MockJobProducerTrait::new());
1288        let signer = MockSolanaSignTrait::new();
1289
1290        let relayer = create_mock_solana_relayer("test-relayer".to_string(), false);
1291        let mut tx = create_mock_solana_transaction();
1292        tx.status = TransactionStatus::Sent;
1293
1294        // Create a single-signer transaction (resubmitable)
1295        let signer_pubkey = Pubkey::from_str("11111111111111111111111111111112").unwrap();
1296        let recipient = Pubkey::new_unique();
1297        let message = Message::new(
1298            &[solana_system_interface::instruction::transfer(
1299                &signer_pubkey,
1300                &recipient,
1301                1000,
1302            )],
1303            Some(&signer_pubkey),
1304        );
1305        let mut transaction = solana_sdk::transaction::Transaction::new_unsigned(message);
1306        let signature = Signature::new_unique();
1307        transaction.signatures = vec![signature];
1308
1309        let encoded_tx = EncodedSerializedTransaction::try_from(&transaction).unwrap();
1310
1311        tx.network_data = NetworkTransactionData::Solana(SolanaTransactionData {
1312            transaction: Some(encoded_tx.into_inner()),
1313            signature: Some(signature.to_string()),
1314            ..Default::default()
1315        });
1316
1317        // Mock provider to return BlockhashNotFound
1318        provider.expect_send_transaction().returning(|_| {
1319            Box::pin(async {
1320                Err(SolanaProviderError::BlockhashNotFound(
1321                    "Blockhash not found".to_string(),
1322                ))
1323            })
1324        });
1325
1326        let handler = SolanaRelayerTransaction {
1327            relayer,
1328            relayer_repository: relayer_repo,
1329            provider: Arc::new(provider),
1330            transaction_repository: tx_repo,
1331            job_producer,
1332            signer: Arc::new(signer),
1333        };
1334
1335        let result = handler.submit_transaction_impl(tx.clone()).await;
1336        assert!(result.is_ok());
1337        let updated_tx = result.unwrap();
1338        assert_eq!(updated_tx.status, tx.status); // Status unchanged, resubmit scheduled
1339    }
1340
1341    #[tokio::test]
1342    async fn test_submit_transaction_permanent_error() {
1343        let mut provider = MockSolanaProviderTrait::new();
1344        let relayer_repo = Arc::new(MockRelayerRepository::new());
1345        let mut tx_repo = MockTransactionRepository::new();
1346        let job_producer = Arc::new(MockJobProducerTrait::new());
1347        let signer = MockSolanaSignTrait::new();
1348
1349        let relayer = create_mock_solana_relayer("test-relayer".to_string(), false);
1350        let mut tx = create_mock_solana_transaction();
1351        tx.status = TransactionStatus::Sent;
1352
1353        // Create a valid transaction
1354        let signer_pubkey = Pubkey::from_str("11111111111111111111111111111112").unwrap();
1355        let recipient = Pubkey::new_unique();
1356        let message = Message::new(
1357            &[solana_system_interface::instruction::transfer(
1358                &signer_pubkey,
1359                &recipient,
1360                1000,
1361            )],
1362            Some(&signer_pubkey),
1363        );
1364        let mut transaction = solana_sdk::transaction::Transaction::new_unsigned(message);
1365        let signature = Signature::new_unique();
1366        transaction.signatures = vec![signature];
1367
1368        let encoded_tx = EncodedSerializedTransaction::try_from(&transaction).unwrap();
1369
1370        tx.network_data = NetworkTransactionData::Solana(SolanaTransactionData {
1371            transaction: Some(encoded_tx.into_inner()),
1372            signature: Some(signature.to_string()),
1373            ..Default::default()
1374        });
1375
1376        let tx_id = tx.id.clone();
1377        let tx_clone = tx.clone();
1378
1379        // Mock provider to return permanent error
1380        provider.expect_send_transaction().returning(|_| {
1381            Box::pin(async {
1382                Err(SolanaProviderError::InsufficientFunds(
1383                    "Insufficient balance".to_string(),
1384                ))
1385            })
1386        });
1387
1388        // Mock repository update to failed
1389        tx_repo
1390            .expect_partial_update()
1391            .withf(move |id, update| {
1392                id == &tx_id && matches!(update.status, Some(TransactionStatus::Failed))
1393            })
1394            .times(1)
1395            .returning(move |_, _| {
1396                let mut updated_tx = tx_clone.clone();
1397                updated_tx.status = TransactionStatus::Failed;
1398                Ok(updated_tx)
1399            });
1400
1401        let handler = SolanaRelayerTransaction {
1402            relayer,
1403            relayer_repository: relayer_repo,
1404            provider: Arc::new(provider),
1405            transaction_repository: Arc::new(tx_repo),
1406            job_producer,
1407            signer: Arc::new(signer),
1408        };
1409
1410        let tx_for_test = tx.clone();
1411        let result = handler.submit_transaction_impl(tx_for_test).await;
1412        assert!(result.is_ok()); // Returns Ok with failed transaction
1413        let updated_tx = result.unwrap();
1414        assert_eq!(updated_tx.status, TransactionStatus::Failed);
1415    }
1416
1417    #[tokio::test]
1418    async fn test_resubmit_transaction_success() {
1419        let mut provider = MockSolanaProviderTrait::new();
1420        let relayer_repo = Arc::new(MockRelayerRepository::new());
1421        let mut tx_repo = MockTransactionRepository::new();
1422        let job_producer = Arc::new(MockJobProducerTrait::new());
1423        let mut signer = MockSolanaSignTrait::new();
1424
1425        let relayer = create_mock_solana_relayer("test-relayer".to_string(), false);
1426        let mut tx = create_mock_solana_transaction();
1427        tx.status = TransactionStatus::Submitted;
1428
1429        // Create a valid transaction
1430        let signer_pubkey = Pubkey::from_str("11111111111111111111111111111112").unwrap();
1431        let recipient = Pubkey::new_unique();
1432        let message = Message::new(
1433            &[solana_system_interface::instruction::transfer(
1434                &signer_pubkey,
1435                &recipient,
1436                1000,
1437            )],
1438            Some(&signer_pubkey),
1439        );
1440        let mut transaction = solana_sdk::transaction::Transaction::new_unsigned(message);
1441        let signature = Signature::new_unique();
1442        transaction.signatures = vec![signature];
1443
1444        let encoded_tx = EncodedSerializedTransaction::try_from(&transaction).unwrap();
1445
1446        tx.network_data = NetworkTransactionData::Solana(SolanaTransactionData {
1447            transaction: Some(encoded_tx.into_inner()),
1448            signature: Some(signature.to_string()),
1449            ..Default::default()
1450        });
1451
1452        let tx_id = tx.id.clone();
1453        let tx_id_clone = tx_id.clone();
1454        let tx_clone = tx.clone();
1455        let tx_for_test = tx.clone();
1456
1457        // Mock fresh blockhash
1458        provider
1459            .expect_get_latest_blockhash()
1460            .returning(|| Box::pin(async { Ok(Hash::new_unique()) }));
1461
1462        // Mock signer
1463        let signer_pubkey_str = signer_pubkey.to_string();
1464        signer.expect_pubkey().returning(move || {
1465            let value = signer_pubkey_str.clone();
1466            Box::pin(async move { Ok(Address::Solana(value)) })
1467        });
1468        signer
1469            .expect_sign()
1470            .returning(|_| Box::pin(async { Ok(Signature::new_unique()) }));
1471
1472        // Mock successful resubmit
1473        provider
1474            .expect_send_transaction()
1475            .returning(|_| Box::pin(async { Ok(Signature::new_unique()) }));
1476
1477        // Mock repository update
1478        tx_repo
1479            .expect_partial_update()
1480            .withf(move |id, update| {
1481                id == &tx_id_clone && matches!(update.status, Some(TransactionStatus::Submitted))
1482            })
1483            .times(1)
1484            .returning(move |_, _| {
1485                let mut updated_tx = tx_clone.clone();
1486                updated_tx.status = TransactionStatus::Submitted;
1487                Ok(updated_tx)
1488            });
1489
1490        let handler = SolanaRelayerTransaction {
1491            relayer,
1492            relayer_repository: relayer_repo,
1493            provider: Arc::new(provider),
1494            transaction_repository: Arc::new(tx_repo),
1495            job_producer,
1496            signer: Arc::new(signer),
1497        };
1498
1499        let result = handler.resubmit_transaction_impl(tx_for_test).await;
1500        assert!(result.is_ok());
1501        let updated_tx = result.unwrap();
1502        assert_eq!(updated_tx.status, TransactionStatus::Submitted);
1503    }
1504
1505    #[tokio::test]
1506    async fn test_validate_transaction_success() {
1507        let mut provider = MockSolanaProviderTrait::new();
1508        let relayer_repo = Arc::new(MockRelayerRepository::new());
1509        let tx_repo = Arc::new(MockTransactionRepository::new());
1510        let job_producer = Arc::new(MockJobProducerTrait::new());
1511        let signer = MockSolanaSignTrait::new();
1512
1513        let relayer = create_mock_solana_relayer("test-relayer".to_string(), false);
1514        let _tx = create_mock_solana_transaction();
1515
1516        // Create a valid transaction
1517        let signer_pubkey = Pubkey::from_str("11111111111111111111111111111112").unwrap();
1518        let recipient = Pubkey::new_unique();
1519        let message = Message::new(
1520            &[solana_system_interface::instruction::transfer(
1521                &signer_pubkey,
1522                &recipient,
1523                1000,
1524            )],
1525            Some(&signer_pubkey),
1526        );
1527        let transaction = solana_sdk::transaction::Transaction::new_unsigned(message);
1528
1529        // Mock all validation calls
1530        provider
1531            .expect_calculate_total_fee()
1532            .returning(|_| Box::pin(async { Ok(5000) }));
1533        provider
1534            .expect_get_balance()
1535            .returning(|_| Box::pin(async { Ok(1000000) }));
1536        provider.expect_get_transaction_status().returning(|_| {
1537            Box::pin(async { Ok(crate::models::SolanaTransactionStatus::Processed) })
1538        });
1539        provider
1540            .expect_is_blockhash_valid()
1541            .returning(|_, _| Box::pin(async { Ok(true) }));
1542        provider.expect_simulate_transaction().returning(|_| {
1543            Box::pin(async {
1544                Ok(solana_client::rpc_response::RpcSimulateTransactionResult {
1545                    err: None,
1546                    logs: Some(vec![]),
1547                    accounts: None,
1548                    units_consumed: Some(0),
1549                    return_data: None,
1550                    fee: Some(0),
1551                    inner_instructions: None,
1552                    loaded_accounts_data_size: Some(0),
1553                    replacement_blockhash: None,
1554                    pre_balances: Some(vec![]),
1555                    post_balances: Some(vec![]),
1556                    pre_token_balances: None,
1557                    post_token_balances: None,
1558                    loaded_addresses: None,
1559                })
1560            })
1561        });
1562
1563        let handler = SolanaRelayerTransaction {
1564            relayer,
1565            relayer_repository: relayer_repo,
1566            provider: Arc::new(provider),
1567            transaction_repository: tx_repo,
1568            job_producer,
1569            signer: Arc::new(signer),
1570        };
1571
1572        let result = handler.validate_transaction_impl(&transaction).await;
1573        assert!(result.is_ok());
1574    }
1575
1576    #[tokio::test]
1577    async fn test_cancel_transaction_not_supported() {
1578        let provider = MockSolanaProviderTrait::new();
1579        let relayer_repo = Arc::new(MockRelayerRepository::new());
1580        let tx_repo = Arc::new(MockTransactionRepository::new());
1581        let job_producer = Arc::new(MockJobProducerTrait::new());
1582        let signer = MockSolanaSignTrait::new();
1583
1584        let relayer = create_mock_solana_relayer("test-relayer".to_string(), false);
1585        let tx = create_mock_solana_transaction();
1586
1587        let handler = SolanaRelayerTransaction {
1588            relayer,
1589            relayer_repository: relayer_repo,
1590            provider: Arc::new(provider),
1591            transaction_repository: tx_repo,
1592            job_producer,
1593            signer: Arc::new(signer),
1594        };
1595
1596        let result = handler.cancel_transaction(tx).await;
1597        assert!(result.is_err());
1598        let error = result.unwrap_err();
1599        match error {
1600            TransactionError::NotSupported(msg) => {
1601                assert!(msg.contains("Transaction cancellation is not supported for Solana"));
1602            }
1603            _ => panic!("Expected NotSupported error"),
1604        }
1605    }
1606
1607    #[tokio::test]
1608    async fn test_replace_transaction_not_supported() {
1609        let provider = MockSolanaProviderTrait::new();
1610        let relayer_repo = Arc::new(MockRelayerRepository::new());
1611        let tx_repo = Arc::new(MockTransactionRepository::new());
1612        let job_producer = Arc::new(MockJobProducerTrait::new());
1613        let signer = MockSolanaSignTrait::new();
1614
1615        let relayer = create_mock_solana_relayer("test-relayer".to_string(), false);
1616        let old_tx = create_mock_solana_transaction();
1617        let new_request = crate::models::NetworkTransactionRequest::Evm(
1618            crate::models::EvmTransactionRequest::default(),
1619        );
1620
1621        let handler = SolanaRelayerTransaction {
1622            relayer,
1623            relayer_repository: relayer_repo,
1624            provider: Arc::new(provider),
1625            transaction_repository: tx_repo,
1626            job_producer,
1627            signer: Arc::new(signer),
1628        };
1629
1630        let result = handler.replace_transaction(old_tx, new_request).await;
1631        assert!(result.is_err());
1632        let error = result.unwrap_err();
1633        match error {
1634            TransactionError::NotSupported(msg) => {
1635                assert!(msg.contains("Transaction replacement is not supported for Solana"));
1636            }
1637            _ => panic!("Expected NotSupported error"),
1638        }
1639    }
1640
1641    #[tokio::test]
1642    async fn test_sign_transaction_not_supported() {
1643        let provider = MockSolanaProviderTrait::new();
1644        let relayer_repo = Arc::new(MockRelayerRepository::new());
1645        let tx_repo = Arc::new(MockTransactionRepository::new());
1646        let job_producer = Arc::new(MockJobProducerTrait::new());
1647        let signer = MockSolanaSignTrait::new();
1648
1649        let relayer = create_mock_solana_relayer("test-relayer".to_string(), false);
1650        let tx = create_mock_solana_transaction();
1651
1652        let handler = SolanaRelayerTransaction {
1653            relayer,
1654            relayer_repository: relayer_repo,
1655            provider: Arc::new(provider),
1656            transaction_repository: tx_repo,
1657            job_producer,
1658            signer: Arc::new(signer),
1659        };
1660
1661        let result = handler.sign_transaction(tx).await;
1662        assert!(result.is_err());
1663        let error = result.unwrap_err();
1664        match error {
1665            TransactionError::NotSupported(msg) => {
1666                assert!(msg.contains("Standalone transaction signing is not supported for Solana"));
1667            }
1668            _ => panic!("Expected NotSupported error"),
1669        }
1670    }
1671
1672    #[tokio::test]
1673    async fn test_handle_transaction_status_calls_impl() {
1674        // Create test data
1675        let relayer = create_mock_solana_relayer("test-solana-relayer".to_string(), false);
1676        let relayer_repository = Arc::new(MockRelayerRepository::new());
1677        let provider = MockSolanaProviderTrait::new();
1678        let transaction_repository = Arc::new(MockTransactionRepository::new());
1679        let mut job_producer = MockJobProducerTrait::new();
1680        let signer = MockSolanaSignTrait::new();
1681
1682        // Create test transaction (will be in Pending status by default)
1683        let test_tx = create_mock_solana_transaction();
1684
1685        job_producer
1686            .expect_produce_transaction_request_job()
1687            .returning(|_, _| Box::pin(async { Ok(()) }));
1688
1689        // Create transaction handler
1690        let transaction_handler = SolanaRelayerTransaction {
1691            relayer,
1692            relayer_repository,
1693            provider: Arc::new(provider),
1694            transaction_repository,
1695            job_producer: Arc::new(job_producer),
1696            signer: Arc::new(signer),
1697        };
1698
1699        // Call handle_transaction_status - with new implementation,
1700        // Pending transactions just return Ok without querying provider
1701        let result = transaction_handler
1702            .handle_transaction_status(test_tx.clone())
1703            .await;
1704
1705        // Verify the result is Ok and transaction is unchanged
1706        assert!(result.is_ok());
1707        let returned_tx = result.unwrap();
1708        assert_eq!(returned_tx.id, test_tx.id);
1709        assert_eq!(returned_tx.status, test_tx.status);
1710    }
1711}