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,
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            self.job_producer
660                .produce_transaction_request_job(
661                    TransactionRequest::new(transaction.id.clone(), transaction.relayer_id.clone()),
662                    None,
663                )
664                .await?;
665
666            // Queue status check job (with initial delay)
667            self.job_producer
668                .produce_check_transaction_status_job(
669                    TransactionStatusCheck::new(
670                        transaction.id.clone(),
671                        transaction.relayer_id.clone(),
672                        NetworkType::Solana,
673                    ),
674                    Some(calculate_scheduled_timestamp(
675                        SOLANA_STATUS_CHECK_INITIAL_DELAY_SECONDS,
676                    )),
677                )
678                .await?;
679
680            Ok(transaction)
681        }
682    }
683
684    #[instrument(
685        level = "debug",
686        skip(self),
687        fields(
688            request_id = ?crate::observability::request_id::get_request_id(),
689            relayer_id = %self.relayer.id,
690        )
691    )]
692    async fn get_balance(&self) -> Result<BalanceResponse, RelayerError> {
693        let address = &self.relayer.address;
694        let balance = self.provider.get_balance(address).await?;
695
696        Ok(BalanceResponse {
697            balance: balance as u128,
698            unit: SOLANA_SMALLEST_UNIT_NAME.to_string(),
699        })
700    }
701
702    #[instrument(
703        level = "debug",
704        skip(self),
705        fields(
706            request_id = ?crate::observability::request_id::get_request_id(),
707            relayer_id = %self.relayer.id,
708        )
709    )]
710    async fn delete_pending_transactions(
711        &self,
712    ) -> Result<DeletePendingTransactionsResponse, RelayerError> {
713        Err(RelayerError::NotSupported(
714            "Delete pending transactions not supported for Solana relayers".to_string(),
715        ))
716    }
717
718    #[instrument(
719        level = "debug",
720        skip(self, _request),
721        fields(
722            request_id = ?crate::observability::request_id::get_request_id(),
723            relayer_id = %self.relayer.id,
724        )
725    )]
726    async fn sign_data(
727        &self,
728        _request: SignDataRequest,
729    ) -> Result<crate::domain::relayer::SignDataResponse, RelayerError> {
730        Err(RelayerError::NotSupported(
731            "Sign data not supported for Solana relayers".to_string(),
732        ))
733    }
734
735    #[instrument(
736        level = "debug",
737        skip(self, _request),
738        fields(
739            request_id = ?crate::observability::request_id::get_request_id(),
740            relayer_id = %self.relayer.id,
741        )
742    )]
743    async fn sign_typed_data(
744        &self,
745        _request: SignTypedDataRequest,
746    ) -> Result<crate::domain::relayer::SignDataResponse, RelayerError> {
747        Err(RelayerError::NotSupported(
748            "Sign typed data 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_transaction(
761        &self,
762        request: &SignTransactionRequest,
763    ) -> Result<SignTransactionExternalResponse, RelayerError> {
764        let policy = self.relayer.policies.get_solana_policy();
765        let user_pays_fee = matches!(
766            policy.fee_payment_strategy.unwrap_or_default(),
767            SolanaFeePaymentStrategy::User
768        );
769
770        // For user-paid fees, delegate to RPC handler (similar to process_transaction_request)
771        if user_pays_fee {
772            let solana_request = match request {
773                SignTransactionRequest::Solana(req) => req,
774                _ => {
775                    error!(
776                        id = %self.relayer.id,
777                        "Invalid request type for Solana relayer",
778                    );
779                    return Err(RelayerError::NotSupported(
780                        "Invalid request type for Solana relayer".to_string(),
781                    ));
782                }
783            };
784
785            let params = SolanaSignTransactionRequestParams {
786                transaction: solana_request.transaction.clone(),
787            };
788
789            let result = self
790                .rpc_handler
791                .rpc_methods()
792                .sign_transaction(params)
793                .await
794                .map_err(|e| RelayerError::Internal(e.to_string()))?;
795
796            Ok(SignTransactionExternalResponse::Solana(
797                SignTransactionResponseSolana {
798                    transaction: result.transaction,
799                    signature: result.signature,
800                },
801            ))
802        } else {
803            // Relayer-paid fees: use the original flow
804            let transaction_bytes = match request {
805                SignTransactionRequest::Solana(req) => &req.transaction,
806                _ => {
807                    error!(
808                        id = %self.relayer.id,
809                        "Invalid request type for Solana relayer",
810                    );
811                    return Err(RelayerError::NotSupported(
812                        "Invalid request type for Solana relayer".to_string(),
813                    ));
814                }
815            };
816
817            // Prepare transaction data for signing
818            let transaction_data = NetworkTransactionData::Solana(SolanaTransactionData {
819                transaction: Some(transaction_bytes.clone().into_inner()),
820                ..Default::default()
821            });
822
823            // Sign the transaction using the signer trait
824            let response = self
825                .signer
826                .sign_transaction(transaction_data)
827                .await
828                .map_err(|e| {
829                    error!(
830                        %e,
831                        id = %self.relayer.id,
832                        "Failed to sign transaction",
833                    );
834                    RelayerError::SignerError(e)
835                })?;
836
837            // Extract Solana-specific response
838            let solana_response = match response {
839                SignTransactionResponse::Solana(resp) => resp,
840                _ => {
841                    return Err(RelayerError::ProviderError(
842                        "Unexpected response type from Solana signer".to_string(),
843                    ))
844                }
845            };
846
847            Ok(SignTransactionExternalResponse::Solana(solana_response))
848        }
849    }
850
851    #[instrument(
852        level = "debug",
853        skip(self, request),
854        fields(
855            request_id = ?crate::observability::request_id::get_request_id(),
856            relayer_id = %self.relayer.id,
857        )
858    )]
859    async fn rpc(
860        &self,
861        request: JsonRpcRequest<NetworkRpcRequest>,
862    ) -> Result<JsonRpcResponse<NetworkRpcResult>, RelayerError> {
863        let JsonRpcRequest {
864            jsonrpc: _,
865            id,
866            params,
867        } = request;
868        let solana_request = match params {
869            NetworkRpcRequest::Solana(sol_req) => sol_req,
870            _ => {
871                return Ok(create_error_response(
872                    id.clone(),
873                    RpcErrorCodes::INVALID_PARAMS,
874                    "Invalid params",
875                    "Expected Solana network request",
876                ))
877            }
878        };
879
880        match solana_request {
881            SolanaRpcRequest::RawRpcRequest { method, params } => {
882                // Handle raw JSON-RPC requests by forwarding to provider
883                let response = self.provider.raw_request_dyn(&method, params).await?;
884
885                Ok(JsonRpcResponse {
886                    jsonrpc: "2.0".to_string(),
887                    result: Some(NetworkRpcResult::Solana(SolanaRpcResult::RawRpc(response))),
888                    error: None,
889                    id: id.clone(),
890                })
891            }
892            _ => {
893                // Handle typed requests using the existing rpc_handler
894                let response = self
895                    .rpc_handler
896                    .handle_request(JsonRpcRequest {
897                        jsonrpc: request.jsonrpc,
898                        params: NetworkRpcRequest::Solana(solana_request),
899                        id: id.clone(),
900                    })
901                    .await;
902
903                match response {
904                    Ok(response) => Ok(response),
905                    Err(e) => {
906                        error!(error = %e, "error while processing RPC request");
907                        let error_response = match e {
908                            SolanaRpcError::UnsupportedMethod(msg) => {
909                                JsonRpcResponse::error(32000, "UNSUPPORTED_METHOD", &msg)
910                            }
911                            SolanaRpcError::FeatureFetch(msg) => JsonRpcResponse::error(
912                                -32008,
913                                "FEATURE_FETCH_ERROR",
914                                &format!("Failed to retrieve the list of enabled features: {msg}"),
915                            ),
916                            SolanaRpcError::InvalidParams(msg) => {
917                                JsonRpcResponse::error(-32602, "INVALID_PARAMS", &msg)
918                            }
919                            SolanaRpcError::UnsupportedFeeToken(msg) => JsonRpcResponse::error(
920                                -32000,
921                                "UNSUPPORTED_FEE_TOKEN",
922                                &format!(
923                                    "The provided fee_token is not supported by the relayer: {msg}"
924                                ),
925                            ),
926                            SolanaRpcError::Estimation(msg) => JsonRpcResponse::error(
927                                -32001,
928                                "ESTIMATION_ERROR",
929                                &format!(
930                                    "Failed to estimate the fee due to internal or network issues: {msg}"
931                                ),
932                            ),
933                            SolanaRpcError::InsufficientFunds(msg) => {
934                                // Trigger a token swap request if the relayer has insufficient funds
935                                self.check_balance_and_trigger_token_swap_if_needed()
936                                    .await?;
937
938                                JsonRpcResponse::error(
939                                    -32002,
940                                    "INSUFFICIENT_FUNDS",
941                                    &format!(
942                                        "The sender does not have enough funds for the transfer: {msg}"
943                                    ),
944                                )
945                            }
946                            SolanaRpcError::TransactionPreparation(msg) => JsonRpcResponse::error(
947                                -32003,
948                                "TRANSACTION_PREPARATION_ERROR",
949                                &format!("Failed to prepare the transfer transaction: {msg}"),
950                            ),
951                            SolanaRpcError::Preparation(msg) => JsonRpcResponse::error(
952                                -32013,
953                                "PREPARATION_ERROR",
954                                &format!("Failed to prepare the transfer transaction: {msg}"),
955                            ),
956                            SolanaRpcError::Signature(msg) => JsonRpcResponse::error(
957                                -32005,
958                                "SIGNATURE_ERROR",
959                                &format!("Failed to sign the transaction: {msg}"),
960                            ),
961                            SolanaRpcError::Signing(msg) => JsonRpcResponse::error(
962                                -32005,
963                                "SIGNATURE_ERROR",
964                                &format!("Failed to sign the transaction: {msg}"),
965                            ),
966                            SolanaRpcError::TokenFetch(msg) => JsonRpcResponse::error(
967                                -32007,
968                                "TOKEN_FETCH_ERROR",
969                                &format!("Failed to retrieve the list of supported tokens: {msg}"),
970                            ),
971                            SolanaRpcError::BadRequest(msg) => JsonRpcResponse::error(
972                                -32007,
973                                "BAD_REQUEST",
974                                &format!("Bad request: {msg}"),
975                            ),
976                            SolanaRpcError::Send(msg) => JsonRpcResponse::error(
977                                -32006,
978                                "SEND_ERROR",
979                                &format!(
980                                    "Failed to submit the transaction to the blockchain: {msg}"
981                                ),
982                            ),
983                            SolanaRpcError::SolanaTransactionValidation(msg) => JsonRpcResponse::error(
984                                -32013,
985                                "PREPARATION_ERROR",
986                                &format!("Failed to prepare the transfer transaction: {msg}"),
987                            ),
988                            SolanaRpcError::Encoding(msg) => JsonRpcResponse::error(
989                                -32601,
990                                "INVALID_PARAMS",
991                                &format!("The transaction parameter is invalid or missing: {msg}"),
992                            ),
993                            SolanaRpcError::TokenAccount(msg) => JsonRpcResponse::error(
994                                -32601,
995                                "PREPARATION_ERROR",
996                                &format!("Invalid Token Account: {msg}"),
997                            ),
998                            SolanaRpcError::Token(msg) => JsonRpcResponse::error(
999                                -32601,
1000                                "PREPARATION_ERROR",
1001                                &format!("Invalid Token Account: {msg}"),
1002                            ),
1003                            SolanaRpcError::Provider(msg) => JsonRpcResponse::error(
1004                                -32006,
1005                                "PREPARATION_ERROR",
1006                                &format!("Failed to prepare the transfer transaction: {msg}"),
1007                            ),
1008                            SolanaRpcError::Internal(_) => {
1009                                JsonRpcResponse::error(-32000, "INTERNAL_ERROR", "Internal error")
1010                            }
1011                        };
1012                        Ok(error_response)
1013                    }
1014                }
1015            }
1016        }
1017    }
1018
1019    #[instrument(
1020        level = "debug",
1021        skip(self),
1022        fields(
1023            request_id = ?crate::observability::request_id::get_request_id(),
1024            relayer_id = %self.relayer.id,
1025        )
1026    )]
1027    async fn get_status(&self) -> Result<RelayerStatus, RelayerError> {
1028        let address = &self.relayer.address;
1029        let balance = self.provider.get_balance(address).await?;
1030
1031        // Use optimized count_by_status
1032        let pending_transactions_count = self
1033            .transaction_repository
1034            .count_by_status(&self.relayer.id, PENDING_TRANSACTION_STATUSES)
1035            .await
1036            .map_err(RelayerError::from)?;
1037
1038        // Use find_by_status_paginated to get the latest confirmed transaction (newest first)
1039        let last_confirmed_transaction_timestamp = self
1040            .transaction_repository
1041            .find_by_status_paginated(
1042                &self.relayer.id,
1043                &[TransactionStatus::Confirmed],
1044                PaginationQuery {
1045                    page: 1,
1046                    per_page: 1,
1047                },
1048                false, // oldest_first = false means newest first
1049            )
1050            .await
1051            .map_err(RelayerError::from)?
1052            .items
1053            .into_iter()
1054            .next()
1055            .and_then(|tx| tx.confirmed_at);
1056
1057        Ok(RelayerStatus::Solana {
1058            balance: (balance as u128).to_string(),
1059            pending_transactions_count,
1060            last_confirmed_transaction_timestamp,
1061            system_disabled: self.relayer.system_disabled,
1062            paused: self.relayer.paused,
1063        })
1064    }
1065
1066    #[instrument(
1067        level = "debug",
1068        skip(self),
1069        fields(
1070            request_id = ?crate::observability::request_id::get_request_id(),
1071            relayer_id = %self.relayer.id,
1072        )
1073    )]
1074    async fn initialize_relayer(&self) -> Result<(), RelayerError> {
1075        debug!("initializing Solana relayer");
1076
1077        // Populate model with allowed token metadata and update DB entry
1078        // Error will be thrown if any of the tokens are not found
1079        self.populate_allowed_tokens_metadata().await.map_err(|_| {
1080            RelayerError::PolicyConfigurationError(
1081                "Error while processing allowed tokens policy".into(),
1082            )
1083        })?;
1084
1085        // Validate relayer allowed programs policy
1086        // Error will be thrown if any of the programs are not executable
1087        self.validate_program_policy().await.map_err(|_| {
1088            RelayerError::PolicyConfigurationError(
1089                "Error while validating allowed programs policy".into(),
1090            )
1091        })?;
1092
1093        match self.check_health().await {
1094            Ok(_) => {
1095                // All checks passed
1096                if self.relayer.system_disabled {
1097                    // Silently re-enable if was disabled (startup, not recovery)
1098                    self.relayer_repository
1099                        .enable_relayer(self.relayer.id.clone())
1100                        .await?;
1101                }
1102            }
1103            Err(failures) => {
1104                // Health checks failed
1105                let reason = DisabledReason::from_health_failures(failures).unwrap_or_else(|| {
1106                    DisabledReason::RpcValidationFailed("Unknown error".to_string())
1107                });
1108
1109                warn!(reason = %reason, "disabling relayer");
1110                let updated_relayer = self
1111                    .relayer_repository
1112                    .disable_relayer(self.relayer.id.clone(), reason.clone())
1113                    .await?;
1114
1115                // Send notification if configured
1116                if let Some(notification_id) = &self.relayer.notification_id {
1117                    self.job_producer
1118                        .produce_send_notification_job(
1119                            produce_relayer_disabled_payload(
1120                                notification_id,
1121                                &updated_relayer,
1122                                &reason.safe_description(),
1123                            ),
1124                            None,
1125                        )
1126                        .await?;
1127                }
1128
1129                // Schedule health check to try re-enabling the relayer after 10 seconds
1130                self.job_producer
1131                    .produce_relayer_health_check_job(
1132                        RelayerHealthCheck::new(self.relayer.id.clone()),
1133                        Some(calculate_scheduled_timestamp(10)),
1134                    )
1135                    .await?;
1136            }
1137        }
1138
1139        self.check_balance_and_trigger_token_swap_if_needed()
1140            .await?;
1141
1142        Ok(())
1143    }
1144
1145    #[instrument(
1146        level = "debug",
1147        skip(self),
1148        fields(
1149            request_id = ?crate::observability::request_id::get_request_id(),
1150            relayer_id = %self.relayer.id,
1151        )
1152    )]
1153    async fn check_health(&self) -> Result<(), Vec<HealthCheckFailure>> {
1154        debug!(
1155            "running health checks for Solana relayer {}",
1156            self.relayer.id
1157        );
1158
1159        let validate_rpc_result = self.validate_rpc().await;
1160        let validate_min_balance_result = self.validate_min_balance().await;
1161
1162        // Collect all failures
1163        let failures: Vec<HealthCheckFailure> = vec![
1164            validate_rpc_result
1165                .err()
1166                .map(|e| HealthCheckFailure::RpcValidationFailed(e.to_string())),
1167            validate_min_balance_result
1168                .err()
1169                .map(|e| HealthCheckFailure::BalanceCheckFailed(e.to_string())),
1170        ]
1171        .into_iter()
1172        .flatten()
1173        .collect();
1174
1175        if failures.is_empty() {
1176            info!("all health checks passed");
1177            Ok(())
1178        } else {
1179            warn!("health checks failed: {:?}", failures);
1180            Err(failures)
1181        }
1182    }
1183
1184    #[instrument(
1185        level = "debug",
1186        skip(self),
1187        fields(
1188            request_id = ?crate::observability::request_id::get_request_id(),
1189            relayer_id = %self.relayer.id,
1190        )
1191    )]
1192    async fn validate_min_balance(&self) -> Result<(), RelayerError> {
1193        let balance = self
1194            .provider
1195            .get_balance(&self.relayer.address)
1196            .await
1197            .map_err(|e| RelayerError::ProviderError(e.to_string()))?;
1198
1199        debug!(balance = %balance, "balance for relayer");
1200
1201        let policy = self.relayer.policies.get_solana_policy();
1202
1203        if balance < policy.min_balance.unwrap_or(DEFAULT_SOLANA_MIN_BALANCE) {
1204            return Err(RelayerError::InsufficientBalanceError(
1205                "Insufficient balance".to_string(),
1206            ));
1207        }
1208
1209        Ok(())
1210    }
1211}
1212
1213#[async_trait]
1214impl<RR, TR, J, S, JS, SP, NR> GasAbstractionTrait for SolanaRelayer<RR, TR, J, S, JS, SP, NR>
1215where
1216    RR: Repository<RelayerRepoModel, String> + RelayerRepository + Send + Sync + 'static,
1217    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
1218    J: JobProducerTrait + Send + Sync + 'static,
1219    S: SolanaSignTrait + Signer + Send + Sync + 'static,
1220    JS: JupiterServiceTrait + Send + Sync + 'static,
1221    SP: SolanaProviderTrait + Send + Sync + 'static,
1222    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
1223{
1224    #[instrument(
1225        level = "debug",
1226        skip(self, params),
1227        fields(
1228            request_id = ?crate::observability::request_id::get_request_id(),
1229            relayer_id = %self.relayer.id,
1230        )
1231    )]
1232    async fn quote_sponsored_transaction(
1233        &self,
1234        params: SponsoredTransactionQuoteRequest,
1235    ) -> Result<SponsoredTransactionQuoteResponse, RelayerError> {
1236        let params = match params {
1237            SponsoredTransactionQuoteRequest::Solana(p) => p,
1238            _ => {
1239                return Err(RelayerError::ValidationError(
1240                    "Expected Solana fee estimate request parameters".to_string(),
1241                ));
1242            }
1243        };
1244
1245        let result = self
1246            .rpc_handler
1247            .rpc_methods()
1248            .fee_estimate(params)
1249            .await
1250            .map_err(|e| RelayerError::Internal(e.to_string()))?;
1251
1252        Ok(SponsoredTransactionQuoteResponse::Solana(result))
1253    }
1254
1255    #[instrument(
1256        level = "debug",
1257        skip(self, params),
1258        fields(
1259            request_id = ?crate::observability::request_id::get_request_id(),
1260            relayer_id = %self.relayer.id,
1261        )
1262    )]
1263    async fn build_sponsored_transaction(
1264        &self,
1265        params: SponsoredTransactionBuildRequest,
1266    ) -> Result<SponsoredTransactionBuildResponse, RelayerError> {
1267        let params = match params {
1268            SponsoredTransactionBuildRequest::Solana(p) => p,
1269            _ => {
1270                return Err(RelayerError::ValidationError(
1271                    "Expected Solana prepare transaction request parameters".to_string(),
1272                ));
1273            }
1274        };
1275
1276        let result = self
1277            .rpc_handler
1278            .rpc_methods()
1279            .prepare_transaction(params)
1280            .await
1281            .map_err(|e| {
1282                let error_msg = format!("{e}");
1283                RelayerError::Internal(error_msg)
1284            })?;
1285
1286        Ok(SponsoredTransactionBuildResponse::Solana(result))
1287    }
1288}
1289
1290#[cfg(test)]
1291mod tests {
1292    use super::*;
1293    use crate::{
1294        config::{NetworkConfigCommon, SolanaNetworkConfig},
1295        domain::{
1296            create_network_dex_generic, Relayer, SignTransactionRequestSolana, SolanaRpcHandler,
1297            SolanaRpcMethodsImpl,
1298        },
1299        jobs::MockJobProducerTrait,
1300        models::{
1301            EncodedSerializedTransaction, JsonRpcId, NetworkConfigData, NetworkRepoModel,
1302            RelayerSolanaSwapConfig, RpcConfig, SolanaAllowedTokensSwapConfig,
1303            SolanaFeeEstimateRequestParams, SolanaGetFeaturesEnabledRequestParams, SolanaRpcResult,
1304            SolanaSwapStrategy,
1305        },
1306        repositories::{MockNetworkRepository, MockRelayerRepository, MockTransactionRepository},
1307        services::{
1308            provider::{MockSolanaProviderTrait, SolanaProviderError},
1309            signer::MockSolanaSignTrait,
1310            MockJupiterServiceTrait, QuoteResponse, RoutePlan, SwapEvents, SwapInfo, SwapResponse,
1311            UltraExecuteResponse, UltraOrderResponse,
1312        },
1313        utils::mocks::mockutils::create_mock_solana_network,
1314    };
1315    use chrono::Utc;
1316    use mockall::predicate::*;
1317    use solana_sdk::{hash::Hash, program_pack::Pack, signature::Signature};
1318    use spl_token_interface::state::Account as SplAccount;
1319
1320    /// Bundles all the pieces you need to instantiate a SolanaRelayer.
1321    /// Default::default gives you fresh mocks, but you can override any of them.
1322    #[allow(dead_code)]
1323    struct TestCtx {
1324        relayer_model: RelayerRepoModel,
1325        mock_repo: MockRelayerRepository,
1326        network_repository: Arc<MockNetworkRepository>,
1327        provider: Arc<MockSolanaProviderTrait>,
1328        signer: Arc<MockSolanaSignTrait>,
1329        jupiter: Arc<MockJupiterServiceTrait>,
1330        job_producer: Arc<MockJobProducerTrait>,
1331        tx_repo: Arc<MockTransactionRepository>,
1332        dex: Arc<NetworkDex<MockSolanaProviderTrait, MockSolanaSignTrait, MockJupiterServiceTrait>>,
1333        rpc_handler: SolanaRpcHandlerType<
1334            MockSolanaProviderTrait,
1335            MockSolanaSignTrait,
1336            MockJupiterServiceTrait,
1337            MockJobProducerTrait,
1338            MockTransactionRepository,
1339        >,
1340    }
1341
1342    impl Default for TestCtx {
1343        fn default() -> Self {
1344            let mock_repo = MockRelayerRepository::new();
1345            let provider = Arc::new(MockSolanaProviderTrait::new());
1346            let signer = Arc::new(MockSolanaSignTrait::new());
1347            let jupiter = Arc::new(MockJupiterServiceTrait::new());
1348            let job = Arc::new(MockJobProducerTrait::new());
1349            let tx_repo = Arc::new(MockTransactionRepository::new());
1350            let mut network_repository = MockNetworkRepository::new();
1351            let transaction_repository = Arc::new(MockTransactionRepository::new());
1352
1353            let relayer_model = RelayerRepoModel {
1354                id: "test-id".to_string(),
1355                address: "...".to_string(),
1356                network: "devnet".to_string(),
1357                ..Default::default()
1358            };
1359
1360            let dex = Arc::new(
1361                create_network_dex_generic(
1362                    &relayer_model,
1363                    provider.clone(),
1364                    signer.clone(),
1365                    jupiter.clone(),
1366                )
1367                .unwrap(),
1368            );
1369
1370            let test_network = create_mock_solana_network();
1371
1372            let rpc_handler = Arc::new(SolanaRpcHandler::new(SolanaRpcMethodsImpl::new_mock(
1373                relayer_model.clone(),
1374                test_network.clone(),
1375                provider.clone(),
1376                signer.clone(),
1377                jupiter.clone(),
1378                job.clone(),
1379                transaction_repository.clone(),
1380            )));
1381
1382            let test_network = NetworkRepoModel {
1383                id: "solana:devnet".to_string(),
1384                name: "devnet".to_string(),
1385                network_type: NetworkType::Solana,
1386                config: NetworkConfigData::Solana(SolanaNetworkConfig {
1387                    common: NetworkConfigCommon {
1388                        network: "devnet".to_string(),
1389                        from: None,
1390                        rpc_urls: Some(vec![RpcConfig::new(
1391                            "https://api.devnet.solana.com".to_string(),
1392                        )]),
1393                        explorer_urls: None,
1394                        average_blocktime_ms: Some(400),
1395                        is_testnet: Some(true),
1396                        tags: None,
1397                    },
1398                }),
1399            };
1400
1401            network_repository
1402                .expect_get_by_name()
1403                .returning(move |_, _| Ok(Some(test_network.clone())));
1404
1405            TestCtx {
1406                relayer_model,
1407                mock_repo,
1408                network_repository: Arc::new(network_repository),
1409                provider,
1410                signer,
1411                jupiter,
1412                job_producer: job,
1413                tx_repo,
1414                dex,
1415                rpc_handler,
1416            }
1417        }
1418    }
1419
1420    impl TestCtx {
1421        async fn into_relayer(
1422            self,
1423        ) -> SolanaRelayer<
1424            MockRelayerRepository,
1425            MockTransactionRepository,
1426            MockJobProducerTrait,
1427            MockSolanaSignTrait,
1428            MockJupiterServiceTrait,
1429            MockSolanaProviderTrait,
1430            MockNetworkRepository,
1431        > {
1432            // Get the network from the repository
1433            let network_repo = self
1434                .network_repository
1435                .get_by_name(NetworkType::Solana, "devnet")
1436                .await
1437                .unwrap()
1438                .unwrap();
1439            let network = SolanaNetwork::try_from(network_repo).unwrap();
1440
1441            SolanaRelayer {
1442                relayer: self.relayer_model.clone(),
1443                signer: self.signer,
1444                network,
1445                provider: self.provider,
1446                rpc_handler: self.rpc_handler,
1447                relayer_repository: Arc::new(self.mock_repo),
1448                transaction_repository: self.tx_repo,
1449                job_producer: self.job_producer,
1450                dex_service: self.dex,
1451                network_repository: self.network_repository,
1452            }
1453        }
1454    }
1455
1456    fn create_test_relayer() -> RelayerRepoModel {
1457        RelayerRepoModel {
1458            id: "test-relayer-id".to_string(),
1459            address: "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin".to_string(),
1460            notification_id: Some("test-notification-id".to_string()),
1461            network_type: NetworkType::Solana,
1462            policies: RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
1463                min_balance: Some(0), // No minimum balance requirement
1464                swap_config: None,
1465                ..Default::default()
1466            }),
1467            ..Default::default()
1468        }
1469    }
1470
1471    fn create_token_policy(
1472        mint: &str,
1473        min_amount: Option<u64>,
1474        max_amount: Option<u64>,
1475        retain_min: Option<u64>,
1476        slippage: Option<u64>,
1477    ) -> SolanaAllowedTokensPolicy {
1478        let mut token = SolanaAllowedTokensPolicy {
1479            mint: mint.to_string(),
1480            max_allowed_fee: Some(0),
1481            swap_config: None,
1482            decimals: Some(9),
1483            symbol: Some("SOL".to_string()),
1484        };
1485
1486        let swap_config = SolanaAllowedTokensSwapConfig {
1487            min_amount,
1488            max_amount,
1489            retain_min_amount: retain_min,
1490            slippage_percentage: slippage.map(|s| s as f32),
1491        };
1492
1493        token.swap_config = Some(swap_config);
1494        token
1495    }
1496
1497    #[tokio::test]
1498    async fn test_calculate_swap_amount_no_limits() {
1499        let ctx = TestCtx::default();
1500        let solana_relayer = ctx.into_relayer().await;
1501
1502        assert_eq!(
1503            solana_relayer
1504                .calculate_swap_amount(100, None, None, None)
1505                .unwrap(),
1506            100
1507        );
1508    }
1509
1510    #[tokio::test]
1511    async fn test_calculate_swap_amount_with_max() {
1512        let ctx = TestCtx::default();
1513        let solana_relayer = ctx.into_relayer().await;
1514
1515        assert_eq!(
1516            solana_relayer
1517                .calculate_swap_amount(100, None, Some(60), None)
1518                .unwrap(),
1519            60
1520        );
1521    }
1522
1523    #[tokio::test]
1524    async fn test_calculate_swap_amount_with_retain() {
1525        let ctx = TestCtx::default();
1526        let solana_relayer = ctx.into_relayer().await;
1527
1528        assert_eq!(
1529            solana_relayer
1530                .calculate_swap_amount(100, None, None, Some(30))
1531                .unwrap(),
1532            70
1533        );
1534
1535        assert_eq!(
1536            solana_relayer
1537                .calculate_swap_amount(20, None, None, Some(30))
1538                .unwrap(),
1539            0
1540        );
1541    }
1542
1543    #[tokio::test]
1544    async fn test_calculate_swap_amount_with_min() {
1545        let ctx = TestCtx::default();
1546        let solana_relayer = ctx.into_relayer().await;
1547
1548        assert_eq!(
1549            solana_relayer
1550                .calculate_swap_amount(40, Some(50), None, None)
1551                .unwrap(),
1552            0
1553        );
1554
1555        assert_eq!(
1556            solana_relayer
1557                .calculate_swap_amount(100, Some(50), None, None)
1558                .unwrap(),
1559            100
1560        );
1561    }
1562
1563    #[tokio::test]
1564    async fn test_calculate_swap_amount_combined() {
1565        let ctx = TestCtx::default();
1566        let solana_relayer = ctx.into_relayer().await;
1567
1568        assert_eq!(
1569            solana_relayer
1570                .calculate_swap_amount(100, None, Some(50), Some(30))
1571                .unwrap(),
1572            50
1573        );
1574
1575        assert_eq!(
1576            solana_relayer
1577                .calculate_swap_amount(100, Some(20), Some(50), Some(30))
1578                .unwrap(),
1579            50
1580        );
1581
1582        assert_eq!(
1583            solana_relayer
1584                .calculate_swap_amount(100, Some(60), Some(50), Some(30))
1585                .unwrap(),
1586            0
1587        );
1588    }
1589
1590    #[tokio::test]
1591    async fn test_handle_token_swap_request_successful_swap_jupiter_swap_strategy() {
1592        let mut relayer_model = create_test_relayer();
1593
1594        let mut mock_relayer_repo = MockRelayerRepository::new();
1595        let id = relayer_model.id.clone();
1596
1597        relayer_model.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
1598            swap_config: Some(RelayerSolanaSwapConfig {
1599                strategy: Some(SolanaSwapStrategy::JupiterSwap),
1600                cron_schedule: None,
1601                min_balance_threshold: None,
1602                jupiter_swap_options: None,
1603            }),
1604            allowed_tokens: Some(vec![create_token_policy(
1605                "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
1606                Some(1),
1607                None,
1608                None,
1609                Some(50),
1610            )]),
1611            ..Default::default()
1612        });
1613        let cloned = relayer_model.clone();
1614
1615        mock_relayer_repo
1616            .expect_get_by_id()
1617            .with(eq(id.clone()))
1618            .times(1)
1619            .returning(move |_| Ok(cloned.clone()));
1620
1621        let mut raw_provider = MockSolanaProviderTrait::new();
1622
1623        raw_provider
1624            .expect_get_account_from_pubkey()
1625            .returning(|_| {
1626                Box::pin(async {
1627                    let mut account_data = vec![0; SplAccount::LEN];
1628
1629                    let token_account = spl_token_interface::state::Account {
1630                        mint: Pubkey::new_unique(),
1631                        owner: Pubkey::new_unique(),
1632                        amount: 10000000,
1633                        state: spl_token_interface::state::AccountState::Initialized,
1634                        ..Default::default()
1635                    };
1636                    spl_token_interface::state::Account::pack(token_account, &mut account_data)
1637                        .unwrap();
1638
1639                    Ok(solana_sdk::account::Account {
1640                        lamports: 1_000_000,
1641                        data: account_data,
1642                        owner: spl_token_interface::id(),
1643                        executable: false,
1644                        rent_epoch: 0,
1645                    })
1646                })
1647            });
1648
1649        let mut jupiter_mock = MockJupiterServiceTrait::new();
1650
1651        jupiter_mock.expect_get_quote().returning(|_| {
1652            Box::pin(async {
1653                Ok(QuoteResponse {
1654                    input_mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
1655                    output_mint: WRAPPED_SOL_MINT.to_string(),
1656                    in_amount: 10,
1657                    out_amount: 10,
1658                    other_amount_threshold: 1,
1659                    swap_mode: "ExactIn".to_string(),
1660                    price_impact_pct: 0.0,
1661                    route_plan: vec![RoutePlan {
1662                        percent: 100,
1663                        swap_info: SwapInfo {
1664                            amm_key: "mock_amm_key".to_string(),
1665                            label: "mock_label".to_string(),
1666                            input_mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
1667                            output_mint: WRAPPED_SOL_MINT.to_string(),
1668                            in_amount: "1000".to_string(),
1669                            out_amount: "1000".to_string(),
1670                            fee_amount: Some("0".to_string()),
1671                            fee_mint: Some("mock_fee_mint".to_string()),
1672                        },
1673                    }],
1674                    slippage_bps: 0,
1675                })
1676            })
1677        });
1678
1679        jupiter_mock.expect_get_swap_transaction().returning(|_| {
1680            Box::pin(async {
1681                Ok(SwapResponse {
1682                    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(),
1683                    last_valid_block_height: 100,
1684                    prioritization_fee_lamports: None,
1685                    compute_unit_limit: None,
1686                    simulation_error: None,
1687                })
1688            })
1689        });
1690
1691        let mut signer = MockSolanaSignTrait::new();
1692        let test_signature = Signature::from_str("2jg9xbGLtZRsiJBrDWQnz33JuLjDkiKSZuxZPdjJ3qrJbMeTEerXFAKynkPW63J88nq63cvosDNRsg9VqHtGixvP").unwrap();
1693
1694        signer
1695            .expect_sign()
1696            .times(1)
1697            .returning(move |_| Box::pin(async move { Ok(test_signature) }));
1698
1699        raw_provider
1700            .expect_send_versioned_transaction()
1701            .times(1)
1702            .returning(move |_| Box::pin(async move { Ok(test_signature) }));
1703
1704        raw_provider
1705            .expect_confirm_transaction()
1706            .times(1)
1707            .returning(move |_| Box::pin(async move { Ok(true) }));
1708
1709        let provider_arc = Arc::new(raw_provider);
1710        let jupiter_arc = Arc::new(jupiter_mock);
1711        let signer_arc = Arc::new(signer);
1712
1713        let dex = Arc::new(
1714            create_network_dex_generic(
1715                &relayer_model,
1716                provider_arc.clone(),
1717                signer_arc.clone(),
1718                jupiter_arc.clone(),
1719            )
1720            .unwrap(),
1721        );
1722
1723        let mut job_producer = MockJobProducerTrait::new();
1724        job_producer
1725            .expect_produce_send_notification_job()
1726            .times(1)
1727            .returning(|_, _| Box::pin(async { Ok(()) }));
1728
1729        let job_producer_arc = Arc::new(job_producer);
1730
1731        let ctx = TestCtx {
1732            relayer_model,
1733            mock_repo: mock_relayer_repo,
1734            provider: provider_arc.clone(),
1735            jupiter: jupiter_arc.clone(),
1736            signer: signer_arc.clone(),
1737            dex,
1738            job_producer: job_producer_arc.clone(),
1739            ..Default::default()
1740        };
1741        let solana_relayer = ctx.into_relayer().await;
1742        let res = solana_relayer
1743            .handle_token_swap_request(create_test_relayer().id)
1744            .await
1745            .unwrap();
1746        assert_eq!(res.len(), 1);
1747        let swap = &res[0];
1748        assert_eq!(swap.source_amount, 10000000);
1749        assert_eq!(swap.destination_amount, 10);
1750        assert_eq!(swap.transaction_signature, test_signature.to_string());
1751    }
1752
1753    #[tokio::test]
1754    async fn test_handle_token_swap_request_successful_swap_jupiter_ultra_strategy() {
1755        let mut relayer_model = create_test_relayer();
1756
1757        let mut mock_relayer_repo = MockRelayerRepository::new();
1758        let id = relayer_model.id.clone();
1759
1760        relayer_model.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
1761            swap_config: Some(RelayerSolanaSwapConfig {
1762                strategy: Some(SolanaSwapStrategy::JupiterUltra),
1763                cron_schedule: None,
1764                min_balance_threshold: None,
1765                jupiter_swap_options: None,
1766            }),
1767            allowed_tokens: Some(vec![create_token_policy(
1768                "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
1769                Some(1),
1770                None,
1771                None,
1772                Some(50),
1773            )]),
1774            ..Default::default()
1775        });
1776        let cloned = relayer_model.clone();
1777
1778        mock_relayer_repo
1779            .expect_get_by_id()
1780            .with(eq(id.clone()))
1781            .times(1)
1782            .returning(move |_| Ok(cloned.clone()));
1783
1784        let mut raw_provider = MockSolanaProviderTrait::new();
1785
1786        raw_provider
1787            .expect_get_account_from_pubkey()
1788            .returning(|_| {
1789                Box::pin(async {
1790                    let mut account_data = vec![0; SplAccount::LEN];
1791
1792                    let token_account = spl_token_interface::state::Account {
1793                        mint: Pubkey::new_unique(),
1794                        owner: Pubkey::new_unique(),
1795                        amount: 10000000,
1796                        state: spl_token_interface::state::AccountState::Initialized,
1797                        ..Default::default()
1798                    };
1799                    spl_token_interface::state::Account::pack(token_account, &mut account_data)
1800                        .unwrap();
1801
1802                    Ok(solana_sdk::account::Account {
1803                        lamports: 1_000_000,
1804                        data: account_data,
1805                        owner: spl_token_interface::id(),
1806                        executable: false,
1807                        rent_epoch: 0,
1808                    })
1809                })
1810            });
1811
1812        let mut jupiter_mock = MockJupiterServiceTrait::new();
1813        jupiter_mock.expect_get_ultra_order().returning(|_| {
1814            Box::pin(async {
1815                Ok(UltraOrderResponse {
1816                    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()),
1817                    input_mint: "PjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
1818                    output_mint: WRAPPED_SOL_MINT.to_string(),
1819                    in_amount: 10,
1820                    out_amount: 10,
1821                    other_amount_threshold: 1,
1822                    swap_mode: "ExactIn".to_string(),
1823                    price_impact_pct: 0.0,
1824                    route_plan: vec![RoutePlan {
1825                        percent: 100,
1826                        swap_info: SwapInfo {
1827                            amm_key: "mock_amm_key".to_string(),
1828                            label: "mock_label".to_string(),
1829                            input_mint: "PjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
1830                            output_mint: WRAPPED_SOL_MINT.to_string(),
1831                            in_amount: "1000".to_string(),
1832                            out_amount: "1000".to_string(),
1833                            fee_amount: Some("0".to_string()),
1834                            fee_mint: Some("mock_fee_mint".to_string()),
1835                        },
1836                    }],
1837                    prioritization_fee_lamports: 0,
1838                    request_id: "mock_request_id".to_string(),
1839                    slippage_bps: 0,
1840                })
1841            })
1842        });
1843
1844        jupiter_mock.expect_execute_ultra_order().returning(|_| {
1845            Box::pin(async {
1846               Ok(UltraExecuteResponse {
1847                    signature: Some("2jg9xbGLtZRsiJBrDWQnz33JuLjDkiKSZuxZPdjJ3qrJbMeTEerXFAKynkPW63J88nq63cvosDNRsg9VqHtGixvP".to_string()),
1848                    status: "success".to_string(),
1849                    slot: Some("123456789".to_string()),
1850                    error: None,
1851                    code: 0,
1852                    total_input_amount: Some("1000000".to_string()),
1853                    total_output_amount: Some("1000000".to_string()),
1854                    input_amount_result: Some("1000000".to_string()),
1855                    output_amount_result: Some("1000000".to_string()),
1856                    swap_events: Some(vec![SwapEvents {
1857                        input_mint: "mock_input_mint".to_string(),
1858                        output_mint: "mock_output_mint".to_string(),
1859                        input_amount: "1000000".to_string(),
1860                        output_amount: "1000000".to_string(),
1861                    }]),
1862                })
1863            })
1864        });
1865
1866        let mut signer = MockSolanaSignTrait::new();
1867        let test_signature = Signature::from_str("2jg9xbGLtZRsiJBrDWQnz33JuLjDkiKSZuxZPdjJ3qrJbMeTEerXFAKynkPW63J88nq63cvosDNRsg9VqHtGixvP").unwrap();
1868
1869        signer
1870            .expect_sign()
1871            .times(1)
1872            .returning(move |_| Box::pin(async move { Ok(test_signature) }));
1873
1874        let provider_arc = Arc::new(raw_provider);
1875        let jupiter_arc = Arc::new(jupiter_mock);
1876        let signer_arc = Arc::new(signer);
1877
1878        let dex = Arc::new(
1879            create_network_dex_generic(
1880                &relayer_model,
1881                provider_arc.clone(),
1882                signer_arc.clone(),
1883                jupiter_arc.clone(),
1884            )
1885            .unwrap(),
1886        );
1887
1888        let mut job_producer = MockJobProducerTrait::new();
1889        job_producer
1890            .expect_produce_send_notification_job()
1891            .times(1)
1892            .returning(|_, _| Box::pin(async { Ok(()) }));
1893
1894        let job_producer_arc = Arc::new(job_producer);
1895
1896        let ctx = TestCtx {
1897            relayer_model,
1898            mock_repo: mock_relayer_repo,
1899            provider: provider_arc.clone(),
1900            jupiter: jupiter_arc.clone(),
1901            signer: signer_arc.clone(),
1902            dex,
1903            job_producer: job_producer_arc.clone(),
1904            ..Default::default()
1905        };
1906        let solana_relayer = ctx.into_relayer().await;
1907
1908        let res = solana_relayer
1909            .handle_token_swap_request(create_test_relayer().id)
1910            .await
1911            .unwrap();
1912        assert_eq!(res.len(), 1);
1913        let swap = &res[0];
1914        assert_eq!(swap.source_amount, 10000000);
1915        assert_eq!(swap.destination_amount, 10);
1916        assert_eq!(swap.transaction_signature, test_signature.to_string());
1917    }
1918
1919    #[tokio::test]
1920    async fn test_handle_token_swap_request_no_swap_config() {
1921        let mut relayer_model = create_test_relayer();
1922
1923        let mut mock_relayer_repo = MockRelayerRepository::new();
1924        let id = relayer_model.id.clone();
1925        let cloned = relayer_model.clone();
1926        mock_relayer_repo
1927            .expect_get_by_id()
1928            .with(eq(id.clone()))
1929            .times(1)
1930            .returning(move |_| Ok(cloned.clone()));
1931
1932        relayer_model.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
1933            swap_config: Some(RelayerSolanaSwapConfig {
1934                strategy: Some(SolanaSwapStrategy::JupiterSwap),
1935                cron_schedule: None,
1936                min_balance_threshold: None,
1937                jupiter_swap_options: None,
1938            }),
1939            allowed_tokens: Some(vec![create_token_policy(
1940                "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
1941                Some(1),
1942                None,
1943                None,
1944                Some(50),
1945            )]),
1946            ..Default::default()
1947        });
1948        let mut job_producer = MockJobProducerTrait::new();
1949        job_producer.expect_produce_send_notification_job().times(0);
1950
1951        let job_producer_arc = Arc::new(job_producer);
1952
1953        let ctx = TestCtx {
1954            relayer_model,
1955            mock_repo: mock_relayer_repo,
1956            job_producer: job_producer_arc,
1957            ..Default::default()
1958        };
1959        let solana_relayer = ctx.into_relayer().await;
1960
1961        let res = solana_relayer.handle_token_swap_request(id).await;
1962        assert!(res.is_ok());
1963        assert!(res.unwrap().is_empty());
1964    }
1965
1966    #[tokio::test]
1967    async fn test_handle_token_swap_request_no_strategy() {
1968        let mut relayer_model: RelayerRepoModel = create_test_relayer();
1969
1970        let mut mock_relayer_repo = MockRelayerRepository::new();
1971        let id = relayer_model.id.clone();
1972        let cloned = relayer_model.clone();
1973        mock_relayer_repo
1974            .expect_get_by_id()
1975            .with(eq(id.clone()))
1976            .times(1)
1977            .returning(move |_| Ok(cloned.clone()));
1978
1979        relayer_model.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
1980            swap_config: Some(RelayerSolanaSwapConfig {
1981                strategy: None,
1982                cron_schedule: None,
1983                min_balance_threshold: Some(1),
1984                jupiter_swap_options: None,
1985            }),
1986            ..Default::default()
1987        });
1988
1989        let ctx = TestCtx {
1990            relayer_model,
1991            mock_repo: mock_relayer_repo,
1992            ..Default::default()
1993        };
1994        let solana_relayer = ctx.into_relayer().await;
1995
1996        let res = solana_relayer.handle_token_swap_request(id).await.unwrap();
1997        assert!(res.is_empty(), "should return empty when no strategy");
1998    }
1999
2000    #[tokio::test]
2001    async fn test_handle_token_swap_request_no_allowed_tokens() {
2002        let mut relayer_model: RelayerRepoModel = create_test_relayer();
2003        let mut mock_relayer_repo = MockRelayerRepository::new();
2004        let id = relayer_model.id.clone();
2005        let cloned = relayer_model.clone();
2006        mock_relayer_repo
2007            .expect_get_by_id()
2008            .with(eq(id.clone()))
2009            .times(1)
2010            .returning(move |_| Ok(cloned.clone()));
2011
2012        relayer_model.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
2013            swap_config: Some(RelayerSolanaSwapConfig {
2014                strategy: Some(SolanaSwapStrategy::JupiterSwap),
2015                cron_schedule: None,
2016                min_balance_threshold: Some(1),
2017                jupiter_swap_options: None,
2018            }),
2019            allowed_tokens: None,
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 allowed_tokens");
2032    }
2033
2034    #[tokio::test]
2035    async fn test_validate_rpc_success() {
2036        let mut raw_provider = MockSolanaProviderTrait::new();
2037        raw_provider
2038            .expect_get_latest_blockhash()
2039            .times(1)
2040            .returning(|| Box::pin(async { Ok(Hash::new_unique()) }));
2041
2042        let ctx = TestCtx {
2043            provider: Arc::new(raw_provider),
2044            ..Default::default()
2045        };
2046        let solana_relayer = ctx.into_relayer().await;
2047        let res = solana_relayer.validate_rpc().await;
2048
2049        assert!(
2050            res.is_ok(),
2051            "validate_rpc should succeed when blockhash fetch succeeds"
2052        );
2053    }
2054
2055    #[tokio::test]
2056    async fn test_validate_rpc_provider_error() {
2057        let mut raw_provider = MockSolanaProviderTrait::new();
2058        raw_provider
2059            .expect_get_latest_blockhash()
2060            .times(1)
2061            .returning(|| {
2062                Box::pin(async { Err(SolanaProviderError::RpcError("rpc failure".to_string())) })
2063            });
2064
2065        let ctx = TestCtx {
2066            provider: Arc::new(raw_provider),
2067            ..Default::default()
2068        };
2069
2070        let solana_relayer = ctx.into_relayer().await;
2071        let err = solana_relayer.validate_rpc().await.unwrap_err();
2072
2073        match err {
2074            RelayerError::ProviderError(msg) => {
2075                assert!(msg.contains("rpc failure"));
2076            }
2077            other => panic!("expected ProviderError, got {other:?}"),
2078        }
2079    }
2080
2081    #[tokio::test]
2082    async fn test_check_balance_no_swap_config() {
2083        // default ctx has no swap_config
2084        let ctx = TestCtx::default();
2085        let solana_relayer = ctx.into_relayer().await;
2086
2087        // should do nothing and succeed
2088        assert!(solana_relayer
2089            .check_balance_and_trigger_token_swap_if_needed()
2090            .await
2091            .is_ok());
2092    }
2093
2094    #[tokio::test]
2095    async fn test_check_balance_no_threshold() {
2096        // override policy to have a swap_config with no min_balance_threshold
2097        let mut ctx = TestCtx::default();
2098        let mut model = ctx.relayer_model.clone();
2099        model.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
2100            swap_config: Some(RelayerSolanaSwapConfig {
2101                strategy: Some(SolanaSwapStrategy::JupiterSwap),
2102                cron_schedule: None,
2103                min_balance_threshold: None,
2104                jupiter_swap_options: None,
2105            }),
2106            ..Default::default()
2107        });
2108        ctx.relayer_model = model;
2109        let solana_relayer = ctx.into_relayer().await;
2110
2111        assert!(solana_relayer
2112            .check_balance_and_trigger_token_swap_if_needed()
2113            .await
2114            .is_ok());
2115    }
2116
2117    #[tokio::test]
2118    async fn test_check_balance_above_threshold() {
2119        let mut raw_provider = MockSolanaProviderTrait::new();
2120        raw_provider
2121            .expect_get_balance()
2122            .times(1)
2123            .returning(|_| Box::pin(async { Ok(20_u64) }));
2124        let provider = Arc::new(raw_provider);
2125        let mut raw_job = MockJobProducerTrait::new();
2126        raw_job
2127            .expect_produce_token_swap_request_job()
2128            .withf(move |req, _opts| req.relayer_id == "test-id")
2129            .times(0);
2130        let job_producer = Arc::new(raw_job);
2131
2132        let ctx = TestCtx {
2133            provider,
2134            job_producer,
2135            ..Default::default()
2136        };
2137        // set threshold to 10
2138        let mut model = ctx.relayer_model.clone();
2139        model.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
2140            swap_config: Some(RelayerSolanaSwapConfig {
2141                strategy: Some(SolanaSwapStrategy::JupiterSwap),
2142                cron_schedule: None,
2143                min_balance_threshold: Some(10),
2144                jupiter_swap_options: None,
2145            }),
2146            ..Default::default()
2147        });
2148        let mut ctx = ctx;
2149        ctx.relayer_model = model;
2150
2151        let solana_relayer = ctx.into_relayer().await;
2152        assert!(solana_relayer
2153            .check_balance_and_trigger_token_swap_if_needed()
2154            .await
2155            .is_ok());
2156    }
2157
2158    #[tokio::test]
2159    async fn test_check_balance_below_threshold_triggers_job() {
2160        let mut raw_provider = MockSolanaProviderTrait::new();
2161        raw_provider
2162            .expect_get_balance()
2163            .times(1)
2164            .returning(|_| Box::pin(async { Ok(5_u64) }));
2165
2166        let mut raw_job = MockJobProducerTrait::new();
2167        raw_job
2168            .expect_produce_token_swap_request_job()
2169            .times(1)
2170            .returning(|_, _| Box::pin(async { Ok(()) }));
2171        let job_producer = Arc::new(raw_job);
2172
2173        let mut model = create_test_relayer();
2174        model.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
2175            swap_config: Some(RelayerSolanaSwapConfig {
2176                strategy: Some(SolanaSwapStrategy::JupiterSwap),
2177                cron_schedule: None,
2178                min_balance_threshold: Some(10),
2179                jupiter_swap_options: None,
2180            }),
2181            ..Default::default()
2182        });
2183
2184        let ctx = TestCtx {
2185            relayer_model: model,
2186            provider: Arc::new(raw_provider),
2187            job_producer,
2188            ..Default::default()
2189        };
2190
2191        let solana_relayer = ctx.into_relayer().await;
2192        assert!(solana_relayer
2193            .check_balance_and_trigger_token_swap_if_needed()
2194            .await
2195            .is_ok());
2196    }
2197
2198    #[tokio::test]
2199    async fn test_get_balance_success() {
2200        let mut raw_provider = MockSolanaProviderTrait::new();
2201        raw_provider
2202            .expect_get_balance()
2203            .times(1)
2204            .returning(|_| Box::pin(async { Ok(42_u64) }));
2205        let ctx = TestCtx {
2206            provider: Arc::new(raw_provider),
2207            ..Default::default()
2208        };
2209        let solana_relayer = ctx.into_relayer().await;
2210
2211        let res = solana_relayer.get_balance().await.unwrap();
2212
2213        assert_eq!(res.balance, 42_u128);
2214        assert_eq!(res.unit, SOLANA_SMALLEST_UNIT_NAME);
2215    }
2216
2217    #[tokio::test]
2218    async fn test_get_balance_provider_error() {
2219        let mut raw_provider = MockSolanaProviderTrait::new();
2220        raw_provider
2221            .expect_get_balance()
2222            .times(1)
2223            .returning(|_| Box::pin(async { Err(SolanaProviderError::RpcError("oops".into())) }));
2224        let ctx = TestCtx {
2225            provider: Arc::new(raw_provider),
2226            ..Default::default()
2227        };
2228        let solana_relayer = ctx.into_relayer().await;
2229
2230        let err = solana_relayer.get_balance().await.unwrap_err();
2231
2232        match err {
2233            RelayerError::UnderlyingSolanaProvider(err) => {
2234                assert!(err.to_string().contains("oops"));
2235            }
2236            other => panic!("expected ProviderError, got {other:?}"),
2237        }
2238    }
2239
2240    #[tokio::test]
2241    async fn test_validate_min_balance_success() {
2242        let mut raw_provider = MockSolanaProviderTrait::new();
2243        raw_provider
2244            .expect_get_balance()
2245            .times(1)
2246            .returning(|_| Box::pin(async { Ok(100_u64) }));
2247
2248        let mut model = create_test_relayer();
2249        model.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
2250            min_balance: Some(50),
2251            ..Default::default()
2252        });
2253
2254        let ctx = TestCtx {
2255            relayer_model: model,
2256            provider: Arc::new(raw_provider),
2257            ..Default::default()
2258        };
2259
2260        let solana_relayer = ctx.into_relayer().await;
2261        assert!(solana_relayer.validate_min_balance().await.is_ok());
2262    }
2263
2264    #[tokio::test]
2265    async fn test_validate_min_balance_insufficient() {
2266        let mut raw_provider = MockSolanaProviderTrait::new();
2267        raw_provider
2268            .expect_get_balance()
2269            .times(1)
2270            .returning(|_| Box::pin(async { Ok(10_u64) }));
2271
2272        let mut model = create_test_relayer();
2273        model.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
2274            min_balance: Some(50),
2275            ..Default::default()
2276        });
2277
2278        let ctx = TestCtx {
2279            relayer_model: model,
2280            provider: Arc::new(raw_provider),
2281            ..Default::default()
2282        };
2283
2284        let solana_relayer = ctx.into_relayer().await;
2285        let err = solana_relayer.validate_min_balance().await.unwrap_err();
2286        match err {
2287            RelayerError::InsufficientBalanceError(msg) => {
2288                assert_eq!(msg, "Insufficient balance");
2289            }
2290            other => panic!("expected InsufficientBalanceError, got {other:?}"),
2291        }
2292    }
2293
2294    #[tokio::test]
2295    async fn test_validate_min_balance_provider_error() {
2296        let mut raw_provider = MockSolanaProviderTrait::new();
2297        raw_provider
2298            .expect_get_balance()
2299            .times(1)
2300            .returning(|_| Box::pin(async { Err(SolanaProviderError::RpcError("fail".into())) }));
2301        let ctx = TestCtx {
2302            provider: Arc::new(raw_provider),
2303            ..Default::default()
2304        };
2305
2306        let solana_relayer = ctx.into_relayer().await;
2307        let err = solana_relayer.validate_min_balance().await.unwrap_err();
2308        match err {
2309            RelayerError::ProviderError(msg) => {
2310                assert!(msg.contains("fail"));
2311            }
2312            other => panic!("expected ProviderError, got {other:?}"),
2313        }
2314    }
2315
2316    #[tokio::test]
2317    async fn test_rpc_invalid_params() {
2318        let ctx = TestCtx::default();
2319        let solana_relayer = ctx.into_relayer().await;
2320
2321        let req = JsonRpcRequest {
2322            jsonrpc: "2.0".to_string(),
2323            params: NetworkRpcRequest::Solana(crate::models::SolanaRpcRequest::FeeEstimate(
2324                SolanaFeeEstimateRequestParams {
2325                    transaction: EncodedSerializedTransaction::new("".to_string()),
2326                    fee_token: "".to_string(),
2327                },
2328            )),
2329            id: Some(JsonRpcId::Number(1)),
2330        };
2331        let resp = solana_relayer.rpc(req).await.unwrap();
2332
2333        assert!(resp.error.is_some(), "expected an error object");
2334        let err = resp.error.unwrap();
2335        assert_eq!(err.code, -32601);
2336        assert_eq!(err.message, "INVALID_PARAMS");
2337    }
2338
2339    #[tokio::test]
2340    async fn test_rpc_success() {
2341        let ctx = TestCtx::default();
2342        let solana_relayer = ctx.into_relayer().await;
2343
2344        let req = JsonRpcRequest {
2345            jsonrpc: "2.0".to_string(),
2346            params: NetworkRpcRequest::Solana(crate::models::SolanaRpcRequest::GetFeaturesEnabled(
2347                SolanaGetFeaturesEnabledRequestParams {},
2348            )),
2349            id: Some(JsonRpcId::Number(1)),
2350        };
2351        let resp = solana_relayer.rpc(req).await.unwrap();
2352
2353        assert!(resp.error.is_none(), "error should be None");
2354        let data = resp.result.unwrap();
2355        let sol_res = match data {
2356            NetworkRpcResult::Solana(inner) => inner,
2357            other => panic!("expected Solana, got {other:?}"),
2358        };
2359        let features = match sol_res {
2360            SolanaRpcResult::GetFeaturesEnabled(f) => f,
2361            other => panic!("expected GetFeaturesEnabled, got {other:?}"),
2362        };
2363        assert_eq!(features.features, vec!["gasless".to_string()]);
2364    }
2365
2366    #[tokio::test]
2367    async fn test_initialize_relayer_disables_when_validation_fails() {
2368        let mut raw_provider = MockSolanaProviderTrait::new();
2369        let mut mock_repo = MockRelayerRepository::new();
2370        let mut job_producer = MockJobProducerTrait::new();
2371
2372        let mut relayer_model = create_test_relayer();
2373        relayer_model.system_disabled = false; // Start as enabled
2374        relayer_model.notification_id = Some("test-notification-id".to_string());
2375
2376        // Mock validation failure - RPC validation fails
2377        raw_provider.expect_get_latest_blockhash().returning(|| {
2378            Box::pin(async { Err(SolanaProviderError::RpcError("RPC error".to_string())) })
2379        });
2380
2381        raw_provider
2382            .expect_get_balance()
2383            .returning(|_| Box::pin(async { Ok(1000000u64) })); // Sufficient balance
2384
2385        // Mock disable_relayer call
2386        let mut disabled_relayer = relayer_model.clone();
2387        disabled_relayer.system_disabled = true;
2388        mock_repo
2389            .expect_disable_relayer()
2390            .with(eq("test-relayer-id".to_string()), always())
2391            .returning(move |_, _| Ok(disabled_relayer.clone()));
2392
2393        // Mock notification job production
2394        job_producer
2395            .expect_produce_send_notification_job()
2396            .returning(|_, _| Box::pin(async { Ok(()) }));
2397
2398        // Mock health check job scheduling
2399        job_producer
2400            .expect_produce_relayer_health_check_job()
2401            .returning(|_, _| Box::pin(async { Ok(()) }));
2402
2403        let ctx = TestCtx {
2404            relayer_model,
2405            mock_repo,
2406            provider: Arc::new(raw_provider),
2407            job_producer: Arc::new(job_producer),
2408            ..Default::default()
2409        };
2410
2411        let solana_relayer = ctx.into_relayer().await;
2412        let result = solana_relayer.initialize_relayer().await;
2413        assert!(result.is_ok());
2414    }
2415
2416    #[tokio::test]
2417    async fn test_initialize_relayer_enables_when_validation_passes_and_was_disabled() {
2418        let mut raw_provider = MockSolanaProviderTrait::new();
2419        let mut mock_repo = MockRelayerRepository::new();
2420
2421        let mut relayer_model = create_test_relayer();
2422        relayer_model.system_disabled = true; // Start as disabled
2423
2424        // Mock successful validations
2425        raw_provider
2426            .expect_get_latest_blockhash()
2427            .returning(|| Box::pin(async { Ok(Hash::new_unique()) }));
2428
2429        raw_provider
2430            .expect_get_balance()
2431            .returning(|_| Box::pin(async { Ok(1000000u64) })); // Sufficient balance
2432
2433        // Mock enable_relayer call
2434        let mut enabled_relayer = relayer_model.clone();
2435        enabled_relayer.system_disabled = false;
2436        mock_repo
2437            .expect_enable_relayer()
2438            .with(eq("test-relayer-id".to_string()))
2439            .returning(move |_| Ok(enabled_relayer.clone()));
2440
2441        // Mock any potential disable_relayer calls (even though they shouldn't happen)
2442        let mut disabled_relayer = relayer_model.clone();
2443        disabled_relayer.system_disabled = true;
2444        mock_repo
2445            .expect_disable_relayer()
2446            .returning(move |_, _| Ok(disabled_relayer.clone()));
2447
2448        let ctx = TestCtx {
2449            relayer_model,
2450            mock_repo,
2451            provider: Arc::new(raw_provider),
2452            ..Default::default()
2453        };
2454
2455        let solana_relayer = ctx.into_relayer().await;
2456        let result = solana_relayer.initialize_relayer().await;
2457        assert!(result.is_ok());
2458    }
2459
2460    #[tokio::test]
2461    async fn test_initialize_relayer_no_action_when_enabled_and_validation_passes() {
2462        let mut raw_provider = MockSolanaProviderTrait::new();
2463        let mock_repo = MockRelayerRepository::new();
2464
2465        let mut relayer_model = create_test_relayer();
2466        relayer_model.system_disabled = false; // Start as enabled
2467
2468        // Mock successful validations
2469        raw_provider
2470            .expect_get_latest_blockhash()
2471            .returning(|| Box::pin(async { Ok(Hash::new_unique()) }));
2472
2473        raw_provider
2474            .expect_get_balance()
2475            .returning(|_| Box::pin(async { Ok(1000000u64) })); // Sufficient balance
2476
2477        let ctx = TestCtx {
2478            relayer_model,
2479            mock_repo,
2480            provider: Arc::new(raw_provider),
2481            ..Default::default()
2482        };
2483
2484        let solana_relayer = ctx.into_relayer().await;
2485        let result = solana_relayer.initialize_relayer().await;
2486        assert!(result.is_ok());
2487    }
2488
2489    #[tokio::test]
2490    async fn test_initialize_relayer_sends_notification_when_disabled() {
2491        let mut raw_provider = MockSolanaProviderTrait::new();
2492        let mut mock_repo = MockRelayerRepository::new();
2493        let mut job_producer = MockJobProducerTrait::new();
2494
2495        let mut relayer_model = create_test_relayer();
2496        relayer_model.system_disabled = false; // Start as enabled
2497        relayer_model.notification_id = Some("test-notification-id".to_string());
2498
2499        // Mock validation failure - balance check fails
2500        raw_provider
2501            .expect_get_latest_blockhash()
2502            .returning(|| Box::pin(async { Ok(Hash::new_unique()) }));
2503
2504        raw_provider
2505            .expect_get_balance()
2506            .returning(|_| Box::pin(async { Ok(100u64) })); // Insufficient balance
2507
2508        // Mock disable_relayer call
2509        let mut disabled_relayer = relayer_model.clone();
2510        disabled_relayer.system_disabled = true;
2511        mock_repo
2512            .expect_disable_relayer()
2513            .with(eq("test-relayer-id".to_string()), always())
2514            .returning(move |_, _| Ok(disabled_relayer.clone()));
2515
2516        // Mock notification job production - verify it's called
2517        job_producer
2518            .expect_produce_send_notification_job()
2519            .returning(|_, _| Box::pin(async { Ok(()) }));
2520
2521        // Mock health check job scheduling
2522        job_producer
2523            .expect_produce_relayer_health_check_job()
2524            .returning(|_, _| Box::pin(async { Ok(()) }));
2525
2526        let ctx = TestCtx {
2527            relayer_model,
2528            mock_repo,
2529            provider: Arc::new(raw_provider),
2530            job_producer: Arc::new(job_producer),
2531            ..Default::default()
2532        };
2533
2534        let solana_relayer = ctx.into_relayer().await;
2535        let result = solana_relayer.initialize_relayer().await;
2536        assert!(result.is_ok());
2537    }
2538
2539    #[tokio::test]
2540    async fn test_initialize_relayer_no_notification_when_no_notification_id() {
2541        let mut raw_provider = MockSolanaProviderTrait::new();
2542        let mut mock_repo = MockRelayerRepository::new();
2543
2544        let mut relayer_model = create_test_relayer();
2545        relayer_model.system_disabled = false; // Start as enabled
2546        relayer_model.notification_id = None; // No notification ID
2547
2548        // Mock validation failure - RPC validation fails
2549        raw_provider.expect_get_latest_blockhash().returning(|| {
2550            Box::pin(async {
2551                Err(SolanaProviderError::RpcError(
2552                    "RPC validation failed".to_string(),
2553                ))
2554            })
2555        });
2556
2557        raw_provider
2558            .expect_get_balance()
2559            .returning(|_| Box::pin(async { Ok(1000000u64) })); // Sufficient balance
2560
2561        // Mock disable_relayer call
2562        let mut disabled_relayer = relayer_model.clone();
2563        disabled_relayer.system_disabled = true;
2564        mock_repo
2565            .expect_disable_relayer()
2566            .with(eq("test-relayer-id".to_string()), always())
2567            .returning(move |_, _| Ok(disabled_relayer.clone()));
2568
2569        // No notification job should be produced since notification_id is None
2570        // But health check job should still be scheduled
2571        let mut job_producer = MockJobProducerTrait::new();
2572        job_producer
2573            .expect_produce_relayer_health_check_job()
2574            .returning(|_, _| Box::pin(async { Ok(()) }));
2575
2576        let ctx = TestCtx {
2577            relayer_model,
2578            mock_repo,
2579            provider: Arc::new(raw_provider),
2580            job_producer: Arc::new(job_producer),
2581            ..Default::default()
2582        };
2583
2584        let solana_relayer = ctx.into_relayer().await;
2585        let result = solana_relayer.initialize_relayer().await;
2586        assert!(result.is_ok());
2587    }
2588
2589    #[tokio::test]
2590    async fn test_initialize_relayer_policy_validation_fails() {
2591        let mut raw_provider = MockSolanaProviderTrait::new();
2592
2593        let mut relayer_model = create_test_relayer();
2594        relayer_model.system_disabled = false;
2595
2596        // Set up a policy that will cause validation to fail
2597        relayer_model.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
2598            allowed_tokens: Some(vec![SolanaAllowedTokensPolicy {
2599                mint: "InvalidMintAddress".to_string(),
2600                decimals: Some(9),
2601                symbol: Some("INVALID".to_string()),
2602                max_allowed_fee: Some(0),
2603                swap_config: None,
2604            }]),
2605            ..Default::default()
2606        });
2607
2608        // Mock provider calls that might be made during token validation
2609        raw_provider
2610            .expect_get_token_metadata_from_pubkey()
2611            .returning(|_| {
2612                Box::pin(async {
2613                    Err(SolanaProviderError::RpcError("Token not found".to_string()))
2614                })
2615            });
2616
2617        let ctx = TestCtx {
2618            relayer_model,
2619            provider: Arc::new(raw_provider),
2620            ..Default::default()
2621        };
2622
2623        let solana_relayer = ctx.into_relayer().await;
2624        let result = solana_relayer.initialize_relayer().await;
2625
2626        // Should fail due to policy validation error
2627        assert!(result.is_err());
2628        match result.unwrap_err() {
2629            RelayerError::PolicyConfigurationError(msg) => {
2630                assert!(msg.contains("Error while processing allowed tokens policy"));
2631            }
2632            other => panic!("Expected PolicyConfigurationError, got {other:?}"),
2633        }
2634    }
2635
2636    #[tokio::test]
2637    async fn test_sign_transaction_success() {
2638        let signer = MockSolanaSignTrait::new();
2639
2640        let relayer_model = RelayerRepoModel {
2641            id: "test-relayer-id".to_string(),
2642            address: "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin".to_string(),
2643            network: "devnet".to_string(),
2644            policies: RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
2645                fee_payment_strategy: Some(SolanaFeePaymentStrategy::Relayer),
2646                min_balance: Some(0),
2647                ..Default::default()
2648            }),
2649            ..Default::default()
2650        };
2651
2652        let ctx = TestCtx {
2653            relayer_model,
2654            signer: Arc::new(signer),
2655            ..Default::default()
2656        };
2657
2658        let solana_relayer = ctx.into_relayer().await;
2659
2660        let sign_request = SignTransactionRequest::Solana(SignTransactionRequestSolana {
2661            transaction: EncodedSerializedTransaction::new("raw_transaction_data".to_string()),
2662        });
2663
2664        let result = solana_relayer.sign_transaction(&sign_request).await;
2665        assert!(result.is_ok());
2666        let response = result.unwrap();
2667        match response {
2668            SignTransactionExternalResponse::Solana(solana_resp) => {
2669                assert_eq!(
2670                    solana_resp.transaction.into_inner(),
2671                    "signed_transaction_data"
2672                );
2673                assert_eq!(solana_resp.signature, "signature_data");
2674            }
2675            _ => panic!("Expected Solana response"),
2676        }
2677    }
2678
2679    #[tokio::test]
2680    async fn test_get_status_success() {
2681        let mut raw_provider = MockSolanaProviderTrait::new();
2682        let mut tx_repo = MockTransactionRepository::new();
2683
2684        // Mock balance retrieval
2685        raw_provider
2686            .expect_get_balance()
2687            .returning(|_| Box::pin(async { Ok(1000000) }));
2688
2689        // Mock count_by_status for pending transactions count
2690        tx_repo
2691            .expect_count_by_status()
2692            .with(
2693                eq("test-id"),
2694                eq(vec![
2695                    TransactionStatus::Pending,
2696                    TransactionStatus::Sent,
2697                    TransactionStatus::Submitted,
2698                ]),
2699            )
2700            .returning(|_, _| Ok(2u64));
2701
2702        // Mock find_by_status_paginated for latest confirmed transaction
2703        let recent_tx = TransactionRepoModel {
2704            id: "recent-tx".to_string(),
2705            relayer_id: "test-id".to_string(),
2706            network_data: NetworkTransactionData::Solana(SolanaTransactionData::default()),
2707            network_type: NetworkType::Solana,
2708            status: TransactionStatus::Confirmed,
2709            confirmed_at: Some(Utc::now().to_string()),
2710            ..Default::default()
2711        };
2712        tx_repo
2713            .expect_find_by_status_paginated()
2714            .withf(|relayer_id, statuses, query, oldest_first| {
2715                *relayer_id == *"test-id"
2716                    && statuses == [TransactionStatus::Confirmed]
2717                    && query.page == 1
2718                    && query.per_page == 1
2719                    && !(*oldest_first)
2720            })
2721            .returning(move |_, _, _, _| {
2722                Ok(crate::repositories::PaginatedResult {
2723                    items: vec![recent_tx.clone()],
2724                    total: 1,
2725                    page: 1,
2726                    per_page: 1,
2727                })
2728            });
2729
2730        let ctx = TestCtx {
2731            tx_repo: Arc::new(tx_repo),
2732            provider: Arc::new(raw_provider),
2733            ..Default::default()
2734        };
2735
2736        let solana_relayer = ctx.into_relayer().await;
2737
2738        let result = solana_relayer.get_status().await;
2739        assert!(result.is_ok());
2740        let status = result.unwrap();
2741
2742        match status {
2743            RelayerStatus::Solana {
2744                balance,
2745                pending_transactions_count,
2746                last_confirmed_transaction_timestamp,
2747                ..
2748            } => {
2749                assert_eq!(balance, "1000000");
2750                assert_eq!(pending_transactions_count, 2);
2751                assert!(last_confirmed_transaction_timestamp.is_some());
2752            }
2753            _ => panic!("Expected Solana status"),
2754        }
2755    }
2756
2757    #[tokio::test]
2758    async fn test_get_status_balance_error() {
2759        let mut raw_provider = MockSolanaProviderTrait::new();
2760        let tx_repo = MockTransactionRepository::new();
2761
2762        // Mock balance error
2763        raw_provider.expect_get_balance().returning(|_| {
2764            Box::pin(async { Err(SolanaProviderError::RpcError("RPC error".to_string())) })
2765        });
2766
2767        let ctx = TestCtx {
2768            tx_repo: Arc::new(tx_repo),
2769            provider: Arc::new(raw_provider),
2770            ..Default::default()
2771        };
2772
2773        let solana_relayer = ctx.into_relayer().await;
2774
2775        let result = solana_relayer.get_status().await;
2776        assert!(result.is_err());
2777        match result.unwrap_err() {
2778            RelayerError::UnderlyingSolanaProvider(err) => {
2779                assert!(err.to_string().contains("RPC error"));
2780            }
2781            other => panic!("Expected UnderlyingSolanaProvider, got {other:?}"),
2782        }
2783    }
2784
2785    #[tokio::test]
2786    async fn test_get_status_no_recent_transactions() {
2787        let mut raw_provider = MockSolanaProviderTrait::new();
2788        let mut tx_repo = MockTransactionRepository::new();
2789
2790        // Mock balance retrieval
2791        raw_provider
2792            .expect_get_balance()
2793            .returning(|_| Box::pin(async { Ok(500000) }));
2794
2795        // Mock count_by_status for pending transactions count
2796        tx_repo
2797            .expect_count_by_status()
2798            .with(
2799                eq("test-id"),
2800                eq(vec![
2801                    TransactionStatus::Pending,
2802                    TransactionStatus::Sent,
2803                    TransactionStatus::Submitted,
2804                ]),
2805            )
2806            .returning(|_, _| Ok(0u64));
2807
2808        // Mock find_by_status_paginated for latest confirmed transaction (none)
2809        tx_repo
2810            .expect_find_by_status_paginated()
2811            .withf(|relayer_id, statuses, query, oldest_first| {
2812                *relayer_id == *"test-id"
2813                    && statuses == [TransactionStatus::Confirmed]
2814                    && query.page == 1
2815                    && query.per_page == 1
2816                    && !(*oldest_first)
2817            })
2818            .returning(|_, _, _, _| {
2819                Ok(crate::repositories::PaginatedResult {
2820                    items: vec![],
2821                    total: 0,
2822                    page: 1,
2823                    per_page: 1,
2824                })
2825            });
2826
2827        let ctx = TestCtx {
2828            tx_repo: Arc::new(tx_repo),
2829            provider: Arc::new(raw_provider),
2830            ..Default::default()
2831        };
2832
2833        let solana_relayer = ctx.into_relayer().await;
2834
2835        let result = solana_relayer.get_status().await;
2836        assert!(result.is_ok());
2837        let status = result.unwrap();
2838
2839        match status {
2840            RelayerStatus::Solana {
2841                balance,
2842                pending_transactions_count,
2843                last_confirmed_transaction_timestamp,
2844                ..
2845            } => {
2846                assert_eq!(balance, "500000");
2847                assert_eq!(pending_transactions_count, 0);
2848                assert!(last_confirmed_transaction_timestamp.is_none());
2849            }
2850            _ => panic!("Expected Solana status"),
2851        }
2852    }
2853
2854    // GasAbstractionTrait tests
2855    // These are passthrough methods to RPC handlers, so we verify:
2856    // 1. Wrong network type returns ValidationError
2857    // The actual RPC handler functionality (including method calls) is tested in the RPC handler tests
2858    // Note: We can't easily mock the RPC handler here due to type constraints in TestCtx,
2859    // but the passthrough behavior is verified through the RPC handler tests.
2860
2861    #[tokio::test]
2862    async fn test_quote_sponsored_transaction_wrong_network() {
2863        let ctx = TestCtx::default();
2864        let solana_relayer = ctx.into_relayer().await;
2865
2866        // Use Stellar request instead of Solana
2867        let request = SponsoredTransactionQuoteRequest::Stellar(
2868            crate::models::StellarFeeEstimateRequestParams {
2869                transaction_xdr: Some("test-xdr".to_string()),
2870                operations: None,
2871                source_account: None,
2872                fee_token: "native".to_string(),
2873            },
2874        );
2875
2876        let result = solana_relayer.quote_sponsored_transaction(request).await;
2877        assert!(result.is_err());
2878
2879        if let Err(RelayerError::ValidationError(msg)) = result {
2880            assert!(msg.contains("Expected Solana fee estimate request parameters"));
2881        } else {
2882            panic!("Expected ValidationError for wrong network type");
2883        }
2884    }
2885
2886    #[tokio::test]
2887    async fn test_build_sponsored_transaction_wrong_network() {
2888        let ctx = TestCtx::default();
2889        let solana_relayer = ctx.into_relayer().await;
2890
2891        // Use Stellar request instead of Solana
2892        let request = SponsoredTransactionBuildRequest::Stellar(
2893            crate::models::StellarPrepareTransactionRequestParams {
2894                transaction_xdr: Some("test-xdr".to_string()),
2895                operations: None,
2896                source_account: None,
2897                fee_token: "native".to_string(),
2898            },
2899        );
2900
2901        let result = solana_relayer.build_sponsored_transaction(request).await;
2902        assert!(result.is_err());
2903
2904        if let Err(RelayerError::ValidationError(msg)) = result {
2905            assert!(msg.contains("Expected Solana prepare transaction request parameters"));
2906        } else {
2907            panic!("Expected ValidationError for wrong network type");
2908        }
2909    }
2910}