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