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