openzeppelin_relayer/domain/relayer/solana/
solana_relayer.rs

1//! # Solana Relayer Module
2//!
3//! This module implements a relayer for the Solana network. It defines a trait
4//! `SolanaRelayerTrait` for common operations such as sending JSON RPC requests,
5//! fetching balance information, signing transactions, etc. The module uses a
6//! SolanaProvider for making RPC calls.
7//!
8//! It integrates with other parts of the system including the job queue ([`JobProducer`]),
9//! in-memory repositories, and the application's domain models.
10use std::{str::FromStr, sync::Arc};
11
12use crate::constants::SOLANA_STATUS_CHECK_INITIAL_DELAY_SECONDS;
13use crate::domain::relayer::solana::rpc::SolanaRpcMethods;
14use crate::domain::{
15    create_error_response, GasAbstractionTrait, Relayer, SignDataRequest,
16    SignTransactionExternalResponse, SignTransactionRequest, SignTransactionResponse,
17    SignTransactionResponseSolana, SignTypedDataRequest, SolanaRpcHandlerType, SwapParams,
18};
19use crate::jobs::{TransactionRequest, TransactionStatusCheck};
20use crate::models::transaction::request::{
21    SponsoredTransactionBuildRequest, SponsoredTransactionQuoteRequest,
22};
23use crate::models::{
24    DeletePendingTransactionsResponse, JsonRpcRequest, JsonRpcResponse, NetworkRpcRequest,
25    NetworkRpcResult, NetworkTransactionRequest, RelayerStatus, RepositoryError, RpcErrorCodes,
26    SolanaRpcRequest, SolanaRpcResult, SolanaSignAndSendTransactionRequestParams,
27    SolanaSignTransactionRequestParams, SponsoredTransactionBuildResponse,
28    SponsoredTransactionQuoteResponse,
29};
30use crate::utils::calculate_scheduled_timestamp;
31use crate::{
32    constants::{
33        transactions::PENDING_TRANSACTION_STATUSES, DEFAULT_CONVERSION_SLIPPAGE_PERCENTAGE,
34        DEFAULT_SOLANA_MIN_BALANCE, SOLANA_SMALLEST_UNIT_NAME, WRAPPED_SOL_MINT,
35    },
36    domain::{relayer::RelayerError, BalanceResponse, DexStrategy, SolanaRelayerDexTrait},
37    jobs::{JobProducerTrait, RelayerHealthCheck, TokenSwapRequest},
38    models::{
39        produce_relayer_disabled_payload, produce_solana_dex_webhook_payload, DisabledReason,
40        HealthCheckFailure, NetworkRepoModel, NetworkTransactionData, NetworkType, PaginationQuery,
41        RelayerNetworkPolicy, RelayerRepoModel, RelayerSolanaPolicy, SolanaAllowedTokensPolicy,
42        SolanaDexPayload, SolanaFeePaymentStrategy, SolanaNetwork, SolanaTransactionData,
43        TransactionRepoModel, TransactionStatus, TransactionUpdateRequest,
44    },
45    repositories::{NetworkRepository, RelayerRepository, Repository, TransactionRepository},
46    services::{
47        provider::{SolanaProvider, SolanaProviderTrait},
48        signer::{Signer, SolanaSignTrait, SolanaSigner},
49        JupiterService, JupiterServiceTrait,
50    },
51};
52
53use async_trait::async_trait;
54use eyre::Result;
55use futures::future::try_join_all;
56use solana_sdk::{account::Account, pubkey::Pubkey};
57use tracing::{debug, error, info, instrument, warn};
58
59use super::{NetworkDex, SolanaRpcError, SolanaTokenProgram, SwapResult, TokenAccount};
60
61#[allow(dead_code)]
62struct TokenSwapCandidate<'a> {
63    policy: &'a SolanaAllowedTokensPolicy,
64    account: TokenAccount,
65    swap_amount: u64,
66}
67
68#[allow(dead_code)]
69pub struct SolanaRelayer<RR, TR, J, S, JS, SP, NR>
70where
71    RR: Repository<RelayerRepoModel, String> + RelayerRepository + Send + Sync + 'static,
72    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
73    J: JobProducerTrait + Send + Sync + 'static,
74    S: SolanaSignTrait + Signer + Send + Sync + 'static,
75    JS: JupiterServiceTrait + Send + Sync + 'static,
76    SP: SolanaProviderTrait + Send + Sync + 'static,
77    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
78{
79    relayer: RelayerRepoModel,
80    signer: Arc<S>,
81    network: SolanaNetwork,
82    provider: Arc<SP>,
83    rpc_handler: SolanaRpcHandlerType<SP, S, JS, J, TR>,
84    relayer_repository: Arc<RR>,
85    transaction_repository: Arc<TR>,
86    job_producer: Arc<J>,
87    dex_service: Arc<NetworkDex<SP, S, JS>>,
88    network_repository: Arc<NR>,
89}
90
91pub type DefaultSolanaRelayer<J, TR, RR, NR> =
92    SolanaRelayer<RR, TR, J, SolanaSigner, JupiterService, SolanaProvider, NR>;
93
94impl<RR, TR, J, S, JS, SP, NR> SolanaRelayer<RR, TR, J, S, JS, SP, NR>
95where
96    RR: Repository<RelayerRepoModel, String> + RelayerRepository + Send + Sync + 'static,
97    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
98    J: JobProducerTrait + Send + Sync + 'static,
99    S: SolanaSignTrait + Signer + Send + Sync + 'static,
100    JS: JupiterServiceTrait + Send + Sync + 'static,
101    SP: SolanaProviderTrait + Send + Sync + 'static,
102    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
103{
104    #[allow(clippy::too_many_arguments)]
105    pub async fn new(
106        relayer: RelayerRepoModel,
107        signer: Arc<S>,
108        relayer_repository: Arc<RR>,
109        network_repository: Arc<NR>,
110        provider: Arc<SP>,
111        rpc_handler: SolanaRpcHandlerType<SP, S, JS, J, TR>,
112        transaction_repository: Arc<TR>,
113        job_producer: Arc<J>,
114        dex_service: Arc<NetworkDex<SP, S, JS>>,
115    ) -> Result<Self, RelayerError> {
116        let network_repo = network_repository
117            .get_by_name(NetworkType::Solana, &relayer.network)
118            .await
119            .ok()
120            .flatten()
121            .ok_or_else(|| {
122                RelayerError::NetworkConfiguration(format!("Network {} not found", relayer.network))
123            })?;
124
125        let network = SolanaNetwork::try_from(network_repo)?;
126
127        Ok(Self {
128            relayer,
129            signer,
130            network,
131            provider,
132            rpc_handler,
133            relayer_repository,
134            transaction_repository,
135            job_producer,
136            dex_service,
137            network_repository,
138        })
139    }
140
141    /// Validates the RPC connection by fetching the latest blockhash.
142    ///
143    /// This method sends a request to the Solana RPC to obtain the latest blockhash.
144    /// If the call fails, it returns a `RelayerError::ProviderError` containing the error message.
145    #[instrument(
146        level = "debug",
147        skip(self),
148        fields(
149            request_id = ?crate::observability::request_id::get_request_id(),
150            relayer_id = %self.relayer.id,
151        )
152    )]
153    async fn validate_rpc(&self) -> Result<(), RelayerError> {
154        self.provider
155            .get_latest_blockhash()
156            .await
157            .map_err(|e| RelayerError::ProviderError(e.to_string()))?;
158
159        Ok(())
160    }
161
162    /// Populates the allowed tokens metadata for the Solana relayer policy.
163    ///
164    /// This method checks whether allowed tokens have been configured in the relayer's policy.
165    /// If allowed tokens are provided, it concurrently fetches token metadata from the Solana
166    /// provider for each token using its mint address, maps the metadata into instances of
167    /// `SolanaAllowedTokensPolicy`, and then updates the relayer policy with the new metadata.
168    ///
169    /// If no allowed tokens are specified, it logs an informational message and returns the policy
170    /// unchanged.
171    ///
172    /// Finally, the updated policy is stored in the repository.
173    #[instrument(
174        level = "debug",
175        skip(self),
176        fields(
177            request_id = ?crate::observability::request_id::get_request_id(),
178            relayer_id = %self.relayer.id,
179        )
180    )]
181    async fn populate_allowed_tokens_metadata(&self) -> Result<RelayerSolanaPolicy, RelayerError> {
182        let mut policy = self.relayer.policies.get_solana_policy();
183        // Check if allowed_tokens is specified; if not, return the policy unchanged.
184        let allowed_tokens = match policy.allowed_tokens.as_ref() {
185            Some(tokens) if !tokens.is_empty() => tokens,
186            _ => {
187                info!("No allowed tokens specified; skipping token metadata population.");
188                return Ok(policy);
189            }
190        };
191
192        let token_metadata_futures = allowed_tokens.iter().map(|token| async {
193            // Propagate errors from get_token_metadata_from_pubkey instead of panicking.
194            let token_metadata = self
195                .provider
196                .get_token_metadata_from_pubkey(&token.mint)
197                .await
198                .map_err(|e| RelayerError::ProviderError(e.to_string()))?;
199            Ok::<SolanaAllowedTokensPolicy, RelayerError>(SolanaAllowedTokensPolicy {
200                mint: token_metadata.mint,
201                decimals: Some(token_metadata.decimals as u8),
202                symbol: Some(token_metadata.symbol.to_string()),
203                max_allowed_fee: token.max_allowed_fee,
204                swap_config: token.swap_config.clone(),
205            })
206        });
207
208        let updated_allowed_tokens = try_join_all(token_metadata_futures).await?;
209
210        policy.allowed_tokens = Some(updated_allowed_tokens);
211
212        self.relayer_repository
213            .update_policy(
214                self.relayer.id.clone(),
215                RelayerNetworkPolicy::Solana(policy.clone()),
216            )
217            .await?;
218
219        Ok(policy)
220    }
221
222    /// Validates the allowed programs policy.
223    ///
224    /// This method retrieves the allowed programs specified in the Solana relayer policy.
225    /// For each allowed program, it fetches the associated account data from the provider and
226    /// verifies that the program is executable.
227    /// If any of the programs are not executable, it returns a
228    /// `RelayerError::PolicyConfigurationError`.
229    #[instrument(
230        level = "debug",
231        skip(self),
232        fields(
233            request_id = ?crate::observability::request_id::get_request_id(),
234            relayer_id = %self.relayer.id,
235        )
236    )]
237    async fn validate_program_policy(&self) -> Result<(), RelayerError> {
238        let policy = self.relayer.policies.get_solana_policy();
239        let allowed_programs = match policy.allowed_programs.as_ref() {
240            Some(programs) if !programs.is_empty() => programs,
241            _ => {
242                info!("No allowed programs specified; skipping program validation.");
243                return Ok(());
244            }
245        };
246        let account_info_futures = allowed_programs.iter().map(|program| {
247            let program = program.clone();
248            async move {
249                let account = self
250                    .provider
251                    .get_account_from_str(&program)
252                    .await
253                    .map_err(|e| RelayerError::ProviderError(e.to_string()))?;
254                Ok::<Account, RelayerError>(account)
255            }
256        });
257
258        let accounts = try_join_all(account_info_futures).await?;
259
260        for account in accounts {
261            if !account.executable {
262                return Err(RelayerError::PolicyConfigurationError(
263                    "Policy Program is not executable".to_string(),
264                ));
265            }
266        }
267
268        Ok(())
269    }
270
271    /// Checks the relayer's balance and triggers a token swap if the balance is below the
272    /// specified threshold.
273    #[instrument(
274        level = "debug",
275        skip(self),
276        fields(
277            request_id = ?crate::observability::request_id::get_request_id(),
278            relayer_id = %self.relayer.id,
279        )
280    )]
281    async fn check_balance_and_trigger_token_swap_if_needed(&self) -> Result<(), RelayerError> {
282        let policy = self.relayer.policies.get_solana_policy();
283        let swap_config = match policy.get_swap_config() {
284            Some(config) => config,
285            None => {
286                info!("No swap configuration specified; skipping validation.");
287                return Ok(());
288            }
289        };
290        let swap_min_balance_threshold = match swap_config.min_balance_threshold {
291            Some(threshold) => threshold,
292            None => {
293                info!("No swap min balance threshold specified; skipping validation.");
294                return Ok(());
295            }
296        };
297
298        let balance = self
299            .provider
300            .get_balance(&self.relayer.address)
301            .await
302            .map_err(|e| RelayerError::ProviderError(e.to_string()))?;
303
304        if balance < swap_min_balance_threshold {
305            info!(
306                "Sending job request for for relayer  {} swapping tokens due to relayer swap_min_balance_threshold: Balance: {}, swap_min_balance_threshold: {}",
307                self.relayer.id, balance, swap_min_balance_threshold
308            );
309
310            self.job_producer
311                .produce_token_swap_request_job(
312                    TokenSwapRequest {
313                        relayer_id: self.relayer.id.clone(),
314                    },
315                    None,
316                )
317                .await?;
318        }
319
320        Ok(())
321    }
322
323    // Helper function to calculate swap amount
324    fn calculate_swap_amount(
325        &self,
326        current_balance: u64,
327        min_amount: Option<u64>,
328        max_amount: Option<u64>,
329        retain_min: Option<u64>,
330    ) -> Result<u64, RelayerError> {
331        // Cap the swap amount at the maximum if specified
332        let mut amount = max_amount
333            .map(|max| std::cmp::min(current_balance, max))
334            .unwrap_or(current_balance);
335
336        // Adjust for retain minimum if specified
337        if let Some(retain) = retain_min {
338            if current_balance > retain {
339                amount = std::cmp::min(amount, current_balance - retain);
340            } else {
341                // Not enough to retain the minimum after swap
342                return Ok(0);
343            }
344        }
345
346        // Check if we have enough tokens to meet minimum swap requirement
347        if let Some(min) = min_amount {
348            if amount < min {
349                return Ok(0); // Not enough tokens to swap
350            }
351        }
352
353        Ok(amount)
354    }
355}
356
357#[async_trait]
358impl<RR, TR, J, S, JS, SP, NR> SolanaRelayerDexTrait for SolanaRelayer<RR, TR, J, S, JS, SP, NR>
359where
360    RR: Repository<RelayerRepoModel, String> + RelayerRepository + Send + Sync + 'static,
361    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
362    J: JobProducerTrait + Send + Sync + 'static,
363    S: SolanaSignTrait + Signer + Send + Sync + 'static,
364    JS: JupiterServiceTrait + Send + Sync + 'static,
365    SP: SolanaProviderTrait + Send + Sync + 'static,
366    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
367{
368    /// Processes a token‐swap request for the given relayer ID:
369    ///
370    /// 1. Loads the relayer's on‐chain policy (must include swap_config & strategy).
371    /// 2. Iterates allowed tokens, fetching each SPL token account and calculating how much
372    ///    to swap based on min, max, and retain settings.
373    /// 3. Executes each swap through the DEX service (e.g. Jupiter).
374    /// 4. Collects and returns all `SwapResult`s (empty if no swaps were needed).
375    ///
376    /// Returns a `RelayerError` on any repository, provider, or swap execution failure.
377    #[instrument(
378        level = "debug",
379        skip(self),
380        fields(
381            request_id = ?crate::observability::request_id::get_request_id(),
382            relayer_id = %self.relayer.id,
383        )
384    )]
385    async fn handle_token_swap_request(
386        &self,
387        relayer_id: String,
388    ) -> Result<Vec<SwapResult>, RelayerError> {
389        debug!("handling token swap request for relayer {}", relayer_id);
390        let relayer = self
391            .relayer_repository
392            .get_by_id(relayer_id.clone())
393            .await?;
394
395        let policy = relayer.policies.get_solana_policy();
396
397        let swap_config = match policy.get_swap_config() {
398            Some(config) => config,
399            None => {
400                debug!(%relayer_id, "No swap configuration specified for relayer; Exiting.");
401                return Ok(vec![]);
402            }
403        };
404
405        match swap_config.strategy {
406            Some(strategy) => strategy,
407            None => {
408                debug!(%relayer_id, "No swap strategy specified for relayer; Exiting.");
409                return Ok(vec![]);
410            }
411        };
412
413        let relayer_pubkey = Pubkey::from_str(&relayer.address)
414            .map_err(|e| RelayerError::ProviderError(format!("Invalid relayer address: {e}")))?;
415
416        let tokens_to_swap = {
417            let mut eligible_tokens = Vec::<TokenSwapCandidate>::new();
418
419            if let Some(allowed_tokens) = policy.allowed_tokens.as_ref() {
420                for token in allowed_tokens {
421                    let token_mint = Pubkey::from_str(&token.mint).map_err(|e| {
422                        RelayerError::ProviderError(format!("Invalid token mint: {e}"))
423                    })?;
424                    let token_account = SolanaTokenProgram::get_and_unpack_token_account(
425                        &*self.provider,
426                        &relayer_pubkey,
427                        &token_mint,
428                    )
429                    .await
430                    .map_err(|e| {
431                        RelayerError::ProviderError(format!("Failed to get token account: {e}"))
432                    })?;
433
434                    let swap_amount = self
435                        .calculate_swap_amount(
436                            token_account.amount,
437                            token
438                                .swap_config
439                                .as_ref()
440                                .and_then(|config| config.min_amount),
441                            token
442                                .swap_config
443                                .as_ref()
444                                .and_then(|config| config.max_amount),
445                            token
446                                .swap_config
447                                .as_ref()
448                                .and_then(|config| config.retain_min_amount),
449                        )
450                        .unwrap_or(0);
451
452                    if swap_amount > 0 {
453                        debug!(%relayer_id, token = ?token, "token swap eligible for token");
454
455                        // Add the token to the list of eligible tokens for swapping
456                        eligible_tokens.push(TokenSwapCandidate {
457                            policy: token,
458                            account: token_account,
459                            swap_amount,
460                        });
461                    }
462                }
463            }
464
465            eligible_tokens
466        };
467
468        // Execute swap for every eligible token
469        let swap_futures = tokens_to_swap.iter().map(|candidate| {
470            let token = candidate.policy;
471            let swap_amount = candidate.swap_amount;
472            let dex = &self.dex_service;
473            let relayer_address = self.relayer.address.clone();
474            let token_mint = token.mint.clone();
475            let relayer_id_clone = relayer_id.clone();
476            let slippage_percent = token
477                .swap_config
478                .as_ref()
479                .and_then(|config| config.slippage_percentage)
480                .unwrap_or(DEFAULT_CONVERSION_SLIPPAGE_PERCENTAGE)
481                as f64;
482
483            async move {
484                info!(
485                    "Swapping {} tokens of type {} for relayer: {}",
486                    swap_amount, token_mint, relayer_id_clone
487                );
488
489                let swap_result = dex
490                    .execute_swap(SwapParams {
491                        owner_address: relayer_address,
492                        source_mint: token_mint.clone(),
493                        destination_mint: WRAPPED_SOL_MINT.to_string(), // SOL mint
494                        amount: swap_amount,
495                        slippage_percent,
496                    })
497                    .await;
498
499                match swap_result {
500                    Ok(swap_result) => {
501                        info!(
502                            "Swap successful for relayer: {}. Amount: {}, Destination amount: {}",
503                            relayer_id_clone, swap_amount, swap_result.destination_amount
504                        );
505                        Ok::<SwapResult, RelayerError>(swap_result)
506                    }
507                    Err(e) => {
508                        error!(
509                            "Error during token swap for relayer: {}. Error: {}",
510                            relayer_id_clone, e
511                        );
512                        Ok::<SwapResult, RelayerError>(SwapResult {
513                            mint: token_mint.clone(),
514                            source_amount: swap_amount,
515                            destination_amount: 0,
516                            transaction_signature: "".to_string(),
517                            error: Some(e.to_string()),
518                        })
519                    }
520                }
521            }
522        });
523
524        let swap_results = try_join_all(swap_futures).await?;
525
526        if !swap_results.is_empty() {
527            let total_sol_received: u64 = swap_results
528                .iter()
529                .map(|result| result.destination_amount)
530                .sum();
531
532            info!(
533                "Completed {} token swaps for relayer {}, total SOL received: {}",
534                swap_results.len(),
535                relayer_id,
536                total_sol_received
537            );
538
539            if let Some(notification_id) = &self.relayer.notification_id {
540                let webhook_result = self
541                    .job_producer
542                    .produce_send_notification_job(
543                        produce_solana_dex_webhook_payload(
544                            notification_id,
545                            "solana_dex".to_string(),
546                            SolanaDexPayload {
547                                swap_results: swap_results.clone(),
548                            },
549                        ),
550                        None,
551                    )
552                    .await;
553
554                if let Err(e) = webhook_result {
555                    error!(error = %e, "failed to produce notification job");
556                }
557            }
558        }
559
560        Ok(swap_results)
561    }
562}
563
564#[async_trait]
565impl<RR, TR, J, S, JS, SP, NR> Relayer for SolanaRelayer<RR, TR, J, S, JS, SP, NR>
566where
567    RR: Repository<RelayerRepoModel, String> + RelayerRepository + Send + Sync + 'static,
568    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
569    J: JobProducerTrait + Send + Sync + 'static,
570    S: SolanaSignTrait + Signer + Send + Sync + 'static,
571    JS: JupiterServiceTrait + Send + Sync + 'static,
572    SP: SolanaProviderTrait + Send + Sync + 'static,
573    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
574{
575    #[instrument(
576        level = "debug",
577        skip(self, network_transaction),
578        fields(
579            request_id = ?crate::observability::request_id::get_request_id(),
580            relayer_id = %self.relayer.id,
581            network_type = ?self.relayer.network_type,
582        )
583    )]
584    async fn process_transaction_request(
585        &self,
586        network_transaction: crate::models::NetworkTransactionRequest,
587    ) -> Result<TransactionRepoModel, RelayerError> {
588        let policy = self.relayer.policies.get_solana_policy();
589        let user_pays_fee = matches!(
590            policy.fee_payment_strategy.unwrap_or_default(),
591            SolanaFeePaymentStrategy::User
592        );
593
594        // For user-paid fees, delegate to RPC handler (similar to build/quote)
595        if user_pays_fee {
596            let solana_request = match &network_transaction {
597                NetworkTransactionRequest::Solana(req) => req,
598                _ => {
599                    return Err(RelayerError::ValidationError(
600                        "Expected Solana transaction request".to_string(),
601                    ));
602                }
603            };
604
605            // For user-paid fees, we need a pre-built transaction (not instructions)
606            let transaction = solana_request.transaction.as_ref().ok_or_else(|| {
607                RelayerError::ValidationError(
608                    "User-paid fees require a pre-built transaction. Use prepareTransaction RPC method first to build the transaction from instructions.".to_string(),
609                )
610            })?;
611
612            let params = SolanaSignAndSendTransactionRequestParams {
613                transaction: transaction.clone(),
614            };
615
616            let result = self
617                .rpc_handler
618                .rpc_methods()
619                .sign_and_send_transaction(params)
620                .await
621                .map_err(|e| RelayerError::Internal(e.to_string()))?;
622
623            // Fetch the transaction from repository using the ID returned by sign_and_send_transaction
624            let transaction = self
625                .transaction_repository
626                .get_by_id(result.id.clone())
627                .await
628                .map_err(|e| {
629                    RelayerError::Internal(format!(
630                        "Failed to fetch transaction after sign and send: {e}"
631                    ))
632                })?;
633
634            Ok(transaction)
635        } else {
636            // Relayer-paid fees: use the original flow
637            let network_model = self
638                .network_repository
639                .get_by_name(NetworkType::Solana, &self.relayer.network)
640                .await?
641                .ok_or_else(|| {
642                    RelayerError::NetworkConfiguration(format!(
643                        "Network {} not found",
644                        self.relayer.network
645                    ))
646                })?;
647
648            let transaction = TransactionRepoModel::try_from((
649                &network_transaction,
650                &self.relayer,
651                &network_model,
652            ))?;
653
654            self.transaction_repository
655                .create(transaction.clone())
656                .await
657                .map_err(|e| RepositoryError::TransactionFailure(e.to_string()))?;
658
659            // Status check FIRST - this is our safety net for monitoring.
660            // If this fails, mark transaction as failed and don't proceed.
661            // This ensures we never have an unmonitored transaction.
662            if let Err(e) = self
663                .job_producer
664                .produce_check_transaction_status_job(
665                    TransactionStatusCheck::new(
666                        transaction.id.clone(),
667                        transaction.relayer_id.clone(),
668                        NetworkType::Solana,
669                    ),
670                    Some(calculate_scheduled_timestamp(
671                        SOLANA_STATUS_CHECK_INITIAL_DELAY_SECONDS,
672                    )),
673                )
674                .await
675            {
676                // Status queue failed - mark transaction as failed to prevent orphaned tx
677                error!(
678                    relayer_id = %self.relayer.id,
679                    transaction_id = %transaction.id,
680                    error = %e,
681                    "Status check queue push failed - marking transaction as failed"
682                );
683                if let Err(update_err) = self
684                    .transaction_repository
685                    .partial_update(
686                        transaction.id.clone(),
687                        TransactionUpdateRequest {
688                            status: Some(TransactionStatus::Failed),
689                            status_reason: Some("Queue unavailable".to_string()),
690                            ..Default::default()
691                        },
692                    )
693                    .await
694                {
695                    warn!(
696                        relayer_id = %self.relayer.id,
697                        transaction_id = %transaction.id,
698                        error = %update_err,
699                        "Failed to mark transaction as failed after queue push failure"
700                    );
701                }
702                return Err(e.into());
703            }
704
705            // Now safe to push transaction request.
706            // Even if this fails, status check will monitor and detect the stuck transaction.
707            self.job_producer
708                .produce_transaction_request_job(
709                    TransactionRequest::new(transaction.id.clone(), transaction.relayer_id.clone()),
710                    None,
711                )
712                .await?;
713
714            Ok(transaction)
715        }
716    }
717
718    #[instrument(
719        level = "debug",
720        skip(self),
721        fields(
722            request_id = ?crate::observability::request_id::get_request_id(),
723            relayer_id = %self.relayer.id,
724        )
725    )]
726    async fn get_balance(&self) -> Result<BalanceResponse, RelayerError> {
727        let address = &self.relayer.address;
728        let balance = self.provider.get_balance(address).await?;
729
730        Ok(BalanceResponse {
731            balance: balance as u128,
732            unit: SOLANA_SMALLEST_UNIT_NAME.to_string(),
733        })
734    }
735
736    #[instrument(
737        level = "debug",
738        skip(self),
739        fields(
740            request_id = ?crate::observability::request_id::get_request_id(),
741            relayer_id = %self.relayer.id,
742        )
743    )]
744    async fn delete_pending_transactions(
745        &self,
746    ) -> Result<DeletePendingTransactionsResponse, RelayerError> {
747        Err(RelayerError::NotSupported(
748            "Delete pending transactions not supported for Solana relayers".to_string(),
749        ))
750    }
751
752    #[instrument(
753        level = "debug",
754        skip(self, _request),
755        fields(
756            request_id = ?crate::observability::request_id::get_request_id(),
757            relayer_id = %self.relayer.id,
758        )
759    )]
760    async fn sign_data(
761        &self,
762        _request: SignDataRequest,
763    ) -> Result<crate::domain::relayer::SignDataResponse, RelayerError> {
764        Err(RelayerError::NotSupported(
765            "Sign data not supported for Solana relayers".to_string(),
766        ))
767    }
768
769    #[instrument(
770        level = "debug",
771        skip(self, _request),
772        fields(
773            request_id = ?crate::observability::request_id::get_request_id(),
774            relayer_id = %self.relayer.id,
775        )
776    )]
777    async fn sign_typed_data(
778        &self,
779        _request: SignTypedDataRequest,
780    ) -> Result<crate::domain::relayer::SignDataResponse, RelayerError> {
781        Err(RelayerError::NotSupported(
782            "Sign typed data not supported for Solana relayers".to_string(),
783        ))
784    }
785
786    #[instrument(
787        level = "debug",
788        skip(self, request),
789        fields(
790            request_id = ?crate::observability::request_id::get_request_id(),
791            relayer_id = %self.relayer.id,
792        )
793    )]
794    async fn sign_transaction(
795        &self,
796        request: &SignTransactionRequest,
797    ) -> Result<SignTransactionExternalResponse, RelayerError> {
798        let policy = self.relayer.policies.get_solana_policy();
799        let user_pays_fee = matches!(
800            policy.fee_payment_strategy.unwrap_or_default(),
801            SolanaFeePaymentStrategy::User
802        );
803
804        // For user-paid fees, delegate to RPC handler (similar to process_transaction_request)
805        if user_pays_fee {
806            let solana_request = match request {
807                SignTransactionRequest::Solana(req) => req,
808                _ => {
809                    error!(
810                        id = %self.relayer.id,
811                        "Invalid request type for Solana relayer",
812                    );
813                    return Err(RelayerError::NotSupported(
814                        "Invalid request type for Solana relayer".to_string(),
815                    ));
816                }
817            };
818
819            let params = SolanaSignTransactionRequestParams {
820                transaction: solana_request.transaction.clone(),
821            };
822
823            let result = self
824                .rpc_handler
825                .rpc_methods()
826                .sign_transaction(params)
827                .await
828                .map_err(|e| RelayerError::Internal(e.to_string()))?;
829
830            Ok(SignTransactionExternalResponse::Solana(
831                SignTransactionResponseSolana {
832                    transaction: result.transaction,
833                    signature: result.signature,
834                },
835            ))
836        } else {
837            // Relayer-paid fees: use the original flow
838            let transaction_bytes = match request {
839                SignTransactionRequest::Solana(req) => &req.transaction,
840                _ => {
841                    error!(
842                        id = %self.relayer.id,
843                        "Invalid request type for Solana relayer",
844                    );
845                    return Err(RelayerError::NotSupported(
846                        "Invalid request type for Solana relayer".to_string(),
847                    ));
848                }
849            };
850
851            // Prepare transaction data for signing
852            let transaction_data = NetworkTransactionData::Solana(SolanaTransactionData {
853                transaction: Some(transaction_bytes.clone().into_inner()),
854                ..Default::default()
855            });
856
857            // Sign the transaction using the signer trait
858            let response = self
859                .signer
860                .sign_transaction(transaction_data)
861                .await
862                .map_err(|e| {
863                    error!(
864                        %e,
865                        id = %self.relayer.id,
866                        "Failed to sign transaction",
867                    );
868                    RelayerError::SignerError(e)
869                })?;
870
871            // Extract Solana-specific response
872            let solana_response = match response {
873                SignTransactionResponse::Solana(resp) => resp,
874                _ => {
875                    return Err(RelayerError::ProviderError(
876                        "Unexpected response type from Solana signer".to_string(),
877                    ))
878                }
879            };
880
881            Ok(SignTransactionExternalResponse::Solana(solana_response))
882        }
883    }
884
885    #[instrument(
886        level = "debug",
887        skip(self, request),
888        fields(
889            request_id = ?crate::observability::request_id::get_request_id(),
890            relayer_id = %self.relayer.id,
891        )
892    )]
893    async fn rpc(
894        &self,
895        request: JsonRpcRequest<NetworkRpcRequest>,
896    ) -> Result<JsonRpcResponse<NetworkRpcResult>, RelayerError> {
897        let JsonRpcRequest {
898            jsonrpc: _,
899            id,
900            params,
901        } = request;
902        let solana_request = match params {
903            NetworkRpcRequest::Solana(sol_req) => sol_req,
904            _ => {
905                return Ok(create_error_response(
906                    id.clone(),
907                    RpcErrorCodes::INVALID_PARAMS,
908                    "Invalid params",
909                    "Expected Solana network request",
910                ))
911            }
912        };
913
914        match solana_request {
915            SolanaRpcRequest::RawRpcRequest { method, params } => {
916                // Handle raw JSON-RPC requests by forwarding to provider
917                let response = self.provider.raw_request_dyn(&method, params).await?;
918
919                Ok(JsonRpcResponse {
920                    jsonrpc: "2.0".to_string(),
921                    result: Some(NetworkRpcResult::Solana(SolanaRpcResult::RawRpc(response))),
922                    error: None,
923                    id: id.clone(),
924                })
925            }
926            _ => {
927                // Handle typed requests using the existing rpc_handler
928                let response = self
929                    .rpc_handler
930                    .handle_request(JsonRpcRequest {
931                        jsonrpc: request.jsonrpc,
932                        params: NetworkRpcRequest::Solana(solana_request),
933                        id: id.clone(),
934                    })
935                    .await;
936
937                match response {
938                    Ok(response) => Ok(response),
939                    Err(e) => {
940                        error!(error = %e, "error while processing RPC request");
941                        let error_response = match e {
942                            SolanaRpcError::UnsupportedMethod(msg) => {
943                                JsonRpcResponse::error(32000, "UNSUPPORTED_METHOD", &msg)
944                            }
945                            SolanaRpcError::FeatureFetch(msg) => JsonRpcResponse::error(
946                                -32008,
947                                "FEATURE_FETCH_ERROR",
948                                &format!("Failed to retrieve the list of enabled features: {msg}"),
949                            ),
950                            SolanaRpcError::InvalidParams(msg) => {
951                                JsonRpcResponse::error(-32602, "INVALID_PARAMS", &msg)
952                            }
953                            SolanaRpcError::UnsupportedFeeToken(msg) => JsonRpcResponse::error(
954                                -32000,
955                                "UNSUPPORTED_FEE_TOKEN",
956                                &format!(
957                                    "The provided fee_token is not supported by the relayer: {msg}"
958                                ),
959                            ),
960                            SolanaRpcError::Estimation(msg) => JsonRpcResponse::error(
961                                -32001,
962                                "ESTIMATION_ERROR",
963                                &format!(
964                                    "Failed to estimate the fee due to internal or network issues: {msg}"
965                                ),
966                            ),
967                            SolanaRpcError::InsufficientFunds(msg) => {
968                                // Trigger a token swap request if the relayer has insufficient funds
969                                self.check_balance_and_trigger_token_swap_if_needed()
970                                    .await?;
971
972                                JsonRpcResponse::error(
973                                    -32002,
974                                    "INSUFFICIENT_FUNDS",
975                                    &format!(
976                                        "The sender does not have enough funds for the transfer: {msg}"
977                                    ),
978                                )
979                            }
980                            SolanaRpcError::TransactionPreparation(msg) => JsonRpcResponse::error(
981                                -32003,
982                                "TRANSACTION_PREPARATION_ERROR",
983                                &format!("Failed to prepare the transfer transaction: {msg}"),
984                            ),
985                            SolanaRpcError::Preparation(msg) => JsonRpcResponse::error(
986                                -32013,
987                                "PREPARATION_ERROR",
988                                &format!("Failed to prepare the transfer transaction: {msg}"),
989                            ),
990                            SolanaRpcError::Signature(msg) => JsonRpcResponse::error(
991                                -32005,
992                                "SIGNATURE_ERROR",
993                                &format!("Failed to sign the transaction: {msg}"),
994                            ),
995                            SolanaRpcError::Signing(msg) => JsonRpcResponse::error(
996                                -32005,
997                                "SIGNATURE_ERROR",
998                                &format!("Failed to sign the transaction: {msg}"),
999                            ),
1000                            SolanaRpcError::TokenFetch(msg) => JsonRpcResponse::error(
1001                                -32007,
1002                                "TOKEN_FETCH_ERROR",
1003                                &format!("Failed to retrieve the list of supported tokens: {msg}"),
1004                            ),
1005                            SolanaRpcError::BadRequest(msg) => JsonRpcResponse::error(
1006                                -32007,
1007                                "BAD_REQUEST",
1008                                &format!("Bad request: {msg}"),
1009                            ),
1010                            SolanaRpcError::Send(msg) => JsonRpcResponse::error(
1011                                -32006,
1012                                "SEND_ERROR",
1013                                &format!(
1014                                    "Failed to submit the transaction to the blockchain: {msg}"
1015                                ),
1016                            ),
1017                            SolanaRpcError::SolanaTransactionValidation(msg) => JsonRpcResponse::error(
1018                                -32013,
1019                                "PREPARATION_ERROR",
1020                                &format!("Failed to prepare the transfer transaction: {msg}"),
1021                            ),
1022                            SolanaRpcError::Encoding(msg) => JsonRpcResponse::error(
1023                                -32601,
1024                                "INVALID_PARAMS",
1025                                &format!("The transaction parameter is invalid or missing: {msg}"),
1026                            ),
1027                            SolanaRpcError::TokenAccount(msg) => JsonRpcResponse::error(
1028                                -32601,
1029                                "PREPARATION_ERROR",
1030                                &format!("Invalid Token Account: {msg}"),
1031                            ),
1032                            SolanaRpcError::Token(msg) => JsonRpcResponse::error(
1033                                -32601,
1034                                "PREPARATION_ERROR",
1035                                &format!("Invalid Token Account: {msg}"),
1036                            ),
1037                            SolanaRpcError::Provider(msg) => JsonRpcResponse::error(
1038                                -32006,
1039                                "PREPARATION_ERROR",
1040                                &format!("Failed to prepare the transfer transaction: {msg}"),
1041                            ),
1042                            SolanaRpcError::Internal(_) => {
1043                                JsonRpcResponse::error(-32000, "INTERNAL_ERROR", "Internal error")
1044                            }
1045                        };
1046                        Ok(error_response)
1047                    }
1048                }
1049            }
1050        }
1051    }
1052
1053    #[instrument(
1054        level = "debug",
1055        skip(self),
1056        fields(
1057            request_id = ?crate::observability::request_id::get_request_id(),
1058            relayer_id = %self.relayer.id,
1059        )
1060    )]
1061    async fn get_status(&self) -> Result<RelayerStatus, RelayerError> {
1062        let address = &self.relayer.address;
1063        let balance = self.provider.get_balance(address).await?;
1064
1065        // Use optimized count_by_status
1066        let pending_transactions_count = self
1067            .transaction_repository
1068            .count_by_status(&self.relayer.id, PENDING_TRANSACTION_STATUSES)
1069            .await
1070            .map_err(RelayerError::from)?;
1071
1072        // Use find_by_status_paginated to get the latest confirmed transaction (newest first)
1073        let last_confirmed_transaction_timestamp = self
1074            .transaction_repository
1075            .find_by_status_paginated(
1076                &self.relayer.id,
1077                &[TransactionStatus::Confirmed],
1078                PaginationQuery {
1079                    page: 1,
1080                    per_page: 1,
1081                },
1082                false, // oldest_first = false means newest first
1083            )
1084            .await
1085            .map_err(RelayerError::from)?
1086            .items
1087            .into_iter()
1088            .next()
1089            .and_then(|tx| tx.confirmed_at);
1090
1091        Ok(RelayerStatus::Solana {
1092            balance: (balance as u128).to_string(),
1093            pending_transactions_count,
1094            last_confirmed_transaction_timestamp,
1095            system_disabled: self.relayer.system_disabled,
1096            paused: self.relayer.paused,
1097        })
1098    }
1099
1100    #[instrument(
1101        level = "debug",
1102        skip(self),
1103        fields(
1104            request_id = ?crate::observability::request_id::get_request_id(),
1105            relayer_id = %self.relayer.id,
1106        )
1107    )]
1108    async fn initialize_relayer(&self) -> Result<(), RelayerError> {
1109        debug!("initializing Solana relayer");
1110
1111        // Populate model with allowed token metadata and update DB entry
1112        // Error will be thrown if any of the tokens are not found
1113        self.populate_allowed_tokens_metadata().await.map_err(|_| {
1114            RelayerError::PolicyConfigurationError(
1115                "Error while processing allowed tokens policy".into(),
1116            )
1117        })?;
1118
1119        // Validate relayer allowed programs policy
1120        // Error will be thrown if any of the programs are not executable
1121        self.validate_program_policy().await.map_err(|_| {
1122            RelayerError::PolicyConfigurationError(
1123                "Error while validating allowed programs policy".into(),
1124            )
1125        })?;
1126
1127        match self.check_health().await {
1128            Ok(_) => {
1129                // All checks passed
1130                if self.relayer.system_disabled {
1131                    // Silently re-enable if was disabled (startup, not recovery)
1132                    self.relayer_repository
1133                        .enable_relayer(self.relayer.id.clone())
1134                        .await?;
1135                }
1136            }
1137            Err(failures) => {
1138                // Health checks failed
1139                let reason = DisabledReason::from_health_failures(failures).unwrap_or_else(|| {
1140                    DisabledReason::RpcValidationFailed("Unknown error".to_string())
1141                });
1142
1143                warn!(reason = %reason, "disabling relayer");
1144                let updated_relayer = self
1145                    .relayer_repository
1146                    .disable_relayer(self.relayer.id.clone(), reason.clone())
1147                    .await?;
1148
1149                // Send notification if configured
1150                if let Some(notification_id) = &self.relayer.notification_id {
1151                    self.job_producer
1152                        .produce_send_notification_job(
1153                            produce_relayer_disabled_payload(
1154                                notification_id,
1155                                &updated_relayer,
1156                                &reason.safe_description(),
1157                            ),
1158                            None,
1159                        )
1160                        .await?;
1161                }
1162
1163                // Schedule health check to try re-enabling the relayer after 10 seconds
1164                self.job_producer
1165                    .produce_relayer_health_check_job(
1166                        RelayerHealthCheck::new(self.relayer.id.clone()),
1167                        Some(calculate_scheduled_timestamp(10)),
1168                    )
1169                    .await?;
1170            }
1171        }
1172
1173        self.check_balance_and_trigger_token_swap_if_needed()
1174            .await?;
1175
1176        Ok(())
1177    }
1178
1179    #[instrument(
1180        level = "debug",
1181        skip(self),
1182        fields(
1183            request_id = ?crate::observability::request_id::get_request_id(),
1184            relayer_id = %self.relayer.id,
1185        )
1186    )]
1187    async fn check_health(&self) -> Result<(), Vec<HealthCheckFailure>> {
1188        debug!(
1189            "running health checks for Solana relayer {}",
1190            self.relayer.id
1191        );
1192
1193        let validate_rpc_result = self.validate_rpc().await;
1194        let validate_min_balance_result = self.validate_min_balance().await;
1195
1196        // Collect all failures
1197        let failures: Vec<HealthCheckFailure> = vec![
1198            validate_rpc_result
1199                .err()
1200                .map(|e| HealthCheckFailure::RpcValidationFailed(e.to_string())),
1201            validate_min_balance_result
1202                .err()
1203                .map(|e| HealthCheckFailure::BalanceCheckFailed(e.to_string())),
1204        ]
1205        .into_iter()
1206        .flatten()
1207        .collect();
1208
1209        if failures.is_empty() {
1210            info!("all health checks passed");
1211            Ok(())
1212        } else {
1213            warn!("health checks failed: {:?}", failures);
1214            Err(failures)
1215        }
1216    }
1217
1218    #[instrument(
1219        level = "debug",
1220        skip(self),
1221        fields(
1222            request_id = ?crate::observability::request_id::get_request_id(),
1223            relayer_id = %self.relayer.id,
1224        )
1225    )]
1226    async fn validate_min_balance(&self) -> Result<(), RelayerError> {
1227        let balance = self
1228            .provider
1229            .get_balance(&self.relayer.address)
1230            .await
1231            .map_err(|e| RelayerError::ProviderError(e.to_string()))?;
1232
1233        debug!(balance = %balance, "balance for relayer");
1234
1235        let policy = self.relayer.policies.get_solana_policy();
1236
1237        if balance < policy.min_balance.unwrap_or(DEFAULT_SOLANA_MIN_BALANCE) {
1238            return Err(RelayerError::InsufficientBalanceError(
1239                "Insufficient balance".to_string(),
1240            ));
1241        }
1242
1243        Ok(())
1244    }
1245}
1246
1247#[async_trait]
1248impl<RR, TR, J, S, JS, SP, NR> GasAbstractionTrait for SolanaRelayer<RR, TR, J, S, JS, SP, NR>
1249where
1250    RR: Repository<RelayerRepoModel, String> + RelayerRepository + Send + Sync + 'static,
1251    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
1252    J: JobProducerTrait + Send + Sync + 'static,
1253    S: SolanaSignTrait + Signer + Send + Sync + 'static,
1254    JS: JupiterServiceTrait + Send + Sync + 'static,
1255    SP: SolanaProviderTrait + Send + Sync + 'static,
1256    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
1257{
1258    #[instrument(
1259        level = "debug",
1260        skip(self, params),
1261        fields(
1262            request_id = ?crate::observability::request_id::get_request_id(),
1263            relayer_id = %self.relayer.id,
1264        )
1265    )]
1266    async fn quote_sponsored_transaction(
1267        &self,
1268        params: SponsoredTransactionQuoteRequest,
1269    ) -> Result<SponsoredTransactionQuoteResponse, RelayerError> {
1270        let params = match params {
1271            SponsoredTransactionQuoteRequest::Solana(p) => p,
1272            _ => {
1273                return Err(RelayerError::ValidationError(
1274                    "Expected Solana fee estimate request parameters".to_string(),
1275                ));
1276            }
1277        };
1278
1279        let result = self
1280            .rpc_handler
1281            .rpc_methods()
1282            .fee_estimate(params)
1283            .await
1284            .map_err(|e| RelayerError::Internal(e.to_string()))?;
1285
1286        Ok(SponsoredTransactionQuoteResponse::Solana(result))
1287    }
1288
1289    #[instrument(
1290        level = "debug",
1291        skip(self, params),
1292        fields(
1293            request_id = ?crate::observability::request_id::get_request_id(),
1294            relayer_id = %self.relayer.id,
1295        )
1296    )]
1297    async fn build_sponsored_transaction(
1298        &self,
1299        params: SponsoredTransactionBuildRequest,
1300    ) -> Result<SponsoredTransactionBuildResponse, RelayerError> {
1301        let params = match params {
1302            SponsoredTransactionBuildRequest::Solana(p) => p,
1303            _ => {
1304                return Err(RelayerError::ValidationError(
1305                    "Expected Solana prepare transaction request parameters".to_string(),
1306                ));
1307            }
1308        };
1309
1310        let result = self
1311            .rpc_handler
1312            .rpc_methods()
1313            .prepare_transaction(params)
1314            .await
1315            .map_err(|e| {
1316                let error_msg = format!("{e}");
1317                RelayerError::Internal(error_msg)
1318            })?;
1319
1320        Ok(SponsoredTransactionBuildResponse::Solana(result))
1321    }
1322}
1323
1324#[cfg(test)]
1325mod tests {
1326    use super::*;
1327    use crate::{
1328        config::{NetworkConfigCommon, SolanaNetworkConfig},
1329        domain::{
1330            create_network_dex_generic, Relayer, SignTransactionRequestSolana, SolanaRpcHandler,
1331            SolanaRpcMethodsImpl,
1332        },
1333        jobs::MockJobProducerTrait,
1334        models::{
1335            EncodedSerializedTransaction, JsonRpcId, NetworkConfigData, NetworkRepoModel,
1336            RelayerSolanaSwapConfig, RpcConfig, SolanaAllowedTokensSwapConfig,
1337            SolanaFeeEstimateRequestParams, SolanaGetFeaturesEnabledRequestParams, SolanaRpcResult,
1338            SolanaSwapStrategy,
1339        },
1340        repositories::{MockNetworkRepository, MockRelayerRepository, MockTransactionRepository},
1341        services::{
1342            provider::{MockSolanaProviderTrait, SolanaProviderError},
1343            signer::MockSolanaSignTrait,
1344            MockJupiterServiceTrait, QuoteResponse, RoutePlan, SwapEvents, SwapInfo, SwapResponse,
1345            UltraExecuteResponse, UltraOrderResponse,
1346        },
1347        utils::mocks::mockutils::create_mock_solana_network,
1348    };
1349    use chrono::Utc;
1350    use mockall::predicate::*;
1351    use solana_sdk::{hash::Hash, program_pack::Pack, signature::Signature};
1352    use spl_token_interface::state::Account as SplAccount;
1353
1354    /// Bundles all the pieces you need to instantiate a SolanaRelayer.
1355    /// Default::default gives you fresh mocks, but you can override any of them.
1356    #[allow(dead_code)]
1357    struct TestCtx {
1358        relayer_model: RelayerRepoModel,
1359        mock_repo: MockRelayerRepository,
1360        network_repository: Arc<MockNetworkRepository>,
1361        provider: Arc<MockSolanaProviderTrait>,
1362        signer: Arc<MockSolanaSignTrait>,
1363        jupiter: Arc<MockJupiterServiceTrait>,
1364        job_producer: Arc<MockJobProducerTrait>,
1365        tx_repo: Arc<MockTransactionRepository>,
1366        dex: Arc<NetworkDex<MockSolanaProviderTrait, MockSolanaSignTrait, MockJupiterServiceTrait>>,
1367        rpc_handler: SolanaRpcHandlerType<
1368            MockSolanaProviderTrait,
1369            MockSolanaSignTrait,
1370            MockJupiterServiceTrait,
1371            MockJobProducerTrait,
1372            MockTransactionRepository,
1373        >,
1374    }
1375
1376    impl Default for TestCtx {
1377        fn default() -> Self {
1378            let mock_repo = MockRelayerRepository::new();
1379            let provider = Arc::new(MockSolanaProviderTrait::new());
1380            let signer = Arc::new(MockSolanaSignTrait::new());
1381            let jupiter = Arc::new(MockJupiterServiceTrait::new());
1382            let job = Arc::new(MockJobProducerTrait::new());
1383            let tx_repo = Arc::new(MockTransactionRepository::new());
1384            let mut network_repository = MockNetworkRepository::new();
1385            let transaction_repository = Arc::new(MockTransactionRepository::new());
1386
1387            let relayer_model = RelayerRepoModel {
1388                id: "test-id".to_string(),
1389                address: "...".to_string(),
1390                network: "devnet".to_string(),
1391                ..Default::default()
1392            };
1393
1394            let dex = Arc::new(
1395                create_network_dex_generic(
1396                    &relayer_model,
1397                    provider.clone(),
1398                    signer.clone(),
1399                    jupiter.clone(),
1400                )
1401                .unwrap(),
1402            );
1403
1404            let test_network = create_mock_solana_network();
1405
1406            let rpc_handler = Arc::new(SolanaRpcHandler::new(SolanaRpcMethodsImpl::new_mock(
1407                relayer_model.clone(),
1408                test_network.clone(),
1409                provider.clone(),
1410                signer.clone(),
1411                jupiter.clone(),
1412                job.clone(),
1413                transaction_repository.clone(),
1414            )));
1415
1416            let test_network = NetworkRepoModel {
1417                id: "solana:devnet".to_string(),
1418                name: "devnet".to_string(),
1419                network_type: NetworkType::Solana,
1420                config: NetworkConfigData::Solana(SolanaNetworkConfig {
1421                    common: NetworkConfigCommon {
1422                        network: "devnet".to_string(),
1423                        from: None,
1424                        rpc_urls: Some(vec![RpcConfig::new(
1425                            "https://api.devnet.solana.com".to_string(),
1426                        )]),
1427                        explorer_urls: None,
1428                        average_blocktime_ms: Some(400),
1429                        is_testnet: Some(true),
1430                        tags: None,
1431                    },
1432                }),
1433            };
1434
1435            network_repository
1436                .expect_get_by_name()
1437                .returning(move |_, _| Ok(Some(test_network.clone())));
1438
1439            TestCtx {
1440                relayer_model,
1441                mock_repo,
1442                network_repository: Arc::new(network_repository),
1443                provider,
1444                signer,
1445                jupiter,
1446                job_producer: job,
1447                tx_repo,
1448                dex,
1449                rpc_handler,
1450            }
1451        }
1452    }
1453
1454    impl TestCtx {
1455        async fn into_relayer(
1456            self,
1457        ) -> SolanaRelayer<
1458            MockRelayerRepository,
1459            MockTransactionRepository,
1460            MockJobProducerTrait,
1461            MockSolanaSignTrait,
1462            MockJupiterServiceTrait,
1463            MockSolanaProviderTrait,
1464            MockNetworkRepository,
1465        > {
1466            // Get the network from the repository
1467            let network_repo = self
1468                .network_repository
1469                .get_by_name(NetworkType::Solana, "devnet")
1470                .await
1471                .unwrap()
1472                .unwrap();
1473            let network = SolanaNetwork::try_from(network_repo).unwrap();
1474
1475            SolanaRelayer {
1476                relayer: self.relayer_model.clone(),
1477                signer: self.signer,
1478                network,
1479                provider: self.provider,
1480                rpc_handler: self.rpc_handler,
1481                relayer_repository: Arc::new(self.mock_repo),
1482                transaction_repository: self.tx_repo,
1483                job_producer: self.job_producer,
1484                dex_service: self.dex,
1485                network_repository: self.network_repository,
1486            }
1487        }
1488    }
1489
1490    fn create_test_relayer() -> RelayerRepoModel {
1491        RelayerRepoModel {
1492            id: "test-relayer-id".to_string(),
1493            address: "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin".to_string(),
1494            notification_id: Some("test-notification-id".to_string()),
1495            network_type: NetworkType::Solana,
1496            policies: RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
1497                min_balance: Some(0), // No minimum balance requirement
1498                swap_config: None,
1499                ..Default::default()
1500            }),
1501            ..Default::default()
1502        }
1503    }
1504
1505    fn create_token_policy(
1506        mint: &str,
1507        min_amount: Option<u64>,
1508        max_amount: Option<u64>,
1509        retain_min: Option<u64>,
1510        slippage: Option<u64>,
1511    ) -> SolanaAllowedTokensPolicy {
1512        let mut token = SolanaAllowedTokensPolicy {
1513            mint: mint.to_string(),
1514            max_allowed_fee: Some(0),
1515            swap_config: None,
1516            decimals: Some(9),
1517            symbol: Some("SOL".to_string()),
1518        };
1519
1520        let swap_config = SolanaAllowedTokensSwapConfig {
1521            min_amount,
1522            max_amount,
1523            retain_min_amount: retain_min,
1524            slippage_percentage: slippage.map(|s| s as f32),
1525        };
1526
1527        token.swap_config = Some(swap_config);
1528        token
1529    }
1530
1531    #[tokio::test]
1532    async fn test_calculate_swap_amount_no_limits() {
1533        let ctx = TestCtx::default();
1534        let solana_relayer = ctx.into_relayer().await;
1535
1536        assert_eq!(
1537            solana_relayer
1538                .calculate_swap_amount(100, None, None, None)
1539                .unwrap(),
1540            100
1541        );
1542    }
1543
1544    #[tokio::test]
1545    async fn test_calculate_swap_amount_with_max() {
1546        let ctx = TestCtx::default();
1547        let solana_relayer = ctx.into_relayer().await;
1548
1549        assert_eq!(
1550            solana_relayer
1551                .calculate_swap_amount(100, None, Some(60), None)
1552                .unwrap(),
1553            60
1554        );
1555    }
1556
1557    #[tokio::test]
1558    async fn test_calculate_swap_amount_with_retain() {
1559        let ctx = TestCtx::default();
1560        let solana_relayer = ctx.into_relayer().await;
1561
1562        assert_eq!(
1563            solana_relayer
1564                .calculate_swap_amount(100, None, None, Some(30))
1565                .unwrap(),
1566            70
1567        );
1568
1569        assert_eq!(
1570            solana_relayer
1571                .calculate_swap_amount(20, None, None, Some(30))
1572                .unwrap(),
1573            0
1574        );
1575    }
1576
1577    #[tokio::test]
1578    async fn test_calculate_swap_amount_with_min() {
1579        let ctx = TestCtx::default();
1580        let solana_relayer = ctx.into_relayer().await;
1581
1582        assert_eq!(
1583            solana_relayer
1584                .calculate_swap_amount(40, Some(50), None, None)
1585                .unwrap(),
1586            0
1587        );
1588
1589        assert_eq!(
1590            solana_relayer
1591                .calculate_swap_amount(100, Some(50), None, None)
1592                .unwrap(),
1593            100
1594        );
1595    }
1596
1597    #[tokio::test]
1598    async fn test_calculate_swap_amount_combined() {
1599        let ctx = TestCtx::default();
1600        let solana_relayer = ctx.into_relayer().await;
1601
1602        assert_eq!(
1603            solana_relayer
1604                .calculate_swap_amount(100, None, Some(50), Some(30))
1605                .unwrap(),
1606            50
1607        );
1608
1609        assert_eq!(
1610            solana_relayer
1611                .calculate_swap_amount(100, Some(20), Some(50), Some(30))
1612                .unwrap(),
1613            50
1614        );
1615
1616        assert_eq!(
1617            solana_relayer
1618                .calculate_swap_amount(100, Some(60), Some(50), Some(30))
1619                .unwrap(),
1620            0
1621        );
1622    }
1623
1624    #[tokio::test]
1625    async fn test_handle_token_swap_request_successful_swap_jupiter_swap_strategy() {
1626        let mut relayer_model = create_test_relayer();
1627
1628        let mut mock_relayer_repo = MockRelayerRepository::new();
1629        let id = relayer_model.id.clone();
1630
1631        relayer_model.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
1632            swap_config: Some(RelayerSolanaSwapConfig {
1633                strategy: Some(SolanaSwapStrategy::JupiterSwap),
1634                cron_schedule: None,
1635                min_balance_threshold: None,
1636                jupiter_swap_options: None,
1637            }),
1638            allowed_tokens: Some(vec![create_token_policy(
1639                "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
1640                Some(1),
1641                None,
1642                None,
1643                Some(50),
1644            )]),
1645            ..Default::default()
1646        });
1647        let cloned = relayer_model.clone();
1648
1649        mock_relayer_repo
1650            .expect_get_by_id()
1651            .with(eq(id.clone()))
1652            .times(1)
1653            .returning(move |_| Ok(cloned.clone()));
1654
1655        let mut raw_provider = MockSolanaProviderTrait::new();
1656
1657        raw_provider
1658            .expect_get_account_from_pubkey()
1659            .returning(|_| {
1660                Box::pin(async {
1661                    let mut account_data = vec![0; SplAccount::LEN];
1662
1663                    let token_account = spl_token_interface::state::Account {
1664                        mint: Pubkey::new_unique(),
1665                        owner: Pubkey::new_unique(),
1666                        amount: 10000000,
1667                        state: spl_token_interface::state::AccountState::Initialized,
1668                        ..Default::default()
1669                    };
1670                    spl_token_interface::state::Account::pack(token_account, &mut account_data)
1671                        .unwrap();
1672
1673                    Ok(solana_sdk::account::Account {
1674                        lamports: 1_000_000,
1675                        data: account_data,
1676                        owner: spl_token_interface::id(),
1677                        executable: false,
1678                        rent_epoch: 0,
1679                    })
1680                })
1681            });
1682
1683        let mut jupiter_mock = MockJupiterServiceTrait::new();
1684
1685        jupiter_mock.expect_get_quote().returning(|_| {
1686            Box::pin(async {
1687                Ok(QuoteResponse {
1688                    input_mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
1689                    output_mint: WRAPPED_SOL_MINT.to_string(),
1690                    in_amount: 10,
1691                    out_amount: 10,
1692                    other_amount_threshold: 1,
1693                    swap_mode: "ExactIn".to_string(),
1694                    price_impact_pct: 0.0,
1695                    route_plan: vec![RoutePlan {
1696                        percent: 100,
1697                        swap_info: SwapInfo {
1698                            amm_key: "mock_amm_key".to_string(),
1699                            label: "mock_label".to_string(),
1700                            input_mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
1701                            output_mint: WRAPPED_SOL_MINT.to_string(),
1702                            in_amount: "1000".to_string(),
1703                            out_amount: "1000".to_string(),
1704                            fee_amount: Some("0".to_string()),
1705                            fee_mint: Some("mock_fee_mint".to_string()),
1706                        },
1707                    }],
1708                    slippage_bps: 0,
1709                })
1710            })
1711        });
1712
1713        jupiter_mock.expect_get_swap_transaction().returning(|_| {
1714            Box::pin(async {
1715                Ok(SwapResponse {
1716                    swap_transaction: "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAKEZhsMunBegjHhwObzSrJeKhnl3sehIwqA8OCTejBJ/Z+O7sAR2gDS0+R1HXkqqjr0Wo3+auYeJQtq0il4DAumgiiHZpJZ1Uy9xq1yiOta3BcBOI7Dv+jmETs0W7Leny+AsVIwZWPN51bjn3Xk4uSzTFeAEom3HHY/EcBBpOfm7HkzWyukBvmNY5l9pnNxB/lTC52M7jy0Pxg6NhYJ37e1WXRYOFdoHOThs0hoFy/UG3+mVBbkR4sB9ywdKopv6IHO9+wuF/sV/02h9w+AjIBszK2bmCBPIrCZH4mqBdRcBFVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABPS2wOQQj9KmokeOrgrMWdshu07fURwWLPYC0eDAkB+1Jh0UqsxbwO7GNdqHBaH3CjnuNams8L+PIsxs5JAZ16jJclj04kifG7PRApFI4NgwtaE5na/xCEBI572Nvp+FmsH4P9uc5VDeldVYzceVRhzPQ3SsaI7BOphAAiCnjaBgMGRm/lIRcy/+ytunLDm+e8jOW7xfcSayxDmzpAAAAAtD/6J/XX9kp0wJsfKVh53ksJqzbfyd1RSzIap7OM5ejnStls42Wf0xNRAChL93gEW4UQqPNOSYySLu5vwwX4aQR51VvyMcBu7nTFbs5oFQf9sbLeo/SOUQKxzaJWvBOPBt324ddloZPZy+FGzut5rBy0he1fWzeROoz1hX7/AKkGtJJ5s3DlXjsp517KoA8Lg71wC+tMHoDO9HDeQbotrwUMAAUCwFwVAAwACQOhzhsAAAAAAAoGAAQAIgcQAQEPOxAIAAUGAgQgIg8PDQ8hEg4JExEGARQUFAgQKAgmKgEDFhgXFSUnJCkQIywQIysIHSIqAh8DHhkbGhwLL8EgmzNB1pyBBwMAAAA6AWQAAU9kAQIvAABkAgNAQg8AAAAAAE3WYgAAAAAADwAAEAMEAAABCQMW8exZwhONJLLrrr9eKTOouI7XVrRLBjytPl3cL6rziwS+v7vCBB+8CQctooGHnRbQ3aoExfOLSH0uJhZijTPAKrJbYSJJ5hP1VwRmY2FlBkRkC2JtQsJRwDIR3Tbag/HLEdZxTPfqLWdCCyd0nco65bHdIoy/ByorMycoLzADMiYs".to_string(),
1717                    last_valid_block_height: 100,
1718                    prioritization_fee_lamports: None,
1719                    compute_unit_limit: None,
1720                    simulation_error: None,
1721                })
1722            })
1723        });
1724
1725        let mut signer = MockSolanaSignTrait::new();
1726        let test_signature = Signature::from_str("2jg9xbGLtZRsiJBrDWQnz33JuLjDkiKSZuxZPdjJ3qrJbMeTEerXFAKynkPW63J88nq63cvosDNRsg9VqHtGixvP").unwrap();
1727
1728        signer
1729            .expect_sign()
1730            .times(1)
1731            .returning(move |_| Box::pin(async move { Ok(test_signature) }));
1732
1733        raw_provider
1734            .expect_send_versioned_transaction()
1735            .times(1)
1736            .returning(move |_| Box::pin(async move { Ok(test_signature) }));
1737
1738        raw_provider
1739            .expect_confirm_transaction()
1740            .times(1)
1741            .returning(move |_| Box::pin(async move { Ok(true) }));
1742
1743        let provider_arc = Arc::new(raw_provider);
1744        let jupiter_arc = Arc::new(jupiter_mock);
1745        let signer_arc = Arc::new(signer);
1746
1747        let dex = Arc::new(
1748            create_network_dex_generic(
1749                &relayer_model,
1750                provider_arc.clone(),
1751                signer_arc.clone(),
1752                jupiter_arc.clone(),
1753            )
1754            .unwrap(),
1755        );
1756
1757        let mut job_producer = MockJobProducerTrait::new();
1758        job_producer
1759            .expect_produce_send_notification_job()
1760            .times(1)
1761            .returning(|_, _| Box::pin(async { Ok(()) }));
1762
1763        let job_producer_arc = Arc::new(job_producer);
1764
1765        let ctx = TestCtx {
1766            relayer_model,
1767            mock_repo: mock_relayer_repo,
1768            provider: provider_arc.clone(),
1769            jupiter: jupiter_arc.clone(),
1770            signer: signer_arc.clone(),
1771            dex,
1772            job_producer: job_producer_arc.clone(),
1773            ..Default::default()
1774        };
1775        let solana_relayer = ctx.into_relayer().await;
1776        let res = solana_relayer
1777            .handle_token_swap_request(create_test_relayer().id)
1778            .await
1779            .unwrap();
1780        assert_eq!(res.len(), 1);
1781        let swap = &res[0];
1782        assert_eq!(swap.source_amount, 10000000);
1783        assert_eq!(swap.destination_amount, 10);
1784        assert_eq!(swap.transaction_signature, test_signature.to_string());
1785    }
1786
1787    #[tokio::test]
1788    async fn test_handle_token_swap_request_successful_swap_jupiter_ultra_strategy() {
1789        let mut relayer_model = create_test_relayer();
1790
1791        let mut mock_relayer_repo = MockRelayerRepository::new();
1792        let id = relayer_model.id.clone();
1793
1794        relayer_model.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
1795            swap_config: Some(RelayerSolanaSwapConfig {
1796                strategy: Some(SolanaSwapStrategy::JupiterUltra),
1797                cron_schedule: None,
1798                min_balance_threshold: None,
1799                jupiter_swap_options: None,
1800            }),
1801            allowed_tokens: Some(vec![create_token_policy(
1802                "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
1803                Some(1),
1804                None,
1805                None,
1806                Some(50),
1807            )]),
1808            ..Default::default()
1809        });
1810        let cloned = relayer_model.clone();
1811
1812        mock_relayer_repo
1813            .expect_get_by_id()
1814            .with(eq(id.clone()))
1815            .times(1)
1816            .returning(move |_| Ok(cloned.clone()));
1817
1818        let mut raw_provider = MockSolanaProviderTrait::new();
1819
1820        raw_provider
1821            .expect_get_account_from_pubkey()
1822            .returning(|_| {
1823                Box::pin(async {
1824                    let mut account_data = vec![0; SplAccount::LEN];
1825
1826                    let token_account = spl_token_interface::state::Account {
1827                        mint: Pubkey::new_unique(),
1828                        owner: Pubkey::new_unique(),
1829                        amount: 10000000,
1830                        state: spl_token_interface::state::AccountState::Initialized,
1831                        ..Default::default()
1832                    };
1833                    spl_token_interface::state::Account::pack(token_account, &mut account_data)
1834                        .unwrap();
1835
1836                    Ok(solana_sdk::account::Account {
1837                        lamports: 1_000_000,
1838                        data: account_data,
1839                        owner: spl_token_interface::id(),
1840                        executable: false,
1841                        rent_epoch: 0,
1842                    })
1843                })
1844            });
1845
1846        let mut jupiter_mock = MockJupiterServiceTrait::new();
1847        jupiter_mock.expect_get_ultra_order().returning(|_| {
1848            Box::pin(async {
1849                Ok(UltraOrderResponse {
1850                    transaction: Some("AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAKEZhsMunBegjHhwObzSrJeKhnl3sehIwqA8OCTejBJ/Z+O7sAR2gDS0+R1HXkqqjr0Wo3+auYeJQtq0il4DAumgiiHZpJZ1Uy9xq1yiOta3BcBOI7Dv+jmETs0W7Leny+AsVIwZWPN51bjn3Xk4uSzTFeAEom3HHY/EcBBpOfm7HkzWyukBvmNY5l9pnNxB/lTC52M7jy0Pxg6NhYJ37e1WXRYOFdoHOThs0hoFy/UG3+mVBbkR4sB9ywdKopv6IHO9+wuF/sV/02h9w+AjIBszK2bmCBPIrCZH4mqBdRcBFVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABPS2wOQQj9KmokeOrgrMWdshu07fURwWLPYC0eDAkB+1Jh0UqsxbwO7GNdqHBaH3CjnuNams8L+PIsxs5JAZ16jJclj04kifG7PRApFI4NgwtaE5na/xCEBI572Nvp+FmsH4P9uc5VDeldVYzceVRhzPQ3SsaI7BOphAAiCnjaBgMGRm/lIRcy/+ytunLDm+e8jOW7xfcSayxDmzpAAAAAtD/6J/XX9kp0wJsfKVh53ksJqzbfyd1RSzIap7OM5ejnStls42Wf0xNRAChL93gEW4UQqPNOSYySLu5vwwX4aQR51VvyMcBu7nTFbs5oFQf9sbLeo/SOUQKxzaJWvBOPBt324ddloZPZy+FGzut5rBy0he1fWzeROoz1hX7/AKkGtJJ5s3DlXjsp517KoA8Lg71wC+tMHoDO9HDeQbotrwUMAAUCwFwVAAwACQOhzhsAAAAAAAoGAAQAIgcQAQEPOxAIAAUGAgQgIg8PDQ8hEg4JExEGARQUFAgQKAgmKgEDFhgXFSUnJCkQIywQIysIHSIqAh8DHhkbGhwLL8EgmzNB1pyBBwMAAAA6AWQAAU9kAQIvAABkAgNAQg8AAAAAAE3WYgAAAAAADwAAEAMEAAABCQMW8exZwhONJLLrrr9eKTOouI7XVrRLBjytPl3cL6rziwS+v7vCBB+8CQctooGHnRbQ3aoExfOLSH0uJhZijTPAKrJbYSJJ5hP1VwRmY2FlBkRkC2JtQsJRwDIR3Tbag/HLEdZxTPfqLWdCCyd0nco65bHdIoy/ByorMycoLzADMiYs".to_string()),
1851                    input_mint: "PjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
1852                    output_mint: WRAPPED_SOL_MINT.to_string(),
1853                    in_amount: 10,
1854                    out_amount: 10,
1855                    other_amount_threshold: 1,
1856                    swap_mode: "ExactIn".to_string(),
1857                    price_impact_pct: 0.0,
1858                    route_plan: vec![RoutePlan {
1859                        percent: 100,
1860                        swap_info: SwapInfo {
1861                            amm_key: "mock_amm_key".to_string(),
1862                            label: "mock_label".to_string(),
1863                            input_mint: "PjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
1864                            output_mint: WRAPPED_SOL_MINT.to_string(),
1865                            in_amount: "1000".to_string(),
1866                            out_amount: "1000".to_string(),
1867                            fee_amount: Some("0".to_string()),
1868                            fee_mint: Some("mock_fee_mint".to_string()),
1869                        },
1870                    }],
1871                    prioritization_fee_lamports: 0,
1872                    request_id: "mock_request_id".to_string(),
1873                    slippage_bps: 0,
1874                })
1875            })
1876        });
1877
1878        jupiter_mock.expect_execute_ultra_order().returning(|_| {
1879            Box::pin(async {
1880               Ok(UltraExecuteResponse {
1881                    signature: Some("2jg9xbGLtZRsiJBrDWQnz33JuLjDkiKSZuxZPdjJ3qrJbMeTEerXFAKynkPW63J88nq63cvosDNRsg9VqHtGixvP".to_string()),
1882                    status: "success".to_string(),
1883                    slot: Some("123456789".to_string()),
1884                    error: None,
1885                    code: 0,
1886                    total_input_amount: Some("1000000".to_string()),
1887                    total_output_amount: Some("1000000".to_string()),
1888                    input_amount_result: Some("1000000".to_string()),
1889                    output_amount_result: Some("1000000".to_string()),
1890                    swap_events: Some(vec![SwapEvents {
1891                        input_mint: "mock_input_mint".to_string(),
1892                        output_mint: "mock_output_mint".to_string(),
1893                        input_amount: "1000000".to_string(),
1894                        output_amount: "1000000".to_string(),
1895                    }]),
1896                })
1897            })
1898        });
1899
1900        let mut signer = MockSolanaSignTrait::new();
1901        let test_signature = Signature::from_str("2jg9xbGLtZRsiJBrDWQnz33JuLjDkiKSZuxZPdjJ3qrJbMeTEerXFAKynkPW63J88nq63cvosDNRsg9VqHtGixvP").unwrap();
1902
1903        signer
1904            .expect_sign()
1905            .times(1)
1906            .returning(move |_| Box::pin(async move { Ok(test_signature) }));
1907
1908        let provider_arc = Arc::new(raw_provider);
1909        let jupiter_arc = Arc::new(jupiter_mock);
1910        let signer_arc = Arc::new(signer);
1911
1912        let dex = Arc::new(
1913            create_network_dex_generic(
1914                &relayer_model,
1915                provider_arc.clone(),
1916                signer_arc.clone(),
1917                jupiter_arc.clone(),
1918            )
1919            .unwrap(),
1920        );
1921
1922        let mut job_producer = MockJobProducerTrait::new();
1923        job_producer
1924            .expect_produce_send_notification_job()
1925            .times(1)
1926            .returning(|_, _| Box::pin(async { Ok(()) }));
1927
1928        let job_producer_arc = Arc::new(job_producer);
1929
1930        let ctx = TestCtx {
1931            relayer_model,
1932            mock_repo: mock_relayer_repo,
1933            provider: provider_arc.clone(),
1934            jupiter: jupiter_arc.clone(),
1935            signer: signer_arc.clone(),
1936            dex,
1937            job_producer: job_producer_arc.clone(),
1938            ..Default::default()
1939        };
1940        let solana_relayer = ctx.into_relayer().await;
1941
1942        let res = solana_relayer
1943            .handle_token_swap_request(create_test_relayer().id)
1944            .await
1945            .unwrap();
1946        assert_eq!(res.len(), 1);
1947        let swap = &res[0];
1948        assert_eq!(swap.source_amount, 10000000);
1949        assert_eq!(swap.destination_amount, 10);
1950        assert_eq!(swap.transaction_signature, test_signature.to_string());
1951    }
1952
1953    #[tokio::test]
1954    async fn test_handle_token_swap_request_no_swap_config() {
1955        let mut relayer_model = create_test_relayer();
1956
1957        let mut mock_relayer_repo = MockRelayerRepository::new();
1958        let id = relayer_model.id.clone();
1959        let cloned = relayer_model.clone();
1960        mock_relayer_repo
1961            .expect_get_by_id()
1962            .with(eq(id.clone()))
1963            .times(1)
1964            .returning(move |_| Ok(cloned.clone()));
1965
1966        relayer_model.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
1967            swap_config: Some(RelayerSolanaSwapConfig {
1968                strategy: Some(SolanaSwapStrategy::JupiterSwap),
1969                cron_schedule: None,
1970                min_balance_threshold: None,
1971                jupiter_swap_options: None,
1972            }),
1973            allowed_tokens: Some(vec![create_token_policy(
1974                "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
1975                Some(1),
1976                None,
1977                None,
1978                Some(50),
1979            )]),
1980            ..Default::default()
1981        });
1982        let mut job_producer = MockJobProducerTrait::new();
1983        job_producer.expect_produce_send_notification_job().times(0);
1984
1985        let job_producer_arc = Arc::new(job_producer);
1986
1987        let ctx = TestCtx {
1988            relayer_model,
1989            mock_repo: mock_relayer_repo,
1990            job_producer: job_producer_arc,
1991            ..Default::default()
1992        };
1993        let solana_relayer = ctx.into_relayer().await;
1994
1995        let res = solana_relayer.handle_token_swap_request(id).await;
1996        assert!(res.is_ok());
1997        assert!(res.unwrap().is_empty());
1998    }
1999
2000    #[tokio::test]
2001    async fn test_handle_token_swap_request_no_strategy() {
2002        let mut relayer_model: RelayerRepoModel = create_test_relayer();
2003
2004        let mut mock_relayer_repo = MockRelayerRepository::new();
2005        let id = relayer_model.id.clone();
2006        let cloned = relayer_model.clone();
2007        mock_relayer_repo
2008            .expect_get_by_id()
2009            .with(eq(id.clone()))
2010            .times(1)
2011            .returning(move |_| Ok(cloned.clone()));
2012
2013        relayer_model.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
2014            swap_config: Some(RelayerSolanaSwapConfig {
2015                strategy: None,
2016                cron_schedule: None,
2017                min_balance_threshold: Some(1),
2018                jupiter_swap_options: None,
2019            }),
2020            ..Default::default()
2021        });
2022
2023        let ctx = TestCtx {
2024            relayer_model,
2025            mock_repo: mock_relayer_repo,
2026            ..Default::default()
2027        };
2028        let solana_relayer = ctx.into_relayer().await;
2029
2030        let res = solana_relayer.handle_token_swap_request(id).await.unwrap();
2031        assert!(res.is_empty(), "should return empty when no strategy");
2032    }
2033
2034    #[tokio::test]
2035    async fn test_handle_token_swap_request_no_allowed_tokens() {
2036        let mut relayer_model: RelayerRepoModel = create_test_relayer();
2037        let mut mock_relayer_repo = MockRelayerRepository::new();
2038        let id = relayer_model.id.clone();
2039        let cloned = relayer_model.clone();
2040        mock_relayer_repo
2041            .expect_get_by_id()
2042            .with(eq(id.clone()))
2043            .times(1)
2044            .returning(move |_| Ok(cloned.clone()));
2045
2046        relayer_model.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
2047            swap_config: Some(RelayerSolanaSwapConfig {
2048                strategy: Some(SolanaSwapStrategy::JupiterSwap),
2049                cron_schedule: None,
2050                min_balance_threshold: Some(1),
2051                jupiter_swap_options: None,
2052            }),
2053            allowed_tokens: None,
2054            ..Default::default()
2055        });
2056
2057        let ctx = TestCtx {
2058            relayer_model,
2059            mock_repo: mock_relayer_repo,
2060            ..Default::default()
2061        };
2062        let solana_relayer = ctx.into_relayer().await;
2063
2064        let res = solana_relayer.handle_token_swap_request(id).await.unwrap();
2065        assert!(res.is_empty(), "should return empty when no allowed_tokens");
2066    }
2067
2068    #[tokio::test]
2069    async fn test_validate_rpc_success() {
2070        let mut raw_provider = MockSolanaProviderTrait::new();
2071        raw_provider
2072            .expect_get_latest_blockhash()
2073            .times(1)
2074            .returning(|| Box::pin(async { Ok(Hash::new_unique()) }));
2075
2076        let ctx = TestCtx {
2077            provider: Arc::new(raw_provider),
2078            ..Default::default()
2079        };
2080        let solana_relayer = ctx.into_relayer().await;
2081        let res = solana_relayer.validate_rpc().await;
2082
2083        assert!(
2084            res.is_ok(),
2085            "validate_rpc should succeed when blockhash fetch succeeds"
2086        );
2087    }
2088
2089    #[tokio::test]
2090    async fn test_validate_rpc_provider_error() {
2091        let mut raw_provider = MockSolanaProviderTrait::new();
2092        raw_provider
2093            .expect_get_latest_blockhash()
2094            .times(1)
2095            .returning(|| {
2096                Box::pin(async { Err(SolanaProviderError::RpcError("rpc failure".to_string())) })
2097            });
2098
2099        let ctx = TestCtx {
2100            provider: Arc::new(raw_provider),
2101            ..Default::default()
2102        };
2103
2104        let solana_relayer = ctx.into_relayer().await;
2105        let err = solana_relayer.validate_rpc().await.unwrap_err();
2106
2107        match err {
2108            RelayerError::ProviderError(msg) => {
2109                assert!(msg.contains("rpc failure"));
2110            }
2111            other => panic!("expected ProviderError, got {other:?}"),
2112        }
2113    }
2114
2115    #[tokio::test]
2116    async fn test_check_balance_no_swap_config() {
2117        // default ctx has no swap_config
2118        let ctx = TestCtx::default();
2119        let solana_relayer = ctx.into_relayer().await;
2120
2121        // should do nothing and succeed
2122        assert!(solana_relayer
2123            .check_balance_and_trigger_token_swap_if_needed()
2124            .await
2125            .is_ok());
2126    }
2127
2128    #[tokio::test]
2129    async fn test_check_balance_no_threshold() {
2130        // override policy to have a swap_config with no min_balance_threshold
2131        let mut ctx = TestCtx::default();
2132        let mut model = ctx.relayer_model.clone();
2133        model.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
2134            swap_config: Some(RelayerSolanaSwapConfig {
2135                strategy: Some(SolanaSwapStrategy::JupiterSwap),
2136                cron_schedule: None,
2137                min_balance_threshold: None,
2138                jupiter_swap_options: None,
2139            }),
2140            ..Default::default()
2141        });
2142        ctx.relayer_model = model;
2143        let solana_relayer = ctx.into_relayer().await;
2144
2145        assert!(solana_relayer
2146            .check_balance_and_trigger_token_swap_if_needed()
2147            .await
2148            .is_ok());
2149    }
2150
2151    #[tokio::test]
2152    async fn test_check_balance_above_threshold() {
2153        let mut raw_provider = MockSolanaProviderTrait::new();
2154        raw_provider
2155            .expect_get_balance()
2156            .times(1)
2157            .returning(|_| Box::pin(async { Ok(20_u64) }));
2158        let provider = Arc::new(raw_provider);
2159        let mut raw_job = MockJobProducerTrait::new();
2160        raw_job
2161            .expect_produce_token_swap_request_job()
2162            .withf(move |req, _opts| req.relayer_id == "test-id")
2163            .times(0);
2164        let job_producer = Arc::new(raw_job);
2165
2166        let ctx = TestCtx {
2167            provider,
2168            job_producer,
2169            ..Default::default()
2170        };
2171        // set threshold to 10
2172        let mut model = ctx.relayer_model.clone();
2173        model.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
2174            swap_config: Some(RelayerSolanaSwapConfig {
2175                strategy: Some(SolanaSwapStrategy::JupiterSwap),
2176                cron_schedule: None,
2177                min_balance_threshold: Some(10),
2178                jupiter_swap_options: None,
2179            }),
2180            ..Default::default()
2181        });
2182        let mut ctx = ctx;
2183        ctx.relayer_model = model;
2184
2185        let solana_relayer = ctx.into_relayer().await;
2186        assert!(solana_relayer
2187            .check_balance_and_trigger_token_swap_if_needed()
2188            .await
2189            .is_ok());
2190    }
2191
2192    #[tokio::test]
2193    async fn test_check_balance_below_threshold_triggers_job() {
2194        let mut raw_provider = MockSolanaProviderTrait::new();
2195        raw_provider
2196            .expect_get_balance()
2197            .times(1)
2198            .returning(|_| Box::pin(async { Ok(5_u64) }));
2199
2200        let mut raw_job = MockJobProducerTrait::new();
2201        raw_job
2202            .expect_produce_token_swap_request_job()
2203            .times(1)
2204            .returning(|_, _| Box::pin(async { Ok(()) }));
2205        let job_producer = Arc::new(raw_job);
2206
2207        let mut model = create_test_relayer();
2208        model.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
2209            swap_config: Some(RelayerSolanaSwapConfig {
2210                strategy: Some(SolanaSwapStrategy::JupiterSwap),
2211                cron_schedule: None,
2212                min_balance_threshold: Some(10),
2213                jupiter_swap_options: None,
2214            }),
2215            ..Default::default()
2216        });
2217
2218        let ctx = TestCtx {
2219            relayer_model: model,
2220            provider: Arc::new(raw_provider),
2221            job_producer,
2222            ..Default::default()
2223        };
2224
2225        let solana_relayer = ctx.into_relayer().await;
2226        assert!(solana_relayer
2227            .check_balance_and_trigger_token_swap_if_needed()
2228            .await
2229            .is_ok());
2230    }
2231
2232    #[tokio::test]
2233    async fn test_get_balance_success() {
2234        let mut raw_provider = MockSolanaProviderTrait::new();
2235        raw_provider
2236            .expect_get_balance()
2237            .times(1)
2238            .returning(|_| Box::pin(async { Ok(42_u64) }));
2239        let ctx = TestCtx {
2240            provider: Arc::new(raw_provider),
2241            ..Default::default()
2242        };
2243        let solana_relayer = ctx.into_relayer().await;
2244
2245        let res = solana_relayer.get_balance().await.unwrap();
2246
2247        assert_eq!(res.balance, 42_u128);
2248        assert_eq!(res.unit, SOLANA_SMALLEST_UNIT_NAME);
2249    }
2250
2251    #[tokio::test]
2252    async fn test_get_balance_provider_error() {
2253        let mut raw_provider = MockSolanaProviderTrait::new();
2254        raw_provider
2255            .expect_get_balance()
2256            .times(1)
2257            .returning(|_| Box::pin(async { Err(SolanaProviderError::RpcError("oops".into())) }));
2258        let ctx = TestCtx {
2259            provider: Arc::new(raw_provider),
2260            ..Default::default()
2261        };
2262        let solana_relayer = ctx.into_relayer().await;
2263
2264        let err = solana_relayer.get_balance().await.unwrap_err();
2265
2266        match err {
2267            RelayerError::UnderlyingSolanaProvider(err) => {
2268                assert!(err.to_string().contains("oops"));
2269            }
2270            other => panic!("expected ProviderError, got {other:?}"),
2271        }
2272    }
2273
2274    #[tokio::test]
2275    async fn test_validate_min_balance_success() {
2276        let mut raw_provider = MockSolanaProviderTrait::new();
2277        raw_provider
2278            .expect_get_balance()
2279            .times(1)
2280            .returning(|_| Box::pin(async { Ok(100_u64) }));
2281
2282        let mut model = create_test_relayer();
2283        model.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
2284            min_balance: Some(50),
2285            ..Default::default()
2286        });
2287
2288        let ctx = TestCtx {
2289            relayer_model: model,
2290            provider: Arc::new(raw_provider),
2291            ..Default::default()
2292        };
2293
2294        let solana_relayer = ctx.into_relayer().await;
2295        assert!(solana_relayer.validate_min_balance().await.is_ok());
2296    }
2297
2298    #[tokio::test]
2299    async fn test_validate_min_balance_insufficient() {
2300        let mut raw_provider = MockSolanaProviderTrait::new();
2301        raw_provider
2302            .expect_get_balance()
2303            .times(1)
2304            .returning(|_| Box::pin(async { Ok(10_u64) }));
2305
2306        let mut model = create_test_relayer();
2307        model.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
2308            min_balance: Some(50),
2309            ..Default::default()
2310        });
2311
2312        let ctx = TestCtx {
2313            relayer_model: model,
2314            provider: Arc::new(raw_provider),
2315            ..Default::default()
2316        };
2317
2318        let solana_relayer = ctx.into_relayer().await;
2319        let err = solana_relayer.validate_min_balance().await.unwrap_err();
2320        match err {
2321            RelayerError::InsufficientBalanceError(msg) => {
2322                assert_eq!(msg, "Insufficient balance");
2323            }
2324            other => panic!("expected InsufficientBalanceError, got {other:?}"),
2325        }
2326    }
2327
2328    #[tokio::test]
2329    async fn test_validate_min_balance_provider_error() {
2330        let mut raw_provider = MockSolanaProviderTrait::new();
2331        raw_provider
2332            .expect_get_balance()
2333            .times(1)
2334            .returning(|_| Box::pin(async { Err(SolanaProviderError::RpcError("fail".into())) }));
2335        let ctx = TestCtx {
2336            provider: Arc::new(raw_provider),
2337            ..Default::default()
2338        };
2339
2340        let solana_relayer = ctx.into_relayer().await;
2341        let err = solana_relayer.validate_min_balance().await.unwrap_err();
2342        match err {
2343            RelayerError::ProviderError(msg) => {
2344                assert!(msg.contains("fail"));
2345            }
2346            other => panic!("expected ProviderError, got {other:?}"),
2347        }
2348    }
2349
2350    #[tokio::test]
2351    async fn test_rpc_invalid_params() {
2352        let ctx = TestCtx::default();
2353        let solana_relayer = ctx.into_relayer().await;
2354
2355        let req = JsonRpcRequest {
2356            jsonrpc: "2.0".to_string(),
2357            params: NetworkRpcRequest::Solana(crate::models::SolanaRpcRequest::FeeEstimate(
2358                SolanaFeeEstimateRequestParams {
2359                    transaction: EncodedSerializedTransaction::new("".to_string()),
2360                    fee_token: "".to_string(),
2361                },
2362            )),
2363            id: Some(JsonRpcId::Number(1)),
2364        };
2365        let resp = solana_relayer.rpc(req).await.unwrap();
2366
2367        assert!(resp.error.is_some(), "expected an error object");
2368        let err = resp.error.unwrap();
2369        assert_eq!(err.code, -32601);
2370        assert_eq!(err.message, "INVALID_PARAMS");
2371    }
2372
2373    #[tokio::test]
2374    async fn test_rpc_success() {
2375        let ctx = TestCtx::default();
2376        let solana_relayer = ctx.into_relayer().await;
2377
2378        let req = JsonRpcRequest {
2379            jsonrpc: "2.0".to_string(),
2380            params: NetworkRpcRequest::Solana(crate::models::SolanaRpcRequest::GetFeaturesEnabled(
2381                SolanaGetFeaturesEnabledRequestParams {},
2382            )),
2383            id: Some(JsonRpcId::Number(1)),
2384        };
2385        let resp = solana_relayer.rpc(req).await.unwrap();
2386
2387        assert!(resp.error.is_none(), "error should be None");
2388        let data = resp.result.unwrap();
2389        let sol_res = match data {
2390            NetworkRpcResult::Solana(inner) => inner,
2391            other => panic!("expected Solana, got {other:?}"),
2392        };
2393        let features = match sol_res {
2394            SolanaRpcResult::GetFeaturesEnabled(f) => f,
2395            other => panic!("expected GetFeaturesEnabled, got {other:?}"),
2396        };
2397        assert_eq!(features.features, vec!["gasless".to_string()]);
2398    }
2399
2400    #[tokio::test]
2401    async fn test_initialize_relayer_disables_when_validation_fails() {
2402        let mut raw_provider = MockSolanaProviderTrait::new();
2403        let mut mock_repo = MockRelayerRepository::new();
2404        let mut job_producer = MockJobProducerTrait::new();
2405
2406        let mut relayer_model = create_test_relayer();
2407        relayer_model.system_disabled = false; // Start as enabled
2408        relayer_model.notification_id = Some("test-notification-id".to_string());
2409
2410        // Mock validation failure - RPC validation fails
2411        raw_provider.expect_get_latest_blockhash().returning(|| {
2412            Box::pin(async { Err(SolanaProviderError::RpcError("RPC error".to_string())) })
2413        });
2414
2415        raw_provider
2416            .expect_get_balance()
2417            .returning(|_| Box::pin(async { Ok(1000000u64) })); // Sufficient balance
2418
2419        // Mock disable_relayer call
2420        let mut disabled_relayer = relayer_model.clone();
2421        disabled_relayer.system_disabled = true;
2422        mock_repo
2423            .expect_disable_relayer()
2424            .with(eq("test-relayer-id".to_string()), always())
2425            .returning(move |_, _| Ok(disabled_relayer.clone()));
2426
2427        // Mock notification job production
2428        job_producer
2429            .expect_produce_send_notification_job()
2430            .returning(|_, _| Box::pin(async { Ok(()) }));
2431
2432        // Mock health check job scheduling
2433        job_producer
2434            .expect_produce_relayer_health_check_job()
2435            .returning(|_, _| Box::pin(async { Ok(()) }));
2436
2437        let ctx = TestCtx {
2438            relayer_model,
2439            mock_repo,
2440            provider: Arc::new(raw_provider),
2441            job_producer: Arc::new(job_producer),
2442            ..Default::default()
2443        };
2444
2445        let solana_relayer = ctx.into_relayer().await;
2446        let result = solana_relayer.initialize_relayer().await;
2447        assert!(result.is_ok());
2448    }
2449
2450    #[tokio::test]
2451    async fn test_initialize_relayer_enables_when_validation_passes_and_was_disabled() {
2452        let mut raw_provider = MockSolanaProviderTrait::new();
2453        let mut mock_repo = MockRelayerRepository::new();
2454
2455        let mut relayer_model = create_test_relayer();
2456        relayer_model.system_disabled = true; // Start as disabled
2457
2458        // Mock successful validations
2459        raw_provider
2460            .expect_get_latest_blockhash()
2461            .returning(|| Box::pin(async { Ok(Hash::new_unique()) }));
2462
2463        raw_provider
2464            .expect_get_balance()
2465            .returning(|_| Box::pin(async { Ok(1000000u64) })); // Sufficient balance
2466
2467        // Mock enable_relayer call
2468        let mut enabled_relayer = relayer_model.clone();
2469        enabled_relayer.system_disabled = false;
2470        mock_repo
2471            .expect_enable_relayer()
2472            .with(eq("test-relayer-id".to_string()))
2473            .returning(move |_| Ok(enabled_relayer.clone()));
2474
2475        // Mock any potential disable_relayer calls (even though they shouldn't happen)
2476        let mut disabled_relayer = relayer_model.clone();
2477        disabled_relayer.system_disabled = true;
2478        mock_repo
2479            .expect_disable_relayer()
2480            .returning(move |_, _| Ok(disabled_relayer.clone()));
2481
2482        let ctx = TestCtx {
2483            relayer_model,
2484            mock_repo,
2485            provider: Arc::new(raw_provider),
2486            ..Default::default()
2487        };
2488
2489        let solana_relayer = ctx.into_relayer().await;
2490        let result = solana_relayer.initialize_relayer().await;
2491        assert!(result.is_ok());
2492    }
2493
2494    #[tokio::test]
2495    async fn test_initialize_relayer_no_action_when_enabled_and_validation_passes() {
2496        let mut raw_provider = MockSolanaProviderTrait::new();
2497        let mock_repo = MockRelayerRepository::new();
2498
2499        let mut relayer_model = create_test_relayer();
2500        relayer_model.system_disabled = false; // Start as enabled
2501
2502        // Mock successful validations
2503        raw_provider
2504            .expect_get_latest_blockhash()
2505            .returning(|| Box::pin(async { Ok(Hash::new_unique()) }));
2506
2507        raw_provider
2508            .expect_get_balance()
2509            .returning(|_| Box::pin(async { Ok(1000000u64) })); // Sufficient balance
2510
2511        let ctx = TestCtx {
2512            relayer_model,
2513            mock_repo,
2514            provider: Arc::new(raw_provider),
2515            ..Default::default()
2516        };
2517
2518        let solana_relayer = ctx.into_relayer().await;
2519        let result = solana_relayer.initialize_relayer().await;
2520        assert!(result.is_ok());
2521    }
2522
2523    #[tokio::test]
2524    async fn test_initialize_relayer_sends_notification_when_disabled() {
2525        let mut raw_provider = MockSolanaProviderTrait::new();
2526        let mut mock_repo = MockRelayerRepository::new();
2527        let mut job_producer = MockJobProducerTrait::new();
2528
2529        let mut relayer_model = create_test_relayer();
2530        relayer_model.system_disabled = false; // Start as enabled
2531        relayer_model.notification_id = Some("test-notification-id".to_string());
2532
2533        // Mock validation failure - balance check fails
2534        raw_provider
2535            .expect_get_latest_blockhash()
2536            .returning(|| Box::pin(async { Ok(Hash::new_unique()) }));
2537
2538        raw_provider
2539            .expect_get_balance()
2540            .returning(|_| Box::pin(async { Ok(100u64) })); // Insufficient balance
2541
2542        // Mock disable_relayer call
2543        let mut disabled_relayer = relayer_model.clone();
2544        disabled_relayer.system_disabled = true;
2545        mock_repo
2546            .expect_disable_relayer()
2547            .with(eq("test-relayer-id".to_string()), always())
2548            .returning(move |_, _| Ok(disabled_relayer.clone()));
2549
2550        // Mock notification job production - verify it's called
2551        job_producer
2552            .expect_produce_send_notification_job()
2553            .returning(|_, _| Box::pin(async { Ok(()) }));
2554
2555        // Mock health check job scheduling
2556        job_producer
2557            .expect_produce_relayer_health_check_job()
2558            .returning(|_, _| Box::pin(async { Ok(()) }));
2559
2560        let ctx = TestCtx {
2561            relayer_model,
2562            mock_repo,
2563            provider: Arc::new(raw_provider),
2564            job_producer: Arc::new(job_producer),
2565            ..Default::default()
2566        };
2567
2568        let solana_relayer = ctx.into_relayer().await;
2569        let result = solana_relayer.initialize_relayer().await;
2570        assert!(result.is_ok());
2571    }
2572
2573    #[tokio::test]
2574    async fn test_initialize_relayer_no_notification_when_no_notification_id() {
2575        let mut raw_provider = MockSolanaProviderTrait::new();
2576        let mut mock_repo = MockRelayerRepository::new();
2577
2578        let mut relayer_model = create_test_relayer();
2579        relayer_model.system_disabled = false; // Start as enabled
2580        relayer_model.notification_id = None; // No notification ID
2581
2582        // Mock validation failure - RPC validation fails
2583        raw_provider.expect_get_latest_blockhash().returning(|| {
2584            Box::pin(async {
2585                Err(SolanaProviderError::RpcError(
2586                    "RPC validation failed".to_string(),
2587                ))
2588            })
2589        });
2590
2591        raw_provider
2592            .expect_get_balance()
2593            .returning(|_| Box::pin(async { Ok(1000000u64) })); // Sufficient balance
2594
2595        // Mock disable_relayer call
2596        let mut disabled_relayer = relayer_model.clone();
2597        disabled_relayer.system_disabled = true;
2598        mock_repo
2599            .expect_disable_relayer()
2600            .with(eq("test-relayer-id".to_string()), always())
2601            .returning(move |_, _| Ok(disabled_relayer.clone()));
2602
2603        // No notification job should be produced since notification_id is None
2604        // But health check job should still be scheduled
2605        let mut job_producer = MockJobProducerTrait::new();
2606        job_producer
2607            .expect_produce_relayer_health_check_job()
2608            .returning(|_, _| Box::pin(async { Ok(()) }));
2609
2610        let ctx = TestCtx {
2611            relayer_model,
2612            mock_repo,
2613            provider: Arc::new(raw_provider),
2614            job_producer: Arc::new(job_producer),
2615            ..Default::default()
2616        };
2617
2618        let solana_relayer = ctx.into_relayer().await;
2619        let result = solana_relayer.initialize_relayer().await;
2620        assert!(result.is_ok());
2621    }
2622
2623    #[tokio::test]
2624    async fn test_initialize_relayer_policy_validation_fails() {
2625        let mut raw_provider = MockSolanaProviderTrait::new();
2626
2627        let mut relayer_model = create_test_relayer();
2628        relayer_model.system_disabled = false;
2629
2630        // Set up a policy that will cause validation to fail
2631        relayer_model.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
2632            allowed_tokens: Some(vec![SolanaAllowedTokensPolicy {
2633                mint: "InvalidMintAddress".to_string(),
2634                decimals: Some(9),
2635                symbol: Some("INVALID".to_string()),
2636                max_allowed_fee: Some(0),
2637                swap_config: None,
2638            }]),
2639            ..Default::default()
2640        });
2641
2642        // Mock provider calls that might be made during token validation
2643        raw_provider
2644            .expect_get_token_metadata_from_pubkey()
2645            .returning(|_| {
2646                Box::pin(async {
2647                    Err(SolanaProviderError::RpcError("Token not found".to_string()))
2648                })
2649            });
2650
2651        let ctx = TestCtx {
2652            relayer_model,
2653            provider: Arc::new(raw_provider),
2654            ..Default::default()
2655        };
2656
2657        let solana_relayer = ctx.into_relayer().await;
2658        let result = solana_relayer.initialize_relayer().await;
2659
2660        // Should fail due to policy validation error
2661        assert!(result.is_err());
2662        match result.unwrap_err() {
2663            RelayerError::PolicyConfigurationError(msg) => {
2664                assert!(msg.contains("Error while processing allowed tokens policy"));
2665            }
2666            other => panic!("Expected PolicyConfigurationError, got {other:?}"),
2667        }
2668    }
2669
2670    #[tokio::test]
2671    async fn test_sign_transaction_success() {
2672        let signer = MockSolanaSignTrait::new();
2673
2674        let relayer_model = RelayerRepoModel {
2675            id: "test-relayer-id".to_string(),
2676            address: "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin".to_string(),
2677            network: "devnet".to_string(),
2678            policies: RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
2679                fee_payment_strategy: Some(SolanaFeePaymentStrategy::Relayer),
2680                min_balance: Some(0),
2681                ..Default::default()
2682            }),
2683            ..Default::default()
2684        };
2685
2686        let ctx = TestCtx {
2687            relayer_model,
2688            signer: Arc::new(signer),
2689            ..Default::default()
2690        };
2691
2692        let solana_relayer = ctx.into_relayer().await;
2693
2694        let sign_request = SignTransactionRequest::Solana(SignTransactionRequestSolana {
2695            transaction: EncodedSerializedTransaction::new("raw_transaction_data".to_string()),
2696        });
2697
2698        let result = solana_relayer.sign_transaction(&sign_request).await;
2699        assert!(result.is_ok());
2700        let response = result.unwrap();
2701        match response {
2702            SignTransactionExternalResponse::Solana(solana_resp) => {
2703                assert_eq!(
2704                    solana_resp.transaction.into_inner(),
2705                    "signed_transaction_data"
2706                );
2707                assert_eq!(solana_resp.signature, "signature_data");
2708            }
2709            _ => panic!("Expected Solana response"),
2710        }
2711    }
2712
2713    #[tokio::test]
2714    async fn test_get_status_success() {
2715        let mut raw_provider = MockSolanaProviderTrait::new();
2716        let mut tx_repo = MockTransactionRepository::new();
2717
2718        // Mock balance retrieval
2719        raw_provider
2720            .expect_get_balance()
2721            .returning(|_| Box::pin(async { Ok(1000000) }));
2722
2723        // Mock count_by_status for pending transactions count
2724        tx_repo
2725            .expect_count_by_status()
2726            .with(
2727                eq("test-id"),
2728                eq(vec![
2729                    TransactionStatus::Pending,
2730                    TransactionStatus::Sent,
2731                    TransactionStatus::Submitted,
2732                ]),
2733            )
2734            .returning(|_, _| Ok(2u64));
2735
2736        // Mock find_by_status_paginated for latest confirmed transaction
2737        let recent_tx = TransactionRepoModel {
2738            id: "recent-tx".to_string(),
2739            relayer_id: "test-id".to_string(),
2740            network_data: NetworkTransactionData::Solana(SolanaTransactionData::default()),
2741            network_type: NetworkType::Solana,
2742            status: TransactionStatus::Confirmed,
2743            confirmed_at: Some(Utc::now().to_string()),
2744            ..Default::default()
2745        };
2746        tx_repo
2747            .expect_find_by_status_paginated()
2748            .withf(|relayer_id, statuses, query, oldest_first| {
2749                *relayer_id == *"test-id"
2750                    && statuses == [TransactionStatus::Confirmed]
2751                    && query.page == 1
2752                    && query.per_page == 1
2753                    && !(*oldest_first)
2754            })
2755            .returning(move |_, _, _, _| {
2756                Ok(crate::repositories::PaginatedResult {
2757                    items: vec![recent_tx.clone()],
2758                    total: 1,
2759                    page: 1,
2760                    per_page: 1,
2761                })
2762            });
2763
2764        let ctx = TestCtx {
2765            tx_repo: Arc::new(tx_repo),
2766            provider: Arc::new(raw_provider),
2767            ..Default::default()
2768        };
2769
2770        let solana_relayer = ctx.into_relayer().await;
2771
2772        let result = solana_relayer.get_status().await;
2773        assert!(result.is_ok());
2774        let status = result.unwrap();
2775
2776        match status {
2777            RelayerStatus::Solana {
2778                balance,
2779                pending_transactions_count,
2780                last_confirmed_transaction_timestamp,
2781                ..
2782            } => {
2783                assert_eq!(balance, "1000000");
2784                assert_eq!(pending_transactions_count, 2);
2785                assert!(last_confirmed_transaction_timestamp.is_some());
2786            }
2787            _ => panic!("Expected Solana status"),
2788        }
2789    }
2790
2791    #[tokio::test]
2792    async fn test_get_status_balance_error() {
2793        let mut raw_provider = MockSolanaProviderTrait::new();
2794        let tx_repo = MockTransactionRepository::new();
2795
2796        // Mock balance error
2797        raw_provider.expect_get_balance().returning(|_| {
2798            Box::pin(async { Err(SolanaProviderError::RpcError("RPC error".to_string())) })
2799        });
2800
2801        let ctx = TestCtx {
2802            tx_repo: Arc::new(tx_repo),
2803            provider: Arc::new(raw_provider),
2804            ..Default::default()
2805        };
2806
2807        let solana_relayer = ctx.into_relayer().await;
2808
2809        let result = solana_relayer.get_status().await;
2810        assert!(result.is_err());
2811        match result.unwrap_err() {
2812            RelayerError::UnderlyingSolanaProvider(err) => {
2813                assert!(err.to_string().contains("RPC error"));
2814            }
2815            other => panic!("Expected UnderlyingSolanaProvider, got {other:?}"),
2816        }
2817    }
2818
2819    #[tokio::test]
2820    async fn test_get_status_no_recent_transactions() {
2821        let mut raw_provider = MockSolanaProviderTrait::new();
2822        let mut tx_repo = MockTransactionRepository::new();
2823
2824        // Mock balance retrieval
2825        raw_provider
2826            .expect_get_balance()
2827            .returning(|_| Box::pin(async { Ok(500000) }));
2828
2829        // Mock count_by_status for pending transactions count
2830        tx_repo
2831            .expect_count_by_status()
2832            .with(
2833                eq("test-id"),
2834                eq(vec![
2835                    TransactionStatus::Pending,
2836                    TransactionStatus::Sent,
2837                    TransactionStatus::Submitted,
2838                ]),
2839            )
2840            .returning(|_, _| Ok(0u64));
2841
2842        // Mock find_by_status_paginated for latest confirmed transaction (none)
2843        tx_repo
2844            .expect_find_by_status_paginated()
2845            .withf(|relayer_id, statuses, query, oldest_first| {
2846                *relayer_id == *"test-id"
2847                    && statuses == [TransactionStatus::Confirmed]
2848                    && query.page == 1
2849                    && query.per_page == 1
2850                    && !(*oldest_first)
2851            })
2852            .returning(|_, _, _, _| {
2853                Ok(crate::repositories::PaginatedResult {
2854                    items: vec![],
2855                    total: 0,
2856                    page: 1,
2857                    per_page: 1,
2858                })
2859            });
2860
2861        let ctx = TestCtx {
2862            tx_repo: Arc::new(tx_repo),
2863            provider: Arc::new(raw_provider),
2864            ..Default::default()
2865        };
2866
2867        let solana_relayer = ctx.into_relayer().await;
2868
2869        let result = solana_relayer.get_status().await;
2870        assert!(result.is_ok());
2871        let status = result.unwrap();
2872
2873        match status {
2874            RelayerStatus::Solana {
2875                balance,
2876                pending_transactions_count,
2877                last_confirmed_transaction_timestamp,
2878                ..
2879            } => {
2880                assert_eq!(balance, "500000");
2881                assert_eq!(pending_transactions_count, 0);
2882                assert!(last_confirmed_transaction_timestamp.is_none());
2883            }
2884            _ => panic!("Expected Solana status"),
2885        }
2886    }
2887
2888    // GasAbstractionTrait tests
2889    // These are passthrough methods to RPC handlers, so we verify:
2890    // 1. Wrong network type returns ValidationError
2891    // The actual RPC handler functionality (including method calls) is tested in the RPC handler tests
2892    // Note: We can't easily mock the RPC handler here due to type constraints in TestCtx,
2893    // but the passthrough behavior is verified through the RPC handler tests.
2894
2895    #[tokio::test]
2896    async fn test_quote_sponsored_transaction_wrong_network() {
2897        let ctx = TestCtx::default();
2898        let solana_relayer = ctx.into_relayer().await;
2899
2900        // Use Stellar request instead of Solana
2901        let request = SponsoredTransactionQuoteRequest::Stellar(
2902            crate::models::StellarFeeEstimateRequestParams {
2903                transaction_xdr: Some("test-xdr".to_string()),
2904                operations: None,
2905                source_account: None,
2906                fee_token: "native".to_string(),
2907            },
2908        );
2909
2910        let result = solana_relayer.quote_sponsored_transaction(request).await;
2911        assert!(result.is_err());
2912
2913        if let Err(RelayerError::ValidationError(msg)) = result {
2914            assert!(msg.contains("Expected Solana fee estimate request parameters"));
2915        } else {
2916            panic!("Expected ValidationError for wrong network type");
2917        }
2918    }
2919
2920    #[tokio::test]
2921    async fn test_build_sponsored_transaction_wrong_network() {
2922        let ctx = TestCtx::default();
2923        let solana_relayer = ctx.into_relayer().await;
2924
2925        // Use Stellar request instead of Solana
2926        let request = SponsoredTransactionBuildRequest::Stellar(
2927            crate::models::StellarPrepareTransactionRequestParams {
2928                transaction_xdr: Some("test-xdr".to_string()),
2929                operations: None,
2930                source_account: None,
2931                fee_token: "native".to_string(),
2932            },
2933        );
2934
2935        let result = solana_relayer.build_sponsored_transaction(request).await;
2936        assert!(result.is_err());
2937
2938        if let Err(RelayerError::ValidationError(msg)) = result {
2939            assert!(msg.contains("Expected Solana prepare transaction request parameters"));
2940        } else {
2941            panic!("Expected ValidationError for wrong network type");
2942        }
2943    }
2944
2945    #[tokio::test]
2946    async fn test_process_transaction_request_status_check_failure_returns_error() {
2947        let relayer_model = RelayerRepoModel {
2948            id: "test-relayer-id".to_string(),
2949            address: "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin".to_string(),
2950            network: "devnet".to_string(),
2951            network_type: NetworkType::Solana,
2952            policies: RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
2953                fee_payment_strategy: Some(SolanaFeePaymentStrategy::Relayer),
2954                min_balance: Some(0),
2955                ..Default::default()
2956            }),
2957            ..Default::default()
2958        };
2959
2960        let network_tx =
2961            NetworkTransactionRequest::Solana(crate::models::SolanaTransactionRequest {
2962                transaction: Some(EncodedSerializedTransaction::new(
2963                    "test_transaction".to_string(),
2964                )),
2965                instructions: None,
2966                valid_until: None,
2967            });
2968
2969        let mut tx_repo = MockTransactionRepository::new();
2970        tx_repo.expect_create().returning(|t| Ok(t.clone()));
2971        // When status check fails, transaction is marked as failed
2972        tx_repo
2973            .expect_partial_update()
2974            .returning(|_, _| Ok(TransactionRepoModel::default()));
2975
2976        let mut job_producer = MockJobProducerTrait::new();
2977
2978        // Status check fails
2979        job_producer
2980            .expect_produce_check_transaction_status_job()
2981            .returning(|_, _| {
2982                Box::pin(async {
2983                    Err(crate::jobs::JobProducerError::QueueError(
2984                        "Failed to queue job".to_string(),
2985                    ))
2986                })
2987            });
2988
2989        // Transaction request should NOT be called when status check fails
2990        // (no expectation set = test fails if called)
2991
2992        let ctx = TestCtx {
2993            relayer_model,
2994            tx_repo: Arc::new(tx_repo),
2995            job_producer: Arc::new(job_producer),
2996            ..Default::default()
2997        };
2998        let solana_relayer = ctx.into_relayer().await;
2999
3000        let result = solana_relayer.process_transaction_request(network_tx).await;
3001        assert!(result.is_err());
3002    }
3003
3004    #[tokio::test]
3005    async fn test_process_transaction_request_status_check_failure_marks_tx_failed() {
3006        let relayer_model = RelayerRepoModel {
3007            id: "test-relayer-id".to_string(),
3008            address: "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin".to_string(),
3009            network: "devnet".to_string(),
3010            network_type: NetworkType::Solana,
3011            policies: RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
3012                fee_payment_strategy: Some(SolanaFeePaymentStrategy::Relayer),
3013                min_balance: Some(0),
3014                ..Default::default()
3015            }),
3016            ..Default::default()
3017        };
3018
3019        let network_tx =
3020            NetworkTransactionRequest::Solana(crate::models::SolanaTransactionRequest {
3021                transaction: Some(EncodedSerializedTransaction::new(
3022                    "test_transaction".to_string(),
3023                )),
3024                instructions: None,
3025                valid_until: None,
3026            });
3027
3028        let mut tx_repo = MockTransactionRepository::new();
3029        tx_repo.expect_create().returning(|t| Ok(t.clone()));
3030
3031        // Verify partial_update is called with correct status and reason
3032        tx_repo
3033            .expect_partial_update()
3034            .withf(|_tx_id, update| {
3035                update.status == Some(TransactionStatus::Failed)
3036                    && update.status_reason == Some("Queue unavailable".to_string())
3037            })
3038            .returning(|_, _| Ok(TransactionRepoModel::default()));
3039
3040        let mut job_producer = MockJobProducerTrait::new();
3041        job_producer
3042            .expect_produce_check_transaction_status_job()
3043            .returning(|_, _| {
3044                Box::pin(async {
3045                    Err(crate::jobs::JobProducerError::QueueError(
3046                        "Redis timeout".to_string(),
3047                    ))
3048                })
3049            });
3050
3051        let ctx = TestCtx {
3052            relayer_model,
3053            tx_repo: Arc::new(tx_repo),
3054            job_producer: Arc::new(job_producer),
3055            ..Default::default()
3056        };
3057        let solana_relayer = ctx.into_relayer().await;
3058
3059        let result = solana_relayer.process_transaction_request(network_tx).await;
3060        assert!(result.is_err());
3061        // The mock verification (withf) ensures partial_update was called correctly
3062    }
3063}