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