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