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, GetStatusOptions, JsonRpcRequest, JsonRpcResponse,
25 NetworkRpcRequest, NetworkRpcResult, NetworkTransactionRequest, RelayerStatus, RepositoryError,
26 RpcErrorCodes, 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, options: GetStatusOptions) -> Result<RelayerStatus, RelayerError> {
1028 let balance = if options.include_balance {
1029 let address = &self.relayer.address;
1030 Some((self.provider.get_balance(address).await? as u128).to_string())
1031 } else {
1032 None
1033 };
1034
1035 let pending_transactions_count = if options.include_pending_count {
1036 Some(
1038 self.transaction_repository
1039 .count_by_status(&self.relayer.id, PENDING_TRANSACTION_STATUSES)
1040 .await
1041 .map_err(RelayerError::from)?,
1042 )
1043 } else {
1044 None
1045 };
1046
1047 let last_confirmed_transaction_timestamp = if options.include_last_confirmed_tx {
1048 self.transaction_repository
1050 .find_by_status_paginated(
1051 &self.relayer.id,
1052 &[TransactionStatus::Confirmed],
1053 PaginationQuery {
1054 page: 1,
1055 per_page: 1,
1056 },
1057 false, )
1059 .await
1060 .map_err(RelayerError::from)?
1061 .items
1062 .into_iter()
1063 .next()
1064 .and_then(|tx| tx.confirmed_at)
1065 } else {
1066 None
1067 };
1068
1069 Ok(RelayerStatus::Solana {
1070 balance,
1071 pending_transactions_count,
1072 last_confirmed_transaction_timestamp,
1073 system_disabled: self.relayer.system_disabled,
1074 paused: self.relayer.paused,
1075 })
1076 }
1077
1078 #[instrument(
1079 level = "debug",
1080 skip(self),
1081 fields(
1082 request_id = ?crate::observability::request_id::get_request_id(),
1083 relayer_id = %self.relayer.id,
1084 )
1085 )]
1086 async fn initialize_relayer(&self) -> Result<(), RelayerError> {
1087 debug!("initializing Solana relayer");
1088
1089 self.populate_allowed_tokens_metadata().await.map_err(|_| {
1092 RelayerError::PolicyConfigurationError(
1093 "Error while processing allowed tokens policy".into(),
1094 )
1095 })?;
1096
1097 self.validate_program_policy().await.map_err(|_| {
1100 RelayerError::PolicyConfigurationError(
1101 "Error while validating allowed programs policy".into(),
1102 )
1103 })?;
1104
1105 match self.check_health().await {
1106 Ok(_) => {
1107 if self.relayer.system_disabled {
1109 self.relayer_repository
1111 .enable_relayer(self.relayer.id.clone())
1112 .await?;
1113 }
1114 }
1115 Err(failures) => {
1116 let reason = DisabledReason::from_health_failures(failures).unwrap_or_else(|| {
1118 DisabledReason::RpcValidationFailed("Unknown error".to_string())
1119 });
1120
1121 warn!(reason = %reason, "disabling relayer");
1122 let updated_relayer = self
1123 .relayer_repository
1124 .disable_relayer(self.relayer.id.clone(), reason.clone())
1125 .await?;
1126
1127 if let Some(notification_id) = &self.relayer.notification_id {
1129 self.job_producer
1130 .produce_send_notification_job(
1131 produce_relayer_disabled_payload(
1132 notification_id,
1133 &updated_relayer,
1134 &reason.safe_description(),
1135 ),
1136 None,
1137 )
1138 .await?;
1139 }
1140
1141 self.job_producer
1143 .produce_relayer_health_check_job(
1144 RelayerHealthCheck::new(self.relayer.id.clone()),
1145 Some(calculate_scheduled_timestamp(10)),
1146 )
1147 .await?;
1148 }
1149 }
1150
1151 self.check_balance_and_trigger_token_swap_if_needed()
1152 .await?;
1153
1154 Ok(())
1155 }
1156
1157 #[instrument(
1158 level = "debug",
1159 skip(self),
1160 fields(
1161 request_id = ?crate::observability::request_id::get_request_id(),
1162 relayer_id = %self.relayer.id,
1163 )
1164 )]
1165 async fn check_health(&self) -> Result<(), Vec<HealthCheckFailure>> {
1166 debug!(
1167 "running health checks for Solana relayer {}",
1168 self.relayer.id
1169 );
1170
1171 let validate_rpc_result = self.validate_rpc().await;
1172 let validate_min_balance_result = self.validate_min_balance().await;
1173
1174 let failures: Vec<HealthCheckFailure> = vec![
1176 validate_rpc_result
1177 .err()
1178 .map(|e| HealthCheckFailure::RpcValidationFailed(e.to_string())),
1179 validate_min_balance_result
1180 .err()
1181 .map(|e| HealthCheckFailure::BalanceCheckFailed(e.to_string())),
1182 ]
1183 .into_iter()
1184 .flatten()
1185 .collect();
1186
1187 if failures.is_empty() {
1188 info!("all health checks passed");
1189 Ok(())
1190 } else {
1191 warn!("health checks failed: {:?}", failures);
1192 Err(failures)
1193 }
1194 }
1195
1196 #[instrument(
1197 level = "debug",
1198 skip(self),
1199 fields(
1200 request_id = ?crate::observability::request_id::get_request_id(),
1201 relayer_id = %self.relayer.id,
1202 )
1203 )]
1204 async fn validate_min_balance(&self) -> Result<(), RelayerError> {
1205 let balance = self
1206 .provider
1207 .get_balance(&self.relayer.address)
1208 .await
1209 .map_err(|e| RelayerError::ProviderError(e.to_string()))?;
1210
1211 debug!(balance = %balance, "balance for relayer");
1212
1213 let policy = self.relayer.policies.get_solana_policy();
1214
1215 if balance < policy.min_balance.unwrap_or(DEFAULT_SOLANA_MIN_BALANCE) {
1216 return Err(RelayerError::InsufficientBalanceError(
1217 "Insufficient balance".to_string(),
1218 ));
1219 }
1220
1221 Ok(())
1222 }
1223}
1224
1225#[async_trait]
1226impl<RR, TR, J, S, JS, SP, NR> GasAbstractionTrait for SolanaRelayer<RR, TR, J, S, JS, SP, NR>
1227where
1228 RR: Repository<RelayerRepoModel, String> + RelayerRepository + Send + Sync + 'static,
1229 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
1230 J: JobProducerTrait + Send + Sync + 'static,
1231 S: SolanaSignTrait + Signer + Send + Sync + 'static,
1232 JS: JupiterServiceTrait + Send + Sync + 'static,
1233 SP: SolanaProviderTrait + Send + Sync + 'static,
1234 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
1235{
1236 #[instrument(
1237 level = "debug",
1238 skip(self, params),
1239 fields(
1240 request_id = ?crate::observability::request_id::get_request_id(),
1241 relayer_id = %self.relayer.id,
1242 )
1243 )]
1244 async fn quote_sponsored_transaction(
1245 &self,
1246 params: SponsoredTransactionQuoteRequest,
1247 ) -> Result<SponsoredTransactionQuoteResponse, RelayerError> {
1248 let params = match params {
1249 SponsoredTransactionQuoteRequest::Solana(p) => p,
1250 _ => {
1251 return Err(RelayerError::ValidationError(
1252 "Expected Solana fee estimate request parameters".to_string(),
1253 ));
1254 }
1255 };
1256
1257 let result = self
1258 .rpc_handler
1259 .rpc_methods()
1260 .fee_estimate(params)
1261 .await
1262 .map_err(|e| RelayerError::Internal(e.to_string()))?;
1263
1264 Ok(SponsoredTransactionQuoteResponse::Solana(result))
1265 }
1266
1267 #[instrument(
1268 level = "debug",
1269 skip(self, params),
1270 fields(
1271 request_id = ?crate::observability::request_id::get_request_id(),
1272 relayer_id = %self.relayer.id,
1273 )
1274 )]
1275 async fn build_sponsored_transaction(
1276 &self,
1277 params: SponsoredTransactionBuildRequest,
1278 ) -> Result<SponsoredTransactionBuildResponse, RelayerError> {
1279 let params = match params {
1280 SponsoredTransactionBuildRequest::Solana(p) => p,
1281 _ => {
1282 return Err(RelayerError::ValidationError(
1283 "Expected Solana prepare transaction request parameters".to_string(),
1284 ));
1285 }
1286 };
1287
1288 let result = self
1289 .rpc_handler
1290 .rpc_methods()
1291 .prepare_transaction(params)
1292 .await
1293 .map_err(|e| {
1294 let error_msg = format!("{e}");
1295 RelayerError::Internal(error_msg)
1296 })?;
1297
1298 Ok(SponsoredTransactionBuildResponse::Solana(result))
1299 }
1300}
1301
1302#[cfg(test)]
1303mod tests {
1304 use super::*;
1305 use crate::{
1306 config::{NetworkConfigCommon, SolanaNetworkConfig},
1307 domain::{
1308 create_network_dex_generic, Relayer, SignTransactionRequestSolana, SolanaRpcHandler,
1309 SolanaRpcMethodsImpl,
1310 },
1311 jobs::MockJobProducerTrait,
1312 models::{
1313 EncodedSerializedTransaction, JsonRpcId, NetworkConfigData, NetworkRepoModel,
1314 RelayerSolanaSwapConfig, RpcConfig, SolanaAllowedTokensSwapConfig,
1315 SolanaFeeEstimateRequestParams, SolanaGetFeaturesEnabledRequestParams, SolanaRpcResult,
1316 SolanaSwapStrategy,
1317 },
1318 repositories::{MockNetworkRepository, MockRelayerRepository, MockTransactionRepository},
1319 services::{
1320 provider::{MockSolanaProviderTrait, SolanaProviderError},
1321 signer::MockSolanaSignTrait,
1322 MockJupiterServiceTrait, QuoteResponse, RoutePlan, SwapEvents, SwapInfo, SwapResponse,
1323 UltraExecuteResponse, UltraOrderResponse,
1324 },
1325 utils::mocks::mockutils::create_mock_solana_network,
1326 };
1327 use chrono::Utc;
1328 use mockall::predicate::*;
1329 use solana_sdk::{hash::Hash, program_pack::Pack, signature::Signature};
1330 use spl_token_interface::state::Account as SplAccount;
1331
1332 #[allow(dead_code)]
1335 struct TestCtx {
1336 relayer_model: RelayerRepoModel,
1337 mock_repo: MockRelayerRepository,
1338 network_repository: Arc<MockNetworkRepository>,
1339 provider: Arc<MockSolanaProviderTrait>,
1340 signer: Arc<MockSolanaSignTrait>,
1341 jupiter: Arc<MockJupiterServiceTrait>,
1342 job_producer: Arc<MockJobProducerTrait>,
1343 tx_repo: Arc<MockTransactionRepository>,
1344 dex: Arc<NetworkDex<MockSolanaProviderTrait, MockSolanaSignTrait, MockJupiterServiceTrait>>,
1345 rpc_handler: SolanaRpcHandlerType<
1346 MockSolanaProviderTrait,
1347 MockSolanaSignTrait,
1348 MockJupiterServiceTrait,
1349 MockJobProducerTrait,
1350 MockTransactionRepository,
1351 >,
1352 }
1353
1354 impl Default for TestCtx {
1355 fn default() -> Self {
1356 let mock_repo = MockRelayerRepository::new();
1357 let provider = Arc::new(MockSolanaProviderTrait::new());
1358 let signer = Arc::new(MockSolanaSignTrait::new());
1359 let jupiter = Arc::new(MockJupiterServiceTrait::new());
1360 let job = Arc::new(MockJobProducerTrait::new());
1361 let tx_repo = Arc::new(MockTransactionRepository::new());
1362 let mut network_repository = MockNetworkRepository::new();
1363 let transaction_repository = Arc::new(MockTransactionRepository::new());
1364
1365 let relayer_model = RelayerRepoModel {
1366 id: "test-id".to_string(),
1367 address: "...".to_string(),
1368 network: "devnet".to_string(),
1369 ..Default::default()
1370 };
1371
1372 let dex = Arc::new(
1373 create_network_dex_generic(
1374 &relayer_model,
1375 provider.clone(),
1376 signer.clone(),
1377 jupiter.clone(),
1378 )
1379 .unwrap(),
1380 );
1381
1382 let test_network = create_mock_solana_network();
1383
1384 let rpc_handler = Arc::new(SolanaRpcHandler::new(SolanaRpcMethodsImpl::new_mock(
1385 relayer_model.clone(),
1386 test_network.clone(),
1387 provider.clone(),
1388 signer.clone(),
1389 jupiter.clone(),
1390 job.clone(),
1391 transaction_repository.clone(),
1392 )));
1393
1394 let test_network = NetworkRepoModel {
1395 id: "solana:devnet".to_string(),
1396 name: "devnet".to_string(),
1397 network_type: NetworkType::Solana,
1398 config: NetworkConfigData::Solana(SolanaNetworkConfig {
1399 common: NetworkConfigCommon {
1400 network: "devnet".to_string(),
1401 from: None,
1402 rpc_urls: Some(vec![RpcConfig::new(
1403 "https://api.devnet.solana.com".to_string(),
1404 )]),
1405 explorer_urls: None,
1406 average_blocktime_ms: Some(400),
1407 is_testnet: Some(true),
1408 tags: None,
1409 },
1410 }),
1411 };
1412
1413 network_repository
1414 .expect_get_by_name()
1415 .returning(move |_, _| Ok(Some(test_network.clone())));
1416
1417 TestCtx {
1418 relayer_model,
1419 mock_repo,
1420 network_repository: Arc::new(network_repository),
1421 provider,
1422 signer,
1423 jupiter,
1424 job_producer: job,
1425 tx_repo,
1426 dex,
1427 rpc_handler,
1428 }
1429 }
1430 }
1431
1432 impl TestCtx {
1433 async fn into_relayer(
1434 self,
1435 ) -> SolanaRelayer<
1436 MockRelayerRepository,
1437 MockTransactionRepository,
1438 MockJobProducerTrait,
1439 MockSolanaSignTrait,
1440 MockJupiterServiceTrait,
1441 MockSolanaProviderTrait,
1442 MockNetworkRepository,
1443 > {
1444 let network_repo = self
1446 .network_repository
1447 .get_by_name(NetworkType::Solana, "devnet")
1448 .await
1449 .unwrap()
1450 .unwrap();
1451 let network = SolanaNetwork::try_from(network_repo).unwrap();
1452
1453 SolanaRelayer {
1454 relayer: self.relayer_model.clone(),
1455 signer: self.signer,
1456 network,
1457 provider: self.provider,
1458 rpc_handler: self.rpc_handler,
1459 relayer_repository: Arc::new(self.mock_repo),
1460 transaction_repository: self.tx_repo,
1461 job_producer: self.job_producer,
1462 dex_service: self.dex,
1463 network_repository: self.network_repository,
1464 }
1465 }
1466 }
1467
1468 fn create_test_relayer() -> RelayerRepoModel {
1469 RelayerRepoModel {
1470 id: "test-relayer-id".to_string(),
1471 address: "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin".to_string(),
1472 notification_id: Some("test-notification-id".to_string()),
1473 network_type: NetworkType::Solana,
1474 policies: RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
1475 min_balance: Some(0), swap_config: None,
1477 ..Default::default()
1478 }),
1479 ..Default::default()
1480 }
1481 }
1482
1483 fn create_token_policy(
1484 mint: &str,
1485 min_amount: Option<u64>,
1486 max_amount: Option<u64>,
1487 retain_min: Option<u64>,
1488 slippage: Option<u64>,
1489 ) -> SolanaAllowedTokensPolicy {
1490 let mut token = SolanaAllowedTokensPolicy {
1491 mint: mint.to_string(),
1492 max_allowed_fee: Some(0),
1493 swap_config: None,
1494 decimals: Some(9),
1495 symbol: Some("SOL".to_string()),
1496 };
1497
1498 let swap_config = SolanaAllowedTokensSwapConfig {
1499 min_amount,
1500 max_amount,
1501 retain_min_amount: retain_min,
1502 slippage_percentage: slippage.map(|s| s as f32),
1503 };
1504
1505 token.swap_config = Some(swap_config);
1506 token
1507 }
1508
1509 #[tokio::test]
1510 async fn test_calculate_swap_amount_no_limits() {
1511 let ctx = TestCtx::default();
1512 let solana_relayer = ctx.into_relayer().await;
1513
1514 assert_eq!(
1515 solana_relayer
1516 .calculate_swap_amount(100, None, None, None)
1517 .unwrap(),
1518 100
1519 );
1520 }
1521
1522 #[tokio::test]
1523 async fn test_calculate_swap_amount_with_max() {
1524 let ctx = TestCtx::default();
1525 let solana_relayer = ctx.into_relayer().await;
1526
1527 assert_eq!(
1528 solana_relayer
1529 .calculate_swap_amount(100, None, Some(60), None)
1530 .unwrap(),
1531 60
1532 );
1533 }
1534
1535 #[tokio::test]
1536 async fn test_calculate_swap_amount_with_retain() {
1537 let ctx = TestCtx::default();
1538 let solana_relayer = ctx.into_relayer().await;
1539
1540 assert_eq!(
1541 solana_relayer
1542 .calculate_swap_amount(100, None, None, Some(30))
1543 .unwrap(),
1544 70
1545 );
1546
1547 assert_eq!(
1548 solana_relayer
1549 .calculate_swap_amount(20, None, None, Some(30))
1550 .unwrap(),
1551 0
1552 );
1553 }
1554
1555 #[tokio::test]
1556 async fn test_calculate_swap_amount_with_min() {
1557 let ctx = TestCtx::default();
1558 let solana_relayer = ctx.into_relayer().await;
1559
1560 assert_eq!(
1561 solana_relayer
1562 .calculate_swap_amount(40, Some(50), None, None)
1563 .unwrap(),
1564 0
1565 );
1566
1567 assert_eq!(
1568 solana_relayer
1569 .calculate_swap_amount(100, Some(50), None, None)
1570 .unwrap(),
1571 100
1572 );
1573 }
1574
1575 #[tokio::test]
1576 async fn test_calculate_swap_amount_combined() {
1577 let ctx = TestCtx::default();
1578 let solana_relayer = ctx.into_relayer().await;
1579
1580 assert_eq!(
1581 solana_relayer
1582 .calculate_swap_amount(100, None, Some(50), Some(30))
1583 .unwrap(),
1584 50
1585 );
1586
1587 assert_eq!(
1588 solana_relayer
1589 .calculate_swap_amount(100, Some(20), Some(50), Some(30))
1590 .unwrap(),
1591 50
1592 );
1593
1594 assert_eq!(
1595 solana_relayer
1596 .calculate_swap_amount(100, Some(60), Some(50), Some(30))
1597 .unwrap(),
1598 0
1599 );
1600 }
1601
1602 #[tokio::test]
1603 async fn test_handle_token_swap_request_successful_swap_jupiter_swap_strategy() {
1604 let mut relayer_model = create_test_relayer();
1605
1606 let mut mock_relayer_repo = MockRelayerRepository::new();
1607 let id = relayer_model.id.clone();
1608
1609 relayer_model.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
1610 swap_config: Some(RelayerSolanaSwapConfig {
1611 strategy: Some(SolanaSwapStrategy::JupiterSwap),
1612 cron_schedule: None,
1613 min_balance_threshold: None,
1614 jupiter_swap_options: None,
1615 }),
1616 allowed_tokens: Some(vec![create_token_policy(
1617 "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
1618 Some(1),
1619 None,
1620 None,
1621 Some(50),
1622 )]),
1623 ..Default::default()
1624 });
1625 let cloned = relayer_model.clone();
1626
1627 mock_relayer_repo
1628 .expect_get_by_id()
1629 .with(eq(id.clone()))
1630 .times(1)
1631 .returning(move |_| Ok(cloned.clone()));
1632
1633 let mut raw_provider = MockSolanaProviderTrait::new();
1634
1635 raw_provider
1636 .expect_get_account_from_pubkey()
1637 .returning(|_| {
1638 Box::pin(async {
1639 let mut account_data = vec![0; SplAccount::LEN];
1640
1641 let token_account = spl_token_interface::state::Account {
1642 mint: Pubkey::new_unique(),
1643 owner: Pubkey::new_unique(),
1644 amount: 10000000,
1645 state: spl_token_interface::state::AccountState::Initialized,
1646 ..Default::default()
1647 };
1648 spl_token_interface::state::Account::pack(token_account, &mut account_data)
1649 .unwrap();
1650
1651 Ok(solana_sdk::account::Account {
1652 lamports: 1_000_000,
1653 data: account_data,
1654 owner: spl_token_interface::id(),
1655 executable: false,
1656 rent_epoch: 0,
1657 })
1658 })
1659 });
1660
1661 let mut jupiter_mock = MockJupiterServiceTrait::new();
1662
1663 jupiter_mock.expect_get_quote().returning(|_| {
1664 Box::pin(async {
1665 Ok(QuoteResponse {
1666 input_mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
1667 output_mint: WRAPPED_SOL_MINT.to_string(),
1668 in_amount: 10,
1669 out_amount: 10,
1670 other_amount_threshold: 1,
1671 swap_mode: "ExactIn".to_string(),
1672 price_impact_pct: 0.0,
1673 route_plan: vec![RoutePlan {
1674 percent: 100,
1675 swap_info: SwapInfo {
1676 amm_key: "mock_amm_key".to_string(),
1677 label: "mock_label".to_string(),
1678 input_mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
1679 output_mint: WRAPPED_SOL_MINT.to_string(),
1680 in_amount: "1000".to_string(),
1681 out_amount: "1000".to_string(),
1682 fee_amount: Some("0".to_string()),
1683 fee_mint: Some("mock_fee_mint".to_string()),
1684 },
1685 }],
1686 slippage_bps: 0,
1687 })
1688 })
1689 });
1690
1691 jupiter_mock.expect_get_swap_transaction().returning(|_| {
1692 Box::pin(async {
1693 Ok(SwapResponse {
1694 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(),
1695 last_valid_block_height: 100,
1696 prioritization_fee_lamports: None,
1697 compute_unit_limit: None,
1698 simulation_error: None,
1699 })
1700 })
1701 });
1702
1703 let mut signer = MockSolanaSignTrait::new();
1704 let test_signature = Signature::from_str("2jg9xbGLtZRsiJBrDWQnz33JuLjDkiKSZuxZPdjJ3qrJbMeTEerXFAKynkPW63J88nq63cvosDNRsg9VqHtGixvP").unwrap();
1705
1706 signer
1707 .expect_sign()
1708 .times(1)
1709 .returning(move |_| Box::pin(async move { Ok(test_signature) }));
1710
1711 raw_provider
1712 .expect_send_versioned_transaction()
1713 .times(1)
1714 .returning(move |_| Box::pin(async move { Ok(test_signature) }));
1715
1716 raw_provider
1717 .expect_confirm_transaction()
1718 .times(1)
1719 .returning(move |_| Box::pin(async move { Ok(true) }));
1720
1721 let provider_arc = Arc::new(raw_provider);
1722 let jupiter_arc = Arc::new(jupiter_mock);
1723 let signer_arc = Arc::new(signer);
1724
1725 let dex = Arc::new(
1726 create_network_dex_generic(
1727 &relayer_model,
1728 provider_arc.clone(),
1729 signer_arc.clone(),
1730 jupiter_arc.clone(),
1731 )
1732 .unwrap(),
1733 );
1734
1735 let mut job_producer = MockJobProducerTrait::new();
1736 job_producer
1737 .expect_produce_send_notification_job()
1738 .times(1)
1739 .returning(|_, _| Box::pin(async { Ok(()) }));
1740
1741 let job_producer_arc = Arc::new(job_producer);
1742
1743 let ctx = TestCtx {
1744 relayer_model,
1745 mock_repo: mock_relayer_repo,
1746 provider: provider_arc.clone(),
1747 jupiter: jupiter_arc.clone(),
1748 signer: signer_arc.clone(),
1749 dex,
1750 job_producer: job_producer_arc.clone(),
1751 ..Default::default()
1752 };
1753 let solana_relayer = ctx.into_relayer().await;
1754 let res = solana_relayer
1755 .handle_token_swap_request(create_test_relayer().id)
1756 .await
1757 .unwrap();
1758 assert_eq!(res.len(), 1);
1759 let swap = &res[0];
1760 assert_eq!(swap.source_amount, 10000000);
1761 assert_eq!(swap.destination_amount, 10);
1762 assert_eq!(swap.transaction_signature, test_signature.to_string());
1763 }
1764
1765 #[tokio::test]
1766 async fn test_handle_token_swap_request_successful_swap_jupiter_ultra_strategy() {
1767 let mut relayer_model = create_test_relayer();
1768
1769 let mut mock_relayer_repo = MockRelayerRepository::new();
1770 let id = relayer_model.id.clone();
1771
1772 relayer_model.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
1773 swap_config: Some(RelayerSolanaSwapConfig {
1774 strategy: Some(SolanaSwapStrategy::JupiterUltra),
1775 cron_schedule: None,
1776 min_balance_threshold: None,
1777 jupiter_swap_options: None,
1778 }),
1779 allowed_tokens: Some(vec![create_token_policy(
1780 "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
1781 Some(1),
1782 None,
1783 None,
1784 Some(50),
1785 )]),
1786 ..Default::default()
1787 });
1788 let cloned = relayer_model.clone();
1789
1790 mock_relayer_repo
1791 .expect_get_by_id()
1792 .with(eq(id.clone()))
1793 .times(1)
1794 .returning(move |_| Ok(cloned.clone()));
1795
1796 let mut raw_provider = MockSolanaProviderTrait::new();
1797
1798 raw_provider
1799 .expect_get_account_from_pubkey()
1800 .returning(|_| {
1801 Box::pin(async {
1802 let mut account_data = vec![0; SplAccount::LEN];
1803
1804 let token_account = spl_token_interface::state::Account {
1805 mint: Pubkey::new_unique(),
1806 owner: Pubkey::new_unique(),
1807 amount: 10000000,
1808 state: spl_token_interface::state::AccountState::Initialized,
1809 ..Default::default()
1810 };
1811 spl_token_interface::state::Account::pack(token_account, &mut account_data)
1812 .unwrap();
1813
1814 Ok(solana_sdk::account::Account {
1815 lamports: 1_000_000,
1816 data: account_data,
1817 owner: spl_token_interface::id(),
1818 executable: false,
1819 rent_epoch: 0,
1820 })
1821 })
1822 });
1823
1824 let mut jupiter_mock = MockJupiterServiceTrait::new();
1825 jupiter_mock.expect_get_ultra_order().returning(|_| {
1826 Box::pin(async {
1827 Ok(UltraOrderResponse {
1828 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()),
1829 input_mint: "PjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
1830 output_mint: WRAPPED_SOL_MINT.to_string(),
1831 in_amount: 10,
1832 out_amount: 10,
1833 other_amount_threshold: 1,
1834 swap_mode: "ExactIn".to_string(),
1835 price_impact_pct: 0.0,
1836 route_plan: vec![RoutePlan {
1837 percent: 100,
1838 swap_info: SwapInfo {
1839 amm_key: "mock_amm_key".to_string(),
1840 label: "mock_label".to_string(),
1841 input_mint: "PjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
1842 output_mint: WRAPPED_SOL_MINT.to_string(),
1843 in_amount: "1000".to_string(),
1844 out_amount: "1000".to_string(),
1845 fee_amount: Some("0".to_string()),
1846 fee_mint: Some("mock_fee_mint".to_string()),
1847 },
1848 }],
1849 prioritization_fee_lamports: 0,
1850 request_id: "mock_request_id".to_string(),
1851 slippage_bps: 0,
1852 })
1853 })
1854 });
1855
1856 jupiter_mock.expect_execute_ultra_order().returning(|_| {
1857 Box::pin(async {
1858 Ok(UltraExecuteResponse {
1859 signature: Some("2jg9xbGLtZRsiJBrDWQnz33JuLjDkiKSZuxZPdjJ3qrJbMeTEerXFAKynkPW63J88nq63cvosDNRsg9VqHtGixvP".to_string()),
1860 status: "success".to_string(),
1861 slot: Some("123456789".to_string()),
1862 error: None,
1863 code: 0,
1864 total_input_amount: Some("1000000".to_string()),
1865 total_output_amount: Some("1000000".to_string()),
1866 input_amount_result: Some("1000000".to_string()),
1867 output_amount_result: Some("1000000".to_string()),
1868 swap_events: Some(vec![SwapEvents {
1869 input_mint: "mock_input_mint".to_string(),
1870 output_mint: "mock_output_mint".to_string(),
1871 input_amount: "1000000".to_string(),
1872 output_amount: "1000000".to_string(),
1873 }]),
1874 })
1875 })
1876 });
1877
1878 let mut signer = MockSolanaSignTrait::new();
1879 let test_signature = Signature::from_str("2jg9xbGLtZRsiJBrDWQnz33JuLjDkiKSZuxZPdjJ3qrJbMeTEerXFAKynkPW63J88nq63cvosDNRsg9VqHtGixvP").unwrap();
1880
1881 signer
1882 .expect_sign()
1883 .times(1)
1884 .returning(move |_| Box::pin(async move { Ok(test_signature) }));
1885
1886 let provider_arc = Arc::new(raw_provider);
1887 let jupiter_arc = Arc::new(jupiter_mock);
1888 let signer_arc = Arc::new(signer);
1889
1890 let dex = Arc::new(
1891 create_network_dex_generic(
1892 &relayer_model,
1893 provider_arc.clone(),
1894 signer_arc.clone(),
1895 jupiter_arc.clone(),
1896 )
1897 .unwrap(),
1898 );
1899
1900 let mut job_producer = MockJobProducerTrait::new();
1901 job_producer
1902 .expect_produce_send_notification_job()
1903 .times(1)
1904 .returning(|_, _| Box::pin(async { Ok(()) }));
1905
1906 let job_producer_arc = Arc::new(job_producer);
1907
1908 let ctx = TestCtx {
1909 relayer_model,
1910 mock_repo: mock_relayer_repo,
1911 provider: provider_arc.clone(),
1912 jupiter: jupiter_arc.clone(),
1913 signer: signer_arc.clone(),
1914 dex,
1915 job_producer: job_producer_arc.clone(),
1916 ..Default::default()
1917 };
1918 let solana_relayer = ctx.into_relayer().await;
1919
1920 let res = solana_relayer
1921 .handle_token_swap_request(create_test_relayer().id)
1922 .await
1923 .unwrap();
1924 assert_eq!(res.len(), 1);
1925 let swap = &res[0];
1926 assert_eq!(swap.source_amount, 10000000);
1927 assert_eq!(swap.destination_amount, 10);
1928 assert_eq!(swap.transaction_signature, test_signature.to_string());
1929 }
1930
1931 #[tokio::test]
1932 async fn test_handle_token_swap_request_no_swap_config() {
1933 let mut relayer_model = create_test_relayer();
1934
1935 let mut mock_relayer_repo = MockRelayerRepository::new();
1936 let id = relayer_model.id.clone();
1937 let cloned = relayer_model.clone();
1938 mock_relayer_repo
1939 .expect_get_by_id()
1940 .with(eq(id.clone()))
1941 .times(1)
1942 .returning(move |_| Ok(cloned.clone()));
1943
1944 relayer_model.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
1945 swap_config: Some(RelayerSolanaSwapConfig {
1946 strategy: Some(SolanaSwapStrategy::JupiterSwap),
1947 cron_schedule: None,
1948 min_balance_threshold: None,
1949 jupiter_swap_options: None,
1950 }),
1951 allowed_tokens: Some(vec![create_token_policy(
1952 "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
1953 Some(1),
1954 None,
1955 None,
1956 Some(50),
1957 )]),
1958 ..Default::default()
1959 });
1960 let mut job_producer = MockJobProducerTrait::new();
1961 job_producer.expect_produce_send_notification_job().times(0);
1962
1963 let job_producer_arc = Arc::new(job_producer);
1964
1965 let ctx = TestCtx {
1966 relayer_model,
1967 mock_repo: mock_relayer_repo,
1968 job_producer: job_producer_arc,
1969 ..Default::default()
1970 };
1971 let solana_relayer = ctx.into_relayer().await;
1972
1973 let res = solana_relayer.handle_token_swap_request(id).await;
1974 assert!(res.is_ok());
1975 assert!(res.unwrap().is_empty());
1976 }
1977
1978 #[tokio::test]
1979 async fn test_handle_token_swap_request_no_strategy() {
1980 let mut relayer_model: RelayerRepoModel = create_test_relayer();
1981
1982 let mut mock_relayer_repo = MockRelayerRepository::new();
1983 let id = relayer_model.id.clone();
1984 let cloned = relayer_model.clone();
1985 mock_relayer_repo
1986 .expect_get_by_id()
1987 .with(eq(id.clone()))
1988 .times(1)
1989 .returning(move |_| Ok(cloned.clone()));
1990
1991 relayer_model.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
1992 swap_config: Some(RelayerSolanaSwapConfig {
1993 strategy: None,
1994 cron_schedule: None,
1995 min_balance_threshold: Some(1),
1996 jupiter_swap_options: None,
1997 }),
1998 ..Default::default()
1999 });
2000
2001 let ctx = TestCtx {
2002 relayer_model,
2003 mock_repo: mock_relayer_repo,
2004 ..Default::default()
2005 };
2006 let solana_relayer = ctx.into_relayer().await;
2007
2008 let res = solana_relayer.handle_token_swap_request(id).await.unwrap();
2009 assert!(res.is_empty(), "should return empty when no strategy");
2010 }
2011
2012 #[tokio::test]
2013 async fn test_handle_token_swap_request_no_allowed_tokens() {
2014 let mut relayer_model: RelayerRepoModel = create_test_relayer();
2015 let mut mock_relayer_repo = MockRelayerRepository::new();
2016 let id = relayer_model.id.clone();
2017 let cloned = relayer_model.clone();
2018 mock_relayer_repo
2019 .expect_get_by_id()
2020 .with(eq(id.clone()))
2021 .times(1)
2022 .returning(move |_| Ok(cloned.clone()));
2023
2024 relayer_model.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
2025 swap_config: Some(RelayerSolanaSwapConfig {
2026 strategy: Some(SolanaSwapStrategy::JupiterSwap),
2027 cron_schedule: None,
2028 min_balance_threshold: Some(1),
2029 jupiter_swap_options: None,
2030 }),
2031 allowed_tokens: None,
2032 ..Default::default()
2033 });
2034
2035 let ctx = TestCtx {
2036 relayer_model,
2037 mock_repo: mock_relayer_repo,
2038 ..Default::default()
2039 };
2040 let solana_relayer = ctx.into_relayer().await;
2041
2042 let res = solana_relayer.handle_token_swap_request(id).await.unwrap();
2043 assert!(res.is_empty(), "should return empty when no allowed_tokens");
2044 }
2045
2046 #[tokio::test]
2047 async fn test_validate_rpc_success() {
2048 let mut raw_provider = MockSolanaProviderTrait::new();
2049 raw_provider
2050 .expect_get_latest_blockhash()
2051 .times(1)
2052 .returning(|| Box::pin(async { Ok(Hash::new_unique()) }));
2053
2054 let ctx = TestCtx {
2055 provider: Arc::new(raw_provider),
2056 ..Default::default()
2057 };
2058 let solana_relayer = ctx.into_relayer().await;
2059 let res = solana_relayer.validate_rpc().await;
2060
2061 assert!(
2062 res.is_ok(),
2063 "validate_rpc should succeed when blockhash fetch succeeds"
2064 );
2065 }
2066
2067 #[tokio::test]
2068 async fn test_validate_rpc_provider_error() {
2069 let mut raw_provider = MockSolanaProviderTrait::new();
2070 raw_provider
2071 .expect_get_latest_blockhash()
2072 .times(1)
2073 .returning(|| {
2074 Box::pin(async { Err(SolanaProviderError::RpcError("rpc failure".to_string())) })
2075 });
2076
2077 let ctx = TestCtx {
2078 provider: Arc::new(raw_provider),
2079 ..Default::default()
2080 };
2081
2082 let solana_relayer = ctx.into_relayer().await;
2083 let err = solana_relayer.validate_rpc().await.unwrap_err();
2084
2085 match err {
2086 RelayerError::ProviderError(msg) => {
2087 assert!(msg.contains("rpc failure"));
2088 }
2089 other => panic!("expected ProviderError, got {:?}", other),
2090 }
2091 }
2092
2093 #[tokio::test]
2094 async fn test_check_balance_no_swap_config() {
2095 let ctx = TestCtx::default();
2097 let solana_relayer = ctx.into_relayer().await;
2098
2099 assert!(solana_relayer
2101 .check_balance_and_trigger_token_swap_if_needed()
2102 .await
2103 .is_ok());
2104 }
2105
2106 #[tokio::test]
2107 async fn test_check_balance_no_threshold() {
2108 let mut ctx = TestCtx::default();
2110 let mut model = ctx.relayer_model.clone();
2111 model.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
2112 swap_config: Some(RelayerSolanaSwapConfig {
2113 strategy: Some(SolanaSwapStrategy::JupiterSwap),
2114 cron_schedule: None,
2115 min_balance_threshold: None,
2116 jupiter_swap_options: None,
2117 }),
2118 ..Default::default()
2119 });
2120 ctx.relayer_model = model;
2121 let solana_relayer = ctx.into_relayer().await;
2122
2123 assert!(solana_relayer
2124 .check_balance_and_trigger_token_swap_if_needed()
2125 .await
2126 .is_ok());
2127 }
2128
2129 #[tokio::test]
2130 async fn test_check_balance_above_threshold() {
2131 let mut raw_provider = MockSolanaProviderTrait::new();
2132 raw_provider
2133 .expect_get_balance()
2134 .times(1)
2135 .returning(|_| Box::pin(async { Ok(20_u64) }));
2136 let provider = Arc::new(raw_provider);
2137 let mut raw_job = MockJobProducerTrait::new();
2138 raw_job
2139 .expect_produce_token_swap_request_job()
2140 .withf(move |req, _opts| req.relayer_id == "test-id")
2141 .times(0);
2142 let job_producer = Arc::new(raw_job);
2143
2144 let ctx = TestCtx {
2145 provider,
2146 job_producer,
2147 ..Default::default()
2148 };
2149 let mut model = ctx.relayer_model.clone();
2151 model.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
2152 swap_config: Some(RelayerSolanaSwapConfig {
2153 strategy: Some(SolanaSwapStrategy::JupiterSwap),
2154 cron_schedule: None,
2155 min_balance_threshold: Some(10),
2156 jupiter_swap_options: None,
2157 }),
2158 ..Default::default()
2159 });
2160 let mut ctx = ctx;
2161 ctx.relayer_model = model;
2162
2163 let solana_relayer = ctx.into_relayer().await;
2164 assert!(solana_relayer
2165 .check_balance_and_trigger_token_swap_if_needed()
2166 .await
2167 .is_ok());
2168 }
2169
2170 #[tokio::test]
2171 async fn test_check_balance_below_threshold_triggers_job() {
2172 let mut raw_provider = MockSolanaProviderTrait::new();
2173 raw_provider
2174 .expect_get_balance()
2175 .times(1)
2176 .returning(|_| Box::pin(async { Ok(5_u64) }));
2177
2178 let mut raw_job = MockJobProducerTrait::new();
2179 raw_job
2180 .expect_produce_token_swap_request_job()
2181 .times(1)
2182 .returning(|_, _| Box::pin(async { Ok(()) }));
2183 let job_producer = Arc::new(raw_job);
2184
2185 let mut model = create_test_relayer();
2186 model.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
2187 swap_config: Some(RelayerSolanaSwapConfig {
2188 strategy: Some(SolanaSwapStrategy::JupiterSwap),
2189 cron_schedule: None,
2190 min_balance_threshold: Some(10),
2191 jupiter_swap_options: None,
2192 }),
2193 ..Default::default()
2194 });
2195
2196 let ctx = TestCtx {
2197 relayer_model: model,
2198 provider: Arc::new(raw_provider),
2199 job_producer,
2200 ..Default::default()
2201 };
2202
2203 let solana_relayer = ctx.into_relayer().await;
2204 assert!(solana_relayer
2205 .check_balance_and_trigger_token_swap_if_needed()
2206 .await
2207 .is_ok());
2208 }
2209
2210 #[tokio::test]
2211 async fn test_get_balance_success() {
2212 let mut raw_provider = MockSolanaProviderTrait::new();
2213 raw_provider
2214 .expect_get_balance()
2215 .times(1)
2216 .returning(|_| Box::pin(async { Ok(42_u64) }));
2217 let ctx = TestCtx {
2218 provider: Arc::new(raw_provider),
2219 ..Default::default()
2220 };
2221 let solana_relayer = ctx.into_relayer().await;
2222
2223 let res = solana_relayer.get_balance().await.unwrap();
2224
2225 assert_eq!(res.balance, 42_u128);
2226 assert_eq!(res.unit, SOLANA_SMALLEST_UNIT_NAME);
2227 }
2228
2229 #[tokio::test]
2230 async fn test_get_balance_provider_error() {
2231 let mut raw_provider = MockSolanaProviderTrait::new();
2232 raw_provider
2233 .expect_get_balance()
2234 .times(1)
2235 .returning(|_| Box::pin(async { Err(SolanaProviderError::RpcError("oops".into())) }));
2236 let ctx = TestCtx {
2237 provider: Arc::new(raw_provider),
2238 ..Default::default()
2239 };
2240 let solana_relayer = ctx.into_relayer().await;
2241
2242 let err = solana_relayer.get_balance().await.unwrap_err();
2243
2244 match err {
2245 RelayerError::UnderlyingSolanaProvider(err) => {
2246 assert!(err.to_string().contains("oops"));
2247 }
2248 other => panic!("expected ProviderError, got {:?}", other),
2249 }
2250 }
2251
2252 #[tokio::test]
2253 async fn test_validate_min_balance_success() {
2254 let mut raw_provider = MockSolanaProviderTrait::new();
2255 raw_provider
2256 .expect_get_balance()
2257 .times(1)
2258 .returning(|_| Box::pin(async { Ok(100_u64) }));
2259
2260 let mut model = create_test_relayer();
2261 model.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
2262 min_balance: Some(50),
2263 ..Default::default()
2264 });
2265
2266 let ctx = TestCtx {
2267 relayer_model: model,
2268 provider: Arc::new(raw_provider),
2269 ..Default::default()
2270 };
2271
2272 let solana_relayer = ctx.into_relayer().await;
2273 assert!(solana_relayer.validate_min_balance().await.is_ok());
2274 }
2275
2276 #[tokio::test]
2277 async fn test_validate_min_balance_insufficient() {
2278 let mut raw_provider = MockSolanaProviderTrait::new();
2279 raw_provider
2280 .expect_get_balance()
2281 .times(1)
2282 .returning(|_| Box::pin(async { Ok(10_u64) }));
2283
2284 let mut model = create_test_relayer();
2285 model.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
2286 min_balance: Some(50),
2287 ..Default::default()
2288 });
2289
2290 let ctx = TestCtx {
2291 relayer_model: model,
2292 provider: Arc::new(raw_provider),
2293 ..Default::default()
2294 };
2295
2296 let solana_relayer = ctx.into_relayer().await;
2297 let err = solana_relayer.validate_min_balance().await.unwrap_err();
2298 match err {
2299 RelayerError::InsufficientBalanceError(msg) => {
2300 assert_eq!(msg, "Insufficient balance");
2301 }
2302 other => panic!("expected InsufficientBalanceError, got {:?}", other),
2303 }
2304 }
2305
2306 #[tokio::test]
2307 async fn test_validate_min_balance_provider_error() {
2308 let mut raw_provider = MockSolanaProviderTrait::new();
2309 raw_provider
2310 .expect_get_balance()
2311 .times(1)
2312 .returning(|_| Box::pin(async { Err(SolanaProviderError::RpcError("fail".into())) }));
2313 let ctx = TestCtx {
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::ProviderError(msg) => {
2322 assert!(msg.contains("fail"));
2323 }
2324 other => panic!("expected ProviderError, got {:?}", other),
2325 }
2326 }
2327
2328 #[tokio::test]
2329 async fn test_rpc_invalid_params() {
2330 let ctx = TestCtx::default();
2331 let solana_relayer = ctx.into_relayer().await;
2332
2333 let req = JsonRpcRequest {
2334 jsonrpc: "2.0".to_string(),
2335 params: NetworkRpcRequest::Solana(crate::models::SolanaRpcRequest::FeeEstimate(
2336 SolanaFeeEstimateRequestParams {
2337 transaction: EncodedSerializedTransaction::new("".to_string()),
2338 fee_token: "".to_string(),
2339 },
2340 )),
2341 id: Some(JsonRpcId::Number(1)),
2342 };
2343 let resp = solana_relayer.rpc(req).await.unwrap();
2344
2345 assert!(resp.error.is_some(), "expected an error object");
2346 let err = resp.error.unwrap();
2347 assert_eq!(err.code, -32601);
2348 assert_eq!(err.message, "INVALID_PARAMS");
2349 }
2350
2351 #[tokio::test]
2352 async fn test_rpc_success() {
2353 let ctx = TestCtx::default();
2354 let solana_relayer = ctx.into_relayer().await;
2355
2356 let req = JsonRpcRequest {
2357 jsonrpc: "2.0".to_string(),
2358 params: NetworkRpcRequest::Solana(crate::models::SolanaRpcRequest::GetFeaturesEnabled(
2359 SolanaGetFeaturesEnabledRequestParams {},
2360 )),
2361 id: Some(JsonRpcId::Number(1)),
2362 };
2363 let resp = solana_relayer.rpc(req).await.unwrap();
2364
2365 assert!(resp.error.is_none(), "error should be None");
2366 let data = resp.result.unwrap();
2367 let sol_res = match data {
2368 NetworkRpcResult::Solana(inner) => inner,
2369 other => panic!("expected Solana, got {:?}", other),
2370 };
2371 let features = match sol_res {
2372 SolanaRpcResult::GetFeaturesEnabled(f) => f,
2373 other => panic!("expected GetFeaturesEnabled, got {:?}", other),
2374 };
2375 assert_eq!(features.features, vec!["gasless".to_string()]);
2376 }
2377
2378 #[tokio::test]
2379 async fn test_initialize_relayer_disables_when_validation_fails() {
2380 let mut raw_provider = MockSolanaProviderTrait::new();
2381 let mut mock_repo = MockRelayerRepository::new();
2382 let mut job_producer = MockJobProducerTrait::new();
2383
2384 let mut relayer_model = create_test_relayer();
2385 relayer_model.system_disabled = false; relayer_model.notification_id = Some("test-notification-id".to_string());
2387
2388 raw_provider.expect_get_latest_blockhash().returning(|| {
2390 Box::pin(async { Err(SolanaProviderError::RpcError("RPC error".to_string())) })
2391 });
2392
2393 raw_provider
2394 .expect_get_balance()
2395 .returning(|_| Box::pin(async { Ok(1000000u64) })); let mut disabled_relayer = relayer_model.clone();
2399 disabled_relayer.system_disabled = true;
2400 mock_repo
2401 .expect_disable_relayer()
2402 .with(eq("test-relayer-id".to_string()), always())
2403 .returning(move |_, _| Ok(disabled_relayer.clone()));
2404
2405 job_producer
2407 .expect_produce_send_notification_job()
2408 .returning(|_, _| Box::pin(async { Ok(()) }));
2409
2410 job_producer
2412 .expect_produce_relayer_health_check_job()
2413 .returning(|_, _| Box::pin(async { Ok(()) }));
2414
2415 let ctx = TestCtx {
2416 relayer_model,
2417 mock_repo,
2418 provider: Arc::new(raw_provider),
2419 job_producer: Arc::new(job_producer),
2420 ..Default::default()
2421 };
2422
2423 let solana_relayer = ctx.into_relayer().await;
2424 let result = solana_relayer.initialize_relayer().await;
2425 assert!(result.is_ok());
2426 }
2427
2428 #[tokio::test]
2429 async fn test_initialize_relayer_enables_when_validation_passes_and_was_disabled() {
2430 let mut raw_provider = MockSolanaProviderTrait::new();
2431 let mut mock_repo = MockRelayerRepository::new();
2432
2433 let mut relayer_model = create_test_relayer();
2434 relayer_model.system_disabled = true; raw_provider
2438 .expect_get_latest_blockhash()
2439 .returning(|| Box::pin(async { Ok(Hash::new_unique()) }));
2440
2441 raw_provider
2442 .expect_get_balance()
2443 .returning(|_| Box::pin(async { Ok(1000000u64) })); let mut enabled_relayer = relayer_model.clone();
2447 enabled_relayer.system_disabled = false;
2448 mock_repo
2449 .expect_enable_relayer()
2450 .with(eq("test-relayer-id".to_string()))
2451 .returning(move |_| Ok(enabled_relayer.clone()));
2452
2453 let mut disabled_relayer = relayer_model.clone();
2455 disabled_relayer.system_disabled = true;
2456 mock_repo
2457 .expect_disable_relayer()
2458 .returning(move |_, _| Ok(disabled_relayer.clone()));
2459
2460 let ctx = TestCtx {
2461 relayer_model,
2462 mock_repo,
2463 provider: Arc::new(raw_provider),
2464 ..Default::default()
2465 };
2466
2467 let solana_relayer = ctx.into_relayer().await;
2468 let result = solana_relayer.initialize_relayer().await;
2469 assert!(result.is_ok());
2470 }
2471
2472 #[tokio::test]
2473 async fn test_initialize_relayer_no_action_when_enabled_and_validation_passes() {
2474 let mut raw_provider = MockSolanaProviderTrait::new();
2475 let mock_repo = MockRelayerRepository::new();
2476
2477 let mut relayer_model = create_test_relayer();
2478 relayer_model.system_disabled = false; raw_provider
2482 .expect_get_latest_blockhash()
2483 .returning(|| Box::pin(async { Ok(Hash::new_unique()) }));
2484
2485 raw_provider
2486 .expect_get_balance()
2487 .returning(|_| Box::pin(async { Ok(1000000u64) })); let ctx = TestCtx {
2490 relayer_model,
2491 mock_repo,
2492 provider: Arc::new(raw_provider),
2493 ..Default::default()
2494 };
2495
2496 let solana_relayer = ctx.into_relayer().await;
2497 let result = solana_relayer.initialize_relayer().await;
2498 assert!(result.is_ok());
2499 }
2500
2501 #[tokio::test]
2502 async fn test_initialize_relayer_sends_notification_when_disabled() {
2503 let mut raw_provider = MockSolanaProviderTrait::new();
2504 let mut mock_repo = MockRelayerRepository::new();
2505 let mut job_producer = MockJobProducerTrait::new();
2506
2507 let mut relayer_model = create_test_relayer();
2508 relayer_model.system_disabled = false; relayer_model.notification_id = Some("test-notification-id".to_string());
2510
2511 raw_provider
2513 .expect_get_latest_blockhash()
2514 .returning(|| Box::pin(async { Ok(Hash::new_unique()) }));
2515
2516 raw_provider
2517 .expect_get_balance()
2518 .returning(|_| Box::pin(async { Ok(100u64) })); let mut disabled_relayer = relayer_model.clone();
2522 disabled_relayer.system_disabled = true;
2523 mock_repo
2524 .expect_disable_relayer()
2525 .with(eq("test-relayer-id".to_string()), always())
2526 .returning(move |_, _| Ok(disabled_relayer.clone()));
2527
2528 job_producer
2530 .expect_produce_send_notification_job()
2531 .returning(|_, _| Box::pin(async { Ok(()) }));
2532
2533 job_producer
2535 .expect_produce_relayer_health_check_job()
2536 .returning(|_, _| Box::pin(async { Ok(()) }));
2537
2538 let ctx = TestCtx {
2539 relayer_model,
2540 mock_repo,
2541 provider: Arc::new(raw_provider),
2542 job_producer: Arc::new(job_producer),
2543 ..Default::default()
2544 };
2545
2546 let solana_relayer = ctx.into_relayer().await;
2547 let result = solana_relayer.initialize_relayer().await;
2548 assert!(result.is_ok());
2549 }
2550
2551 #[tokio::test]
2552 async fn test_initialize_relayer_no_notification_when_no_notification_id() {
2553 let mut raw_provider = MockSolanaProviderTrait::new();
2554 let mut mock_repo = MockRelayerRepository::new();
2555
2556 let mut relayer_model = create_test_relayer();
2557 relayer_model.system_disabled = false; relayer_model.notification_id = None; raw_provider.expect_get_latest_blockhash().returning(|| {
2562 Box::pin(async {
2563 Err(SolanaProviderError::RpcError(
2564 "RPC validation failed".to_string(),
2565 ))
2566 })
2567 });
2568
2569 raw_provider
2570 .expect_get_balance()
2571 .returning(|_| Box::pin(async { Ok(1000000u64) })); let mut disabled_relayer = relayer_model.clone();
2575 disabled_relayer.system_disabled = true;
2576 mock_repo
2577 .expect_disable_relayer()
2578 .with(eq("test-relayer-id".to_string()), always())
2579 .returning(move |_, _| Ok(disabled_relayer.clone()));
2580
2581 let mut job_producer = MockJobProducerTrait::new();
2584 job_producer
2585 .expect_produce_relayer_health_check_job()
2586 .returning(|_, _| Box::pin(async { Ok(()) }));
2587
2588 let ctx = TestCtx {
2589 relayer_model,
2590 mock_repo,
2591 provider: Arc::new(raw_provider),
2592 job_producer: Arc::new(job_producer),
2593 ..Default::default()
2594 };
2595
2596 let solana_relayer = ctx.into_relayer().await;
2597 let result = solana_relayer.initialize_relayer().await;
2598 assert!(result.is_ok());
2599 }
2600
2601 #[tokio::test]
2602 async fn test_initialize_relayer_policy_validation_fails() {
2603 let mut raw_provider = MockSolanaProviderTrait::new();
2604
2605 let mut relayer_model = create_test_relayer();
2606 relayer_model.system_disabled = false;
2607
2608 relayer_model.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
2610 allowed_tokens: Some(vec![SolanaAllowedTokensPolicy {
2611 mint: "InvalidMintAddress".to_string(),
2612 decimals: Some(9),
2613 symbol: Some("INVALID".to_string()),
2614 max_allowed_fee: Some(0),
2615 swap_config: None,
2616 }]),
2617 ..Default::default()
2618 });
2619
2620 raw_provider
2622 .expect_get_token_metadata_from_pubkey()
2623 .returning(|_| {
2624 Box::pin(async {
2625 Err(SolanaProviderError::RpcError("Token not found".to_string()))
2626 })
2627 });
2628
2629 let ctx = TestCtx {
2630 relayer_model,
2631 provider: Arc::new(raw_provider),
2632 ..Default::default()
2633 };
2634
2635 let solana_relayer = ctx.into_relayer().await;
2636 let result = solana_relayer.initialize_relayer().await;
2637
2638 assert!(result.is_err());
2640 match result.unwrap_err() {
2641 RelayerError::PolicyConfigurationError(msg) => {
2642 assert!(msg.contains("Error while processing allowed tokens policy"));
2643 }
2644 other => panic!("Expected PolicyConfigurationError, got {:?}", other),
2645 }
2646 }
2647
2648 #[tokio::test]
2649 async fn test_sign_transaction_success() {
2650 let signer = MockSolanaSignTrait::new();
2651
2652 let relayer_model = RelayerRepoModel {
2653 id: "test-relayer-id".to_string(),
2654 address: "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin".to_string(),
2655 network: "devnet".to_string(),
2656 policies: RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
2657 fee_payment_strategy: Some(SolanaFeePaymentStrategy::Relayer),
2658 min_balance: Some(0),
2659 ..Default::default()
2660 }),
2661 ..Default::default()
2662 };
2663
2664 let ctx = TestCtx {
2665 relayer_model,
2666 signer: Arc::new(signer),
2667 ..Default::default()
2668 };
2669
2670 let solana_relayer = ctx.into_relayer().await;
2671
2672 let sign_request = SignTransactionRequest::Solana(SignTransactionRequestSolana {
2673 transaction: EncodedSerializedTransaction::new("raw_transaction_data".to_string()),
2674 });
2675
2676 let result = solana_relayer.sign_transaction(&sign_request).await;
2677 assert!(result.is_ok());
2678 let response = result.unwrap();
2679 match response {
2680 SignTransactionExternalResponse::Solana(solana_resp) => {
2681 assert_eq!(
2682 solana_resp.transaction.into_inner(),
2683 "signed_transaction_data"
2684 );
2685 assert_eq!(solana_resp.signature, "signature_data");
2686 }
2687 _ => panic!("Expected Solana response"),
2688 }
2689 }
2690
2691 #[tokio::test]
2692 async fn test_get_status_success() {
2693 let mut raw_provider = MockSolanaProviderTrait::new();
2694 let mut tx_repo = MockTransactionRepository::new();
2695
2696 raw_provider
2698 .expect_get_balance()
2699 .returning(|_| Box::pin(async { Ok(1000000) }));
2700
2701 tx_repo
2703 .expect_count_by_status()
2704 .with(
2705 eq("test-id"),
2706 eq(vec![
2707 TransactionStatus::Pending,
2708 TransactionStatus::Sent,
2709 TransactionStatus::Submitted,
2710 ]),
2711 )
2712 .returning(|_, _| Ok(2u64));
2713
2714 let recent_tx = TransactionRepoModel {
2716 id: "recent-tx".to_string(),
2717 relayer_id: "test-id".to_string(),
2718 network_data: NetworkTransactionData::Solana(SolanaTransactionData::default()),
2719 network_type: NetworkType::Solana,
2720 status: TransactionStatus::Confirmed,
2721 confirmed_at: Some(Utc::now().to_string()),
2722 ..Default::default()
2723 };
2724 tx_repo
2725 .expect_find_by_status_paginated()
2726 .withf(|relayer_id, statuses, query, oldest_first| {
2727 *relayer_id == *"test-id"
2728 && statuses == [TransactionStatus::Confirmed]
2729 && query.page == 1
2730 && query.per_page == 1
2731 && *oldest_first == false
2732 })
2733 .returning(move |_, _, _, _| {
2734 Ok(crate::repositories::PaginatedResult {
2735 items: vec![recent_tx.clone()],
2736 total: 1,
2737 page: 1,
2738 per_page: 1,
2739 })
2740 });
2741
2742 let ctx = TestCtx {
2743 tx_repo: Arc::new(tx_repo),
2744 provider: Arc::new(raw_provider),
2745 ..Default::default()
2746 };
2747
2748 let solana_relayer = ctx.into_relayer().await;
2749
2750 let result = solana_relayer.get_status(GetStatusOptions::default()).await;
2751 assert!(result.is_ok());
2752 let status = result.unwrap();
2753
2754 match status {
2755 RelayerStatus::Solana {
2756 balance,
2757 pending_transactions_count,
2758 last_confirmed_transaction_timestamp,
2759 ..
2760 } => {
2761 assert_eq!(balance, Some("1000000".to_string()));
2762 assert_eq!(pending_transactions_count, Some(2));
2763 assert!(last_confirmed_transaction_timestamp.is_some());
2764 }
2765 _ => panic!("Expected Solana status"),
2766 }
2767 }
2768
2769 #[tokio::test]
2770 async fn test_get_status_skip_all_optional_fields() {
2771 let ctx = TestCtx::default();
2773 let solana_relayer = ctx.into_relayer().await;
2774
2775 let options = GetStatusOptions {
2776 include_balance: false,
2777 include_pending_count: false,
2778 include_last_confirmed_tx: false,
2779 };
2780 let result = solana_relayer.get_status(options).await;
2781 assert!(result.is_ok());
2782
2783 match result.unwrap() {
2784 RelayerStatus::Solana {
2785 balance,
2786 pending_transactions_count,
2787 last_confirmed_transaction_timestamp,
2788 system_disabled,
2789 paused,
2790 } => {
2791 assert_eq!(balance, None);
2792 assert_eq!(pending_transactions_count, None);
2793 assert_eq!(last_confirmed_transaction_timestamp, None);
2794 assert!(!system_disabled);
2795 assert!(!paused);
2796 }
2797 _ => panic!("Expected Solana status"),
2798 }
2799 }
2800
2801 #[tokio::test]
2802 async fn test_get_status_partial_options() {
2803 let mut raw_provider = MockSolanaProviderTrait::new();
2804 let mut tx_repo = MockTransactionRepository::new();
2805
2806 raw_provider
2808 .expect_get_balance()
2809 .returning(|_| Box::pin(async { Ok(500000) }));
2810
2811 tx_repo
2815 .expect_find_by_status_paginated()
2816 .returning(|_, _, _, _| {
2817 Ok(crate::repositories::PaginatedResult {
2818 items: vec![],
2819 total: 0,
2820 page: 1,
2821 per_page: 1,
2822 })
2823 });
2824
2825 let ctx = TestCtx {
2826 tx_repo: Arc::new(tx_repo),
2827 provider: Arc::new(raw_provider),
2828 ..Default::default()
2829 };
2830 let solana_relayer = ctx.into_relayer().await;
2831
2832 let options = GetStatusOptions {
2833 include_balance: true,
2834 include_pending_count: false,
2835 include_last_confirmed_tx: true,
2836 };
2837 let result = solana_relayer.get_status(options).await;
2838 assert!(result.is_ok());
2839
2840 match result.unwrap() {
2841 RelayerStatus::Solana {
2842 balance,
2843 pending_transactions_count,
2844 last_confirmed_transaction_timestamp,
2845 ..
2846 } => {
2847 assert_eq!(balance, Some("500000".to_string()));
2848 assert_eq!(pending_transactions_count, None);
2849 assert_eq!(last_confirmed_transaction_timestamp, None); }
2851 _ => panic!("Expected Solana status"),
2852 }
2853 }
2854
2855 #[tokio::test]
2856 async fn test_get_status_balance_error() {
2857 let mut raw_provider = MockSolanaProviderTrait::new();
2858 let tx_repo = MockTransactionRepository::new();
2859
2860 raw_provider.expect_get_balance().returning(|_| {
2862 Box::pin(async { Err(SolanaProviderError::RpcError("RPC error".to_string())) })
2863 });
2864
2865 let ctx = TestCtx {
2866 tx_repo: Arc::new(tx_repo),
2867 provider: Arc::new(raw_provider),
2868 ..Default::default()
2869 };
2870
2871 let solana_relayer = ctx.into_relayer().await;
2872
2873 let result = solana_relayer.get_status(GetStatusOptions::default()).await;
2874 assert!(result.is_err());
2875 match result.unwrap_err() {
2876 RelayerError::UnderlyingSolanaProvider(err) => {
2877 assert!(err.to_string().contains("RPC error"));
2878 }
2879 other => panic!("Expected UnderlyingSolanaProvider, got {:?}", other),
2880 }
2881 }
2882
2883 #[tokio::test]
2884 async fn test_get_status_no_recent_transactions() {
2885 let mut raw_provider = MockSolanaProviderTrait::new();
2886 let mut tx_repo = MockTransactionRepository::new();
2887
2888 raw_provider
2890 .expect_get_balance()
2891 .returning(|_| Box::pin(async { Ok(500000) }));
2892
2893 tx_repo
2895 .expect_count_by_status()
2896 .with(
2897 eq("test-id"),
2898 eq(vec![
2899 TransactionStatus::Pending,
2900 TransactionStatus::Sent,
2901 TransactionStatus::Submitted,
2902 ]),
2903 )
2904 .returning(|_, _| Ok(0u64));
2905
2906 tx_repo
2908 .expect_find_by_status_paginated()
2909 .withf(|relayer_id, statuses, query, oldest_first| {
2910 *relayer_id == *"test-id"
2911 && statuses == [TransactionStatus::Confirmed]
2912 && query.page == 1
2913 && query.per_page == 1
2914 && *oldest_first == false
2915 })
2916 .returning(|_, _, _, _| {
2917 Ok(crate::repositories::PaginatedResult {
2918 items: vec![],
2919 total: 0,
2920 page: 1,
2921 per_page: 1,
2922 })
2923 });
2924
2925 let ctx = TestCtx {
2926 tx_repo: Arc::new(tx_repo),
2927 provider: Arc::new(raw_provider),
2928 ..Default::default()
2929 };
2930
2931 let solana_relayer = ctx.into_relayer().await;
2932
2933 let result = solana_relayer.get_status(GetStatusOptions::default()).await;
2934 assert!(result.is_ok());
2935 let status = result.unwrap();
2936
2937 match status {
2938 RelayerStatus::Solana {
2939 balance,
2940 pending_transactions_count,
2941 last_confirmed_transaction_timestamp,
2942 ..
2943 } => {
2944 assert_eq!(balance, Some("500000".to_string()));
2945 assert_eq!(pending_transactions_count, Some(0));
2946 assert!(last_confirmed_transaction_timestamp.is_none());
2947 }
2948 _ => panic!("Expected Solana status"),
2949 }
2950 }
2951
2952 #[tokio::test]
2960 async fn test_quote_sponsored_transaction_wrong_network() {
2961 let ctx = TestCtx::default();
2962 let solana_relayer = ctx.into_relayer().await;
2963
2964 let request = SponsoredTransactionQuoteRequest::Stellar(
2966 crate::models::StellarFeeEstimateRequestParams {
2967 transaction_xdr: Some("test-xdr".to_string()),
2968 operations: None,
2969 source_account: None,
2970 fee_token: "native".to_string(),
2971 },
2972 );
2973
2974 let result = solana_relayer.quote_sponsored_transaction(request).await;
2975 assert!(result.is_err());
2976
2977 if let Err(RelayerError::ValidationError(msg)) = result {
2978 assert!(msg.contains("Expected Solana fee estimate request parameters"));
2979 } else {
2980 panic!("Expected ValidationError for wrong network type");
2981 }
2982 }
2983
2984 #[tokio::test]
2985 async fn test_build_sponsored_transaction_wrong_network() {
2986 let ctx = TestCtx::default();
2987 let solana_relayer = ctx.into_relayer().await;
2988
2989 let request = SponsoredTransactionBuildRequest::Stellar(
2991 crate::models::StellarPrepareTransactionRequestParams {
2992 transaction_xdr: Some("test-xdr".to_string()),
2993 operations: None,
2994 source_account: None,
2995 fee_token: "native".to_string(),
2996 },
2997 );
2998
2999 let result = solana_relayer.build_sponsored_transaction(request).await;
3000 assert!(result.is_err());
3001
3002 if let Err(RelayerError::ValidationError(msg)) = result {
3003 assert!(msg.contains("Expected Solana prepare transaction request parameters"));
3004 } else {
3005 panic!("Expected ValidationError for wrong network type");
3006 }
3007 }
3008}