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