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