1use std::{str::FromStr, sync::Arc};
11
12use crate::utils::calculate_scheduled_timestamp;
13use crate::{
14 constants::{
15 DEFAULT_CONVERSION_SLIPPAGE_PERCENTAGE, DEFAULT_SOLANA_MIN_BALANCE,
16 SOLANA_SMALLEST_UNIT_NAME, WRAPPED_SOL_MINT,
17 },
18 domain::{
19 relayer::RelayerError, BalanceResponse, DexStrategy, SolanaRelayerDexTrait,
20 SolanaRelayerTrait, SolanaRpcHandlerType, SwapParams,
21 },
22 jobs::{JobProducerTrait, RelayerHealthCheck, SolanaTokenSwapRequest},
23 models::{
24 produce_relayer_disabled_payload, produce_solana_dex_webhook_payload, DisabledReason,
25 HealthCheckFailure, JsonRpcRequest, JsonRpcResponse, NetworkRepoModel, NetworkRpcRequest,
26 NetworkRpcResult, NetworkType, RelayerNetworkPolicy, RelayerRepoModel, RelayerSolanaPolicy,
27 SolanaAllowedTokensPolicy, SolanaDexPayload, SolanaNetwork, TransactionRepoModel,
28 },
29 repositories::{NetworkRepository, RelayerRepository, Repository, TransactionRepository},
30 services::{
31 JupiterService, JupiterServiceTrait, SolanaProvider, SolanaProviderTrait, SolanaSignTrait,
32 SolanaSigner,
33 },
34};
35
36use async_trait::async_trait;
37use eyre::Result;
38use futures::future::try_join_all;
39use solana_sdk::{account::Account, pubkey::Pubkey};
40use tracing::{debug, error, info, warn};
41
42use super::{NetworkDex, SolanaRpcError, SolanaTokenProgram, SwapResult, TokenAccount};
43
44#[allow(dead_code)]
45struct TokenSwapCandidate<'a> {
46 policy: &'a SolanaAllowedTokensPolicy,
47 account: TokenAccount,
48 swap_amount: u64,
49}
50
51#[allow(dead_code)]
52pub struct SolanaRelayer<RR, TR, J, S, JS, SP, NR>
53where
54 RR: Repository<RelayerRepoModel, String> + RelayerRepository + Send + Sync + 'static,
55 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
56 J: JobProducerTrait + Send + Sync + 'static,
57 S: SolanaSignTrait + Send + Sync + 'static,
58 JS: JupiterServiceTrait + Send + Sync + 'static,
59 SP: SolanaProviderTrait + Send + Sync + 'static,
60 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
61{
62 relayer: RelayerRepoModel,
63 signer: Arc<S>,
64 network: SolanaNetwork,
65 provider: Arc<SP>,
66 rpc_handler: SolanaRpcHandlerType<SP, S, JS, J, TR>,
67 relayer_repository: Arc<RR>,
68 transaction_repository: Arc<TR>,
69 job_producer: Arc<J>,
70 dex_service: Arc<NetworkDex<SP, S, JS>>,
71 network_repository: Arc<NR>,
72}
73
74pub type DefaultSolanaRelayer<J, TR, RR, NR> =
75 SolanaRelayer<RR, TR, J, SolanaSigner, JupiterService, SolanaProvider, NR>;
76
77impl<RR, TR, J, S, JS, SP, NR> SolanaRelayer<RR, TR, J, S, JS, SP, NR>
78where
79 RR: Repository<RelayerRepoModel, String> + RelayerRepository + Send + Sync + 'static,
80 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
81 J: JobProducerTrait + Send + Sync + 'static,
82 S: SolanaSignTrait + Send + Sync + 'static,
83 JS: JupiterServiceTrait + Send + Sync + 'static,
84 SP: SolanaProviderTrait + Send + Sync + 'static,
85 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
86{
87 #[allow(clippy::too_many_arguments)]
88 pub async fn new(
89 relayer: RelayerRepoModel,
90 signer: Arc<S>,
91 relayer_repository: Arc<RR>,
92 network_repository: Arc<NR>,
93 provider: Arc<SP>,
94 rpc_handler: SolanaRpcHandlerType<SP, S, JS, J, TR>,
95 transaction_repository: Arc<TR>,
96 job_producer: Arc<J>,
97 dex_service: Arc<NetworkDex<SP, S, JS>>,
98 ) -> Result<Self, RelayerError> {
99 let network_repo = network_repository
100 .get_by_name(NetworkType::Solana, &relayer.network)
101 .await
102 .ok()
103 .flatten()
104 .ok_or_else(|| {
105 RelayerError::NetworkConfiguration(format!("Network {} not found", relayer.network))
106 })?;
107
108 let network = SolanaNetwork::try_from(network_repo)?;
109
110 Ok(Self {
111 relayer,
112 signer,
113 network,
114 provider,
115 rpc_handler,
116 relayer_repository,
117 transaction_repository,
118 job_producer,
119 dex_service,
120 network_repository,
121 })
122 }
123
124 async fn validate_rpc(&self) -> Result<(), RelayerError> {
129 self.provider
130 .get_latest_blockhash()
131 .await
132 .map_err(|e| RelayerError::ProviderError(e.to_string()))?;
133
134 Ok(())
135 }
136
137 async fn populate_allowed_tokens_metadata(&self) -> Result<RelayerSolanaPolicy, RelayerError> {
149 let mut policy = self.relayer.policies.get_solana_policy();
150 let allowed_tokens = match policy.allowed_tokens.as_ref() {
152 Some(tokens) if !tokens.is_empty() => tokens,
153 _ => {
154 info!("No allowed tokens specified; skipping token metadata population.");
155 return Ok(policy);
156 }
157 };
158
159 let token_metadata_futures = allowed_tokens.iter().map(|token| async {
160 let token_metadata = self
162 .provider
163 .get_token_metadata_from_pubkey(&token.mint)
164 .await
165 .map_err(|e| RelayerError::ProviderError(e.to_string()))?;
166 Ok::<SolanaAllowedTokensPolicy, RelayerError>(SolanaAllowedTokensPolicy {
167 mint: token_metadata.mint,
168 decimals: Some(token_metadata.decimals as u8),
169 symbol: Some(token_metadata.symbol.to_string()),
170 max_allowed_fee: token.max_allowed_fee,
171 swap_config: token.swap_config.clone(),
172 })
173 });
174
175 let updated_allowed_tokens = try_join_all(token_metadata_futures).await?;
176
177 policy.allowed_tokens = Some(updated_allowed_tokens);
178
179 self.relayer_repository
180 .update_policy(
181 self.relayer.id.clone(),
182 RelayerNetworkPolicy::Solana(policy.clone()),
183 )
184 .await?;
185
186 Ok(policy)
187 }
188
189 async fn validate_program_policy(&self) -> Result<(), RelayerError> {
197 let policy = self.relayer.policies.get_solana_policy();
198 let allowed_programs = match policy.allowed_programs.as_ref() {
199 Some(programs) if !programs.is_empty() => programs,
200 _ => {
201 info!("No allowed programs specified; skipping program validation.");
202 return Ok(());
203 }
204 };
205 let account_info_futures = allowed_programs.iter().map(|program| {
206 let program = program.clone();
207 async move {
208 let account = self
209 .provider
210 .get_account_from_str(&program)
211 .await
212 .map_err(|e| RelayerError::ProviderError(e.to_string()))?;
213 Ok::<Account, RelayerError>(account)
214 }
215 });
216
217 let accounts = try_join_all(account_info_futures).await?;
218
219 for account in accounts {
220 if !account.executable {
221 return Err(RelayerError::PolicyConfigurationError(
222 "Policy Program is not executable".to_string(),
223 ));
224 }
225 }
226
227 Ok(())
228 }
229
230 async fn check_balance_and_trigger_token_swap_if_needed(&self) -> Result<(), RelayerError> {
233 let policy = self.relayer.policies.get_solana_policy();
234 let swap_config = match policy.get_swap_config() {
235 Some(config) => config,
236 None => {
237 info!("No swap configuration specified; skipping validation.");
238 return Ok(());
239 }
240 };
241 let swap_min_balance_threshold = match swap_config.min_balance_threshold {
242 Some(threshold) => threshold,
243 None => {
244 info!("No swap min balance threshold specified; skipping validation.");
245 return Ok(());
246 }
247 };
248
249 let balance = self
250 .provider
251 .get_balance(&self.relayer.address)
252 .await
253 .map_err(|e| RelayerError::ProviderError(e.to_string()))?;
254
255 if balance < swap_min_balance_threshold {
256 info!(
257 "Sending job request for for relayer {} swapping tokens due to relayer swap_min_balance_threshold: Balance: {}, swap_min_balance_threshold: {}",
258 self.relayer.id, balance, swap_min_balance_threshold
259 );
260
261 self.job_producer
262 .produce_solana_token_swap_request_job(
263 SolanaTokenSwapRequest {
264 relayer_id: self.relayer.id.clone(),
265 },
266 None,
267 )
268 .await?;
269 }
270
271 Ok(())
272 }
273
274 fn calculate_swap_amount(
276 &self,
277 current_balance: u64,
278 min_amount: Option<u64>,
279 max_amount: Option<u64>,
280 retain_min: Option<u64>,
281 ) -> Result<u64, RelayerError> {
282 let mut amount = max_amount
284 .map(|max| std::cmp::min(current_balance, max))
285 .unwrap_or(current_balance);
286
287 if let Some(retain) = retain_min {
289 if current_balance > retain {
290 amount = std::cmp::min(amount, current_balance - retain);
291 } else {
292 return Ok(0);
294 }
295 }
296
297 if let Some(min) = min_amount {
299 if amount < min {
300 return Ok(0); }
302 }
303
304 Ok(amount)
305 }
306}
307
308#[async_trait]
309impl<RR, TR, J, S, JS, SP, NR> SolanaRelayerDexTrait for SolanaRelayer<RR, TR, J, S, JS, SP, NR>
310where
311 RR: Repository<RelayerRepoModel, String> + RelayerRepository + Send + Sync + 'static,
312 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
313 J: JobProducerTrait + Send + Sync + 'static,
314 S: SolanaSignTrait + Send + Sync + 'static,
315 JS: JupiterServiceTrait + Send + Sync + 'static,
316 SP: SolanaProviderTrait + Send + Sync + 'static,
317 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
318{
319 async fn handle_token_swap_request(
329 &self,
330 relayer_id: String,
331 ) -> Result<Vec<SwapResult>, RelayerError> {
332 debug!("handling token swap request for relayer");
333 let relayer = self
334 .relayer_repository
335 .get_by_id(relayer_id.clone())
336 .await?;
337
338 let policy = relayer.policies.get_solana_policy();
339
340 let swap_config = match policy.get_swap_config() {
341 Some(config) => config,
342 None => {
343 info!("No swap configuration specified; Exiting.");
344 return Ok(vec![]);
345 }
346 };
347
348 match swap_config.strategy {
349 Some(strategy) => strategy,
350 None => {
351 info!("No swap strategy specified; Exiting.");
352 return Ok(vec![]);
353 }
354 };
355
356 let relayer_pubkey = Pubkey::from_str(&relayer.address)
357 .map_err(|e| RelayerError::ProviderError(format!("Invalid relayer address: {}", e)))?;
358
359 let tokens_to_swap = {
360 let mut eligible_tokens = Vec::<TokenSwapCandidate>::new();
361
362 if let Some(allowed_tokens) = policy.allowed_tokens.as_ref() {
363 for token in allowed_tokens {
364 let token_mint = Pubkey::from_str(&token.mint).map_err(|e| {
365 RelayerError::ProviderError(format!("Invalid token mint: {}", e))
366 })?;
367 let token_account = SolanaTokenProgram::get_and_unpack_token_account(
368 &*self.provider,
369 &relayer_pubkey,
370 &token_mint,
371 )
372 .await
373 .map_err(|e| {
374 RelayerError::ProviderError(format!("Failed to get token account: {}", e))
375 })?;
376
377 let swap_amount = self
378 .calculate_swap_amount(
379 token_account.amount,
380 token
381 .swap_config
382 .as_ref()
383 .and_then(|config| config.min_amount),
384 token
385 .swap_config
386 .as_ref()
387 .and_then(|config| config.max_amount),
388 token
389 .swap_config
390 .as_ref()
391 .and_then(|config| config.retain_min_amount),
392 )
393 .unwrap_or(0);
394
395 if swap_amount > 0 {
396 debug!(token = ?token, "token swap eligible for token");
397
398 eligible_tokens.push(TokenSwapCandidate {
400 policy: token,
401 account: token_account,
402 swap_amount,
403 });
404 }
405 }
406 }
407
408 eligible_tokens
409 };
410
411 let swap_futures = tokens_to_swap.iter().map(|candidate| {
413 let token = candidate.policy;
414 let swap_amount = candidate.swap_amount;
415 let dex = &self.dex_service;
416 let relayer_address = self.relayer.address.clone();
417 let token_mint = token.mint.clone();
418 let relayer_id_clone = relayer_id.clone();
419 let slippage_percent = token
420 .swap_config
421 .as_ref()
422 .and_then(|config| config.slippage_percentage)
423 .unwrap_or(DEFAULT_CONVERSION_SLIPPAGE_PERCENTAGE)
424 as f64;
425
426 async move {
427 info!(
428 "Swapping {} tokens of type {} for relayer: {}",
429 swap_amount, token_mint, relayer_id_clone
430 );
431
432 let swap_result = dex
433 .execute_swap(SwapParams {
434 owner_address: relayer_address,
435 source_mint: token_mint.clone(),
436 destination_mint: WRAPPED_SOL_MINT.to_string(), amount: swap_amount,
438 slippage_percent,
439 })
440 .await;
441
442 match swap_result {
443 Ok(swap_result) => {
444 info!(
445 "Swap successful for relayer: {}. Amount: {}, Destination amount: {}",
446 relayer_id_clone, swap_amount, swap_result.destination_amount
447 );
448 Ok::<SwapResult, RelayerError>(swap_result)
449 }
450 Err(e) => {
451 error!(
452 "Error during token swap for relayer: {}. Error: {}",
453 relayer_id_clone, e
454 );
455 Ok::<SwapResult, RelayerError>(SwapResult {
456 mint: token_mint.clone(),
457 source_amount: swap_amount,
458 destination_amount: 0,
459 transaction_signature: "".to_string(),
460 error: Some(e.to_string()),
461 })
462 }
463 }
464 }
465 });
466
467 let swap_results = try_join_all(swap_futures).await?;
468
469 if !swap_results.is_empty() {
470 let total_sol_received: u64 = swap_results
471 .iter()
472 .map(|result| result.destination_amount)
473 .sum();
474
475 info!(
476 "Completed {} token swaps for relayer {}, total SOL received: {}",
477 swap_results.len(),
478 relayer_id,
479 total_sol_received
480 );
481
482 if let Some(notification_id) = &self.relayer.notification_id {
483 let webhook_result = self
484 .job_producer
485 .produce_send_notification_job(
486 produce_solana_dex_webhook_payload(
487 notification_id,
488 "solana_dex".to_string(),
489 SolanaDexPayload {
490 swap_results: swap_results.clone(),
491 },
492 ),
493 None,
494 )
495 .await;
496
497 if let Err(e) = webhook_result {
498 error!(error = %e, "failed to produce notification job");
499 }
500 }
501 }
502
503 Ok(swap_results)
504 }
505}
506
507#[async_trait]
508impl<RR, TR, J, S, JS, SP, NR> SolanaRelayerTrait for SolanaRelayer<RR, TR, J, S, JS, SP, NR>
509where
510 RR: Repository<RelayerRepoModel, String> + RelayerRepository + Send + Sync + 'static,
511 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
512 J: JobProducerTrait + Send + Sync + 'static,
513 S: SolanaSignTrait + Send + Sync + 'static,
514 JS: JupiterServiceTrait + Send + Sync + 'static,
515 SP: SolanaProviderTrait + Send + Sync + 'static,
516 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
517{
518 async fn get_balance(&self) -> Result<BalanceResponse, RelayerError> {
519 let address = &self.relayer.address;
520 let balance = self.provider.get_balance(address).await?;
521
522 Ok(BalanceResponse {
523 balance: balance as u128,
524 unit: SOLANA_SMALLEST_UNIT_NAME.to_string(),
525 })
526 }
527
528 async fn rpc(
529 &self,
530 request: JsonRpcRequest<NetworkRpcRequest>,
531 ) -> Result<JsonRpcResponse<NetworkRpcResult>, RelayerError> {
532 let response = self.rpc_handler.handle_request(request).await;
533
534 match response {
535 Ok(response) => Ok(response),
536 Err(e) => {
537 error!(error = %e, "error while processing RPC request");
538 let error_response = match e {
539 SolanaRpcError::UnsupportedMethod(msg) => {
540 JsonRpcResponse::error(32000, "UNSUPPORTED_METHOD", &msg)
541 }
542 SolanaRpcError::FeatureFetch(msg) => JsonRpcResponse::error(
543 -32008,
544 "FEATURE_FETCH_ERROR",
545 &format!("Failed to retrieve the list of enabled features: {}", msg),
546 ),
547 SolanaRpcError::InvalidParams(msg) => {
548 JsonRpcResponse::error(-32602, "INVALID_PARAMS", &msg)
549 }
550 SolanaRpcError::UnsupportedFeeToken(msg) => JsonRpcResponse::error(
551 -32000,
552 "UNSUPPORTED
553 FEE_TOKEN",
554 &format!(
555 "The provided fee_token is not supported by the relayer: {}",
556 msg
557 ),
558 ),
559 SolanaRpcError::Estimation(msg) => JsonRpcResponse::error(
560 -32001,
561 "ESTIMATION_ERROR",
562 &format!(
563 "Failed to estimate the fee due to internal or network issues: {}",
564 msg
565 ),
566 ),
567 SolanaRpcError::InsufficientFunds(msg) => {
568 self.check_balance_and_trigger_token_swap_if_needed()
570 .await?;
571
572 JsonRpcResponse::error(
573 -32002,
574 "INSUFFICIENT_FUNDS",
575 &format!(
576 "The sender does not have enough funds for the transfer: {}",
577 msg
578 ),
579 )
580 }
581 SolanaRpcError::TransactionPreparation(msg) => JsonRpcResponse::error(
582 -32003,
583 "TRANSACTION_PREPARATION_ERROR",
584 &format!("Failed to prepare the transfer transaction: {}", msg),
585 ),
586 SolanaRpcError::Preparation(msg) => JsonRpcResponse::error(
587 -32013,
588 "PREPARATION_ERROR",
589 &format!("Failed to prepare the transfer transaction: {}", msg),
590 ),
591 SolanaRpcError::Signature(msg) => JsonRpcResponse::error(
592 -32005,
593 "SIGNATURE_ERROR",
594 &format!("Failed to sign the transaction: {}", msg),
595 ),
596 SolanaRpcError::Signing(msg) => JsonRpcResponse::error(
597 -32005,
598 "SIGNATURE_ERROR",
599 &format!("Failed to sign the transaction: {}", msg),
600 ),
601 SolanaRpcError::TokenFetch(msg) => JsonRpcResponse::error(
602 -32007,
603 "TOKEN_FETCH_ERROR",
604 &format!("Failed to retrieve the list of supported tokens: {}", msg),
605 ),
606 SolanaRpcError::BadRequest(msg) => JsonRpcResponse::error(
607 -32007,
608 "BAD_REQUEST",
609 &format!("Bad request: {}", msg),
610 ),
611 SolanaRpcError::Send(msg) => JsonRpcResponse::error(
612 -32006,
613 "SEND_ERROR",
614 &format!(
615 "Failed to submit the transaction to the blockchain: {}",
616 msg
617 ),
618 ),
619 SolanaRpcError::SolanaTransactionValidation(msg) => JsonRpcResponse::error(
620 -32013,
621 "PREPARATION_ERROR",
622 &format!("Failed to prepare the transfer transaction: {}", msg),
623 ),
624 SolanaRpcError::Encoding(msg) => JsonRpcResponse::error(
625 -32601,
626 "INVALID_PARAMS",
627 &format!("The transaction parameter is invalid or missing: {}", msg),
628 ),
629 SolanaRpcError::TokenAccount(msg) => JsonRpcResponse::error(
630 -32601,
631 "PREPARATION_ERROR",
632 &format!("Invalid Token Account: {}", msg),
633 ),
634 SolanaRpcError::Token(msg) => JsonRpcResponse::error(
635 -32601,
636 "PREPARATION_ERROR",
637 &format!("Invalid Token Account: {}", msg),
638 ),
639 SolanaRpcError::Provider(msg) => JsonRpcResponse::error(
640 -32006,
641 "PREPARATION_ERROR",
642 &format!("Failed to prepare the transfer transaction: {}", msg),
643 ),
644 SolanaRpcError::Internal(_) => {
645 JsonRpcResponse::error(-32000, "INTERNAL_ERROR", "Internal error")
646 }
647 };
648 Ok(error_response)
649 }
650 }
651 }
652
653 async fn validate_min_balance(&self) -> Result<(), RelayerError> {
654 let balance = self
655 .provider
656 .get_balance(&self.relayer.address)
657 .await
658 .map_err(|e| RelayerError::ProviderError(e.to_string()))?;
659
660 debug!(balance = %balance, "balance for relayer");
661
662 let policy = self.relayer.policies.get_solana_policy();
663
664 if balance < policy.min_balance.unwrap_or(DEFAULT_SOLANA_MIN_BALANCE) {
665 return Err(RelayerError::InsufficientBalanceError(
666 "Insufficient balance".to_string(),
667 ));
668 }
669
670 Ok(())
671 }
672
673 async fn check_health(&self) -> Result<(), Vec<HealthCheckFailure>> {
674 debug!(
675 "running health checks for Solana relayer {}",
676 self.relayer.id
677 );
678
679 let validate_rpc_result = self.validate_rpc().await;
680 let validate_min_balance_result = self.validate_min_balance().await;
681
682 let failures: Vec<HealthCheckFailure> = vec![
684 validate_rpc_result
685 .err()
686 .map(|e| HealthCheckFailure::RpcValidationFailed(e.to_string())),
687 validate_min_balance_result
688 .err()
689 .map(|e| HealthCheckFailure::BalanceCheckFailed(e.to_string())),
690 ]
691 .into_iter()
692 .flatten()
693 .collect();
694
695 if failures.is_empty() {
696 info!("all health checks passed");
697 Ok(())
698 } else {
699 warn!("health checks failed: {:?}", failures);
700 Err(failures)
701 }
702 }
703
704 async fn initialize_relayer(&self) -> Result<(), RelayerError> {
705 debug!("initializing Solana relayer {}", self.relayer.id);
706
707 self.populate_allowed_tokens_metadata().await.map_err(|_| {
710 RelayerError::PolicyConfigurationError(
711 "Error while processing allowed tokens policy".into(),
712 )
713 })?;
714
715 self.validate_program_policy().await.map_err(|_| {
718 RelayerError::PolicyConfigurationError(
719 "Error while validating allowed programs policy".into(),
720 )
721 })?;
722
723 match self.check_health().await {
724 Ok(_) => {
725 if self.relayer.system_disabled {
727 self.relayer_repository
729 .enable_relayer(self.relayer.id.clone())
730 .await?;
731 }
732 }
733 Err(failures) => {
734 let reason = DisabledReason::from_health_failures(failures).unwrap_or_else(|| {
736 DisabledReason::RpcValidationFailed("Unknown error".to_string())
737 });
738
739 warn!(reason = %reason, "disabling relayer");
740 let updated_relayer = self
741 .relayer_repository
742 .disable_relayer(self.relayer.id.clone(), reason.clone())
743 .await?;
744
745 if let Some(notification_id) = &self.relayer.notification_id {
747 self.job_producer
748 .produce_send_notification_job(
749 produce_relayer_disabled_payload(
750 notification_id,
751 &updated_relayer,
752 &reason.safe_description(),
753 ),
754 None,
755 )
756 .await?;
757 }
758
759 self.job_producer
761 .produce_relayer_health_check_job(
762 RelayerHealthCheck::new(self.relayer.id.clone()),
763 Some(calculate_scheduled_timestamp(10)),
764 )
765 .await?;
766 }
767 }
768
769 self.check_balance_and_trigger_token_swap_if_needed()
770 .await?;
771
772 Ok(())
773 }
774}
775
776#[cfg(test)]
777mod tests {
778 use super::*;
779 use crate::{
780 config::{NetworkConfigCommon, SolanaNetworkConfig},
781 domain::{create_network_dex_generic, SolanaRpcHandler, SolanaRpcMethodsImpl},
782 jobs::MockJobProducerTrait,
783 models::{
784 EncodedSerializedTransaction, FeeEstimateRequestParams,
785 GetFeaturesEnabledRequestParams, JsonRpcId, NetworkConfigData, NetworkRepoModel,
786 RelayerSolanaSwapConfig, SolanaAllowedTokensSwapConfig, SolanaRpcResult,
787 SolanaSwapStrategy,
788 },
789 repositories::{MockNetworkRepository, MockRelayerRepository, MockTransactionRepository},
790 services::{
791 MockJupiterServiceTrait, MockSolanaProviderTrait, MockSolanaSignTrait, QuoteResponse,
792 RoutePlan, SolanaProviderError, SwapEvents, SwapInfo, SwapResponse,
793 UltraExecuteResponse, UltraOrderResponse,
794 },
795 utils::mocks::mockutils::create_mock_solana_network,
796 };
797 use mockall::predicate::*;
798 use solana_sdk::{hash::Hash, program_pack::Pack, signature::Signature};
799 use spl_token::state::Account as SplAccount;
800
801 #[allow(dead_code)]
804 struct TestCtx {
805 relayer_model: RelayerRepoModel,
806 mock_repo: MockRelayerRepository,
807 network_repository: Arc<MockNetworkRepository>,
808 provider: Arc<MockSolanaProviderTrait>,
809 signer: Arc<MockSolanaSignTrait>,
810 jupiter: Arc<MockJupiterServiceTrait>,
811 job_producer: Arc<MockJobProducerTrait>,
812 tx_repo: Arc<MockTransactionRepository>,
813 dex: Arc<NetworkDex<MockSolanaProviderTrait, MockSolanaSignTrait, MockJupiterServiceTrait>>,
814 rpc_handler: SolanaRpcHandlerType<
815 MockSolanaProviderTrait,
816 MockSolanaSignTrait,
817 MockJupiterServiceTrait,
818 MockJobProducerTrait,
819 MockTransactionRepository,
820 >,
821 }
822
823 impl Default for TestCtx {
824 fn default() -> Self {
825 let mock_repo = MockRelayerRepository::new();
826 let provider = Arc::new(MockSolanaProviderTrait::new());
827 let signer = Arc::new(MockSolanaSignTrait::new());
828 let jupiter = Arc::new(MockJupiterServiceTrait::new());
829 let job = Arc::new(MockJobProducerTrait::new());
830 let tx_repo = Arc::new(MockTransactionRepository::new());
831 let mut network_repository = MockNetworkRepository::new();
832 let transaction_repository = Arc::new(MockTransactionRepository::new());
833
834 let relayer_model = RelayerRepoModel {
835 id: "test-id".to_string(),
836 address: "...".to_string(),
837 network: "devnet".to_string(),
838 ..Default::default()
839 };
840
841 let dex = Arc::new(
842 create_network_dex_generic(
843 &relayer_model,
844 provider.clone(),
845 signer.clone(),
846 jupiter.clone(),
847 )
848 .unwrap(),
849 );
850
851 let test_network = create_mock_solana_network();
852
853 let rpc_handler = Arc::new(SolanaRpcHandler::new(SolanaRpcMethodsImpl::new_mock(
854 relayer_model.clone(),
855 test_network.clone(),
856 provider.clone(),
857 signer.clone(),
858 jupiter.clone(),
859 job.clone(),
860 transaction_repository.clone(),
861 )));
862
863 let test_network = NetworkRepoModel {
864 id: "solana:devnet".to_string(),
865 name: "devnet".to_string(),
866 network_type: NetworkType::Solana,
867 config: NetworkConfigData::Solana(SolanaNetworkConfig {
868 common: NetworkConfigCommon {
869 network: "devnet".to_string(),
870 from: None,
871 rpc_urls: Some(vec!["https://api.devnet.solana.com".to_string()]),
872 explorer_urls: None,
873 average_blocktime_ms: Some(400),
874 is_testnet: Some(true),
875 tags: None,
876 },
877 }),
878 };
879
880 network_repository
881 .expect_get_by_name()
882 .returning(move |_, _| Ok(Some(test_network.clone())));
883
884 TestCtx {
885 relayer_model,
886 mock_repo,
887 network_repository: Arc::new(network_repository),
888 provider,
889 signer,
890 jupiter,
891 job_producer: job,
892 tx_repo,
893 dex,
894 rpc_handler,
895 }
896 }
897 }
898
899 impl TestCtx {
900 async fn into_relayer(
901 self,
902 ) -> SolanaRelayer<
903 MockRelayerRepository,
904 MockTransactionRepository,
905 MockJobProducerTrait,
906 MockSolanaSignTrait,
907 MockJupiterServiceTrait,
908 MockSolanaProviderTrait,
909 MockNetworkRepository,
910 > {
911 let network_repo = self
913 .network_repository
914 .get_by_name(NetworkType::Solana, "devnet")
915 .await
916 .unwrap()
917 .unwrap();
918 let network = SolanaNetwork::try_from(network_repo).unwrap();
919
920 SolanaRelayer {
921 relayer: self.relayer_model.clone(),
922 signer: self.signer,
923 network,
924 provider: self.provider,
925 rpc_handler: self.rpc_handler,
926 relayer_repository: Arc::new(self.mock_repo),
927 transaction_repository: self.tx_repo,
928 job_producer: self.job_producer,
929 dex_service: self.dex,
930 network_repository: self.network_repository,
931 }
932 }
933 }
934
935 fn create_test_relayer() -> RelayerRepoModel {
936 RelayerRepoModel {
937 id: "test-relayer-id".to_string(),
938 address: "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin".to_string(),
939 notification_id: Some("test-notification-id".to_string()),
940 network_type: NetworkType::Solana,
941 policies: RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
942 min_balance: Some(0), swap_config: None,
944 ..Default::default()
945 }),
946 ..Default::default()
947 }
948 }
949
950 fn create_token_policy(
951 mint: &str,
952 min_amount: Option<u64>,
953 max_amount: Option<u64>,
954 retain_min: Option<u64>,
955 slippage: Option<u64>,
956 ) -> SolanaAllowedTokensPolicy {
957 let mut token = SolanaAllowedTokensPolicy {
958 mint: mint.to_string(),
959 max_allowed_fee: Some(0),
960 swap_config: None,
961 decimals: Some(9),
962 symbol: Some("SOL".to_string()),
963 };
964
965 let swap_config = SolanaAllowedTokensSwapConfig {
966 min_amount,
967 max_amount,
968 retain_min_amount: retain_min,
969 slippage_percentage: slippage.map(|s| s as f32),
970 };
971
972 token.swap_config = Some(swap_config);
973 token
974 }
975
976 #[tokio::test]
977 async fn test_calculate_swap_amount_no_limits() {
978 let ctx = TestCtx::default();
979 let solana_relayer = ctx.into_relayer().await;
980
981 assert_eq!(
982 solana_relayer
983 .calculate_swap_amount(100, None, None, None)
984 .unwrap(),
985 100
986 );
987 }
988
989 #[tokio::test]
990 async fn test_calculate_swap_amount_with_max() {
991 let ctx = TestCtx::default();
992 let solana_relayer = ctx.into_relayer().await;
993
994 assert_eq!(
995 solana_relayer
996 .calculate_swap_amount(100, None, Some(60), None)
997 .unwrap(),
998 60
999 );
1000 }
1001
1002 #[tokio::test]
1003 async fn test_calculate_swap_amount_with_retain() {
1004 let ctx = TestCtx::default();
1005 let solana_relayer = ctx.into_relayer().await;
1006
1007 assert_eq!(
1008 solana_relayer
1009 .calculate_swap_amount(100, None, None, Some(30))
1010 .unwrap(),
1011 70
1012 );
1013
1014 assert_eq!(
1015 solana_relayer
1016 .calculate_swap_amount(20, None, None, Some(30))
1017 .unwrap(),
1018 0
1019 );
1020 }
1021
1022 #[tokio::test]
1023 async fn test_calculate_swap_amount_with_min() {
1024 let ctx = TestCtx::default();
1025 let solana_relayer = ctx.into_relayer().await;
1026
1027 assert_eq!(
1028 solana_relayer
1029 .calculate_swap_amount(40, Some(50), None, None)
1030 .unwrap(),
1031 0
1032 );
1033
1034 assert_eq!(
1035 solana_relayer
1036 .calculate_swap_amount(100, Some(50), None, None)
1037 .unwrap(),
1038 100
1039 );
1040 }
1041
1042 #[tokio::test]
1043 async fn test_calculate_swap_amount_combined() {
1044 let ctx = TestCtx::default();
1045 let solana_relayer = ctx.into_relayer().await;
1046
1047 assert_eq!(
1048 solana_relayer
1049 .calculate_swap_amount(100, None, Some(50), Some(30))
1050 .unwrap(),
1051 50
1052 );
1053
1054 assert_eq!(
1055 solana_relayer
1056 .calculate_swap_amount(100, Some(20), Some(50), Some(30))
1057 .unwrap(),
1058 50
1059 );
1060
1061 assert_eq!(
1062 solana_relayer
1063 .calculate_swap_amount(100, Some(60), Some(50), Some(30))
1064 .unwrap(),
1065 0
1066 );
1067 }
1068
1069 #[tokio::test]
1070 async fn test_handle_token_swap_request_successful_swap_jupiter_swap_strategy() {
1071 let mut relayer_model = create_test_relayer();
1072
1073 let mut mock_relayer_repo = MockRelayerRepository::new();
1074 let id = relayer_model.id.clone();
1075
1076 relayer_model.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
1077 swap_config: Some(RelayerSolanaSwapConfig {
1078 strategy: Some(SolanaSwapStrategy::JupiterSwap),
1079 cron_schedule: None,
1080 min_balance_threshold: None,
1081 jupiter_swap_options: None,
1082 }),
1083 allowed_tokens: Some(vec![create_token_policy(
1084 "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
1085 Some(1),
1086 None,
1087 None,
1088 Some(50),
1089 )]),
1090 ..Default::default()
1091 });
1092 let cloned = relayer_model.clone();
1093
1094 mock_relayer_repo
1095 .expect_get_by_id()
1096 .with(eq(id.clone()))
1097 .times(1)
1098 .returning(move |_| Ok(cloned.clone()));
1099
1100 let mut raw_provider = MockSolanaProviderTrait::new();
1101
1102 raw_provider
1103 .expect_get_account_from_pubkey()
1104 .returning(|_| {
1105 Box::pin(async {
1106 let mut account_data = vec![0; SplAccount::LEN];
1107
1108 let token_account = spl_token::state::Account {
1109 mint: Pubkey::new_unique(),
1110 owner: Pubkey::new_unique(),
1111 amount: 10000000,
1112 state: spl_token::state::AccountState::Initialized,
1113 ..Default::default()
1114 };
1115 spl_token::state::Account::pack(token_account, &mut account_data).unwrap();
1116
1117 Ok(solana_sdk::account::Account {
1118 lamports: 1_000_000,
1119 data: account_data,
1120 owner: spl_token::id(),
1121 executable: false,
1122 rent_epoch: 0,
1123 })
1124 })
1125 });
1126
1127 let mut jupiter_mock = MockJupiterServiceTrait::new();
1128
1129 jupiter_mock.expect_get_quote().returning(|_| {
1130 Box::pin(async {
1131 Ok(QuoteResponse {
1132 input_mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
1133 output_mint: WRAPPED_SOL_MINT.to_string(),
1134 in_amount: 10,
1135 out_amount: 10,
1136 other_amount_threshold: 1,
1137 swap_mode: "ExactIn".to_string(),
1138 price_impact_pct: 0.0,
1139 route_plan: vec![RoutePlan {
1140 percent: 100,
1141 swap_info: SwapInfo {
1142 amm_key: "mock_amm_key".to_string(),
1143 label: "mock_label".to_string(),
1144 input_mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
1145 output_mint: WRAPPED_SOL_MINT.to_string(),
1146 in_amount: "1000".to_string(),
1147 out_amount: "1000".to_string(),
1148 fee_amount: "0".to_string(),
1149 fee_mint: "mock_fee_mint".to_string(),
1150 },
1151 }],
1152 slippage_bps: 0,
1153 })
1154 })
1155 });
1156
1157 jupiter_mock.expect_get_swap_transaction().returning(|_| {
1158 Box::pin(async {
1159 Ok(SwapResponse {
1160 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(),
1161 last_valid_block_height: 100,
1162 prioritization_fee_lamports: None,
1163 compute_unit_limit: None,
1164 simulation_error: None,
1165 })
1166 })
1167 });
1168
1169 let mut signer = MockSolanaSignTrait::new();
1170 let test_signature = Signature::from_str("2jg9xbGLtZRsiJBrDWQnz33JuLjDkiKSZuxZPdjJ3qrJbMeTEerXFAKynkPW63J88nq63cvosDNRsg9VqHtGixvP").unwrap();
1171
1172 signer
1173 .expect_sign()
1174 .times(1)
1175 .returning(move |_| Box::pin(async move { Ok(test_signature) }));
1176
1177 raw_provider
1178 .expect_send_versioned_transaction()
1179 .times(1)
1180 .returning(move |_| Box::pin(async move { Ok(test_signature) }));
1181
1182 raw_provider
1183 .expect_confirm_transaction()
1184 .times(1)
1185 .returning(move |_| Box::pin(async move { Ok(true) }));
1186
1187 let provider_arc = Arc::new(raw_provider);
1188 let jupiter_arc = Arc::new(jupiter_mock);
1189 let signer_arc = Arc::new(signer);
1190
1191 let dex = Arc::new(
1192 create_network_dex_generic(
1193 &relayer_model,
1194 provider_arc.clone(),
1195 signer_arc.clone(),
1196 jupiter_arc.clone(),
1197 )
1198 .unwrap(),
1199 );
1200
1201 let mut job_producer = MockJobProducerTrait::new();
1202 job_producer
1203 .expect_produce_send_notification_job()
1204 .times(1)
1205 .returning(|_, _| Box::pin(async { Ok(()) }));
1206
1207 let job_producer_arc = Arc::new(job_producer);
1208
1209 let ctx = TestCtx {
1210 relayer_model,
1211 mock_repo: mock_relayer_repo,
1212 provider: provider_arc.clone(),
1213 jupiter: jupiter_arc.clone(),
1214 signer: signer_arc.clone(),
1215 dex,
1216 job_producer: job_producer_arc.clone(),
1217 ..Default::default()
1218 };
1219 let solana_relayer = ctx.into_relayer().await;
1220 let res = solana_relayer
1221 .handle_token_swap_request(create_test_relayer().id)
1222 .await
1223 .unwrap();
1224 assert_eq!(res.len(), 1);
1225 let swap = &res[0];
1226 assert_eq!(swap.source_amount, 10000000);
1227 assert_eq!(swap.destination_amount, 10);
1228 assert_eq!(swap.transaction_signature, test_signature.to_string());
1229 }
1230
1231 #[tokio::test]
1232 async fn test_handle_token_swap_request_successful_swap_jupiter_ultra_strategy() {
1233 let mut relayer_model = create_test_relayer();
1234
1235 let mut mock_relayer_repo = MockRelayerRepository::new();
1236 let id = relayer_model.id.clone();
1237
1238 relayer_model.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
1239 swap_config: Some(RelayerSolanaSwapConfig {
1240 strategy: Some(SolanaSwapStrategy::JupiterUltra),
1241 cron_schedule: None,
1242 min_balance_threshold: None,
1243 jupiter_swap_options: None,
1244 }),
1245 allowed_tokens: Some(vec![create_token_policy(
1246 "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
1247 Some(1),
1248 None,
1249 None,
1250 Some(50),
1251 )]),
1252 ..Default::default()
1253 });
1254 let cloned = relayer_model.clone();
1255
1256 mock_relayer_repo
1257 .expect_get_by_id()
1258 .with(eq(id.clone()))
1259 .times(1)
1260 .returning(move |_| Ok(cloned.clone()));
1261
1262 let mut raw_provider = MockSolanaProviderTrait::new();
1263
1264 raw_provider
1265 .expect_get_account_from_pubkey()
1266 .returning(|_| {
1267 Box::pin(async {
1268 let mut account_data = vec![0; SplAccount::LEN];
1269
1270 let token_account = spl_token::state::Account {
1271 mint: Pubkey::new_unique(),
1272 owner: Pubkey::new_unique(),
1273 amount: 10000000,
1274 state: spl_token::state::AccountState::Initialized,
1275 ..Default::default()
1276 };
1277 spl_token::state::Account::pack(token_account, &mut account_data).unwrap();
1278
1279 Ok(solana_sdk::account::Account {
1280 lamports: 1_000_000,
1281 data: account_data,
1282 owner: spl_token::id(),
1283 executable: false,
1284 rent_epoch: 0,
1285 })
1286 })
1287 });
1288
1289 let mut jupiter_mock = MockJupiterServiceTrait::new();
1290 jupiter_mock.expect_get_ultra_order().returning(|_| {
1291 Box::pin(async {
1292 Ok(UltraOrderResponse {
1293 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()),
1294 input_mint: "PjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
1295 output_mint: WRAPPED_SOL_MINT.to_string(),
1296 in_amount: 10,
1297 out_amount: 10,
1298 other_amount_threshold: 1,
1299 swap_mode: "ExactIn".to_string(),
1300 price_impact_pct: 0.0,
1301 route_plan: vec![RoutePlan {
1302 percent: 100,
1303 swap_info: SwapInfo {
1304 amm_key: "mock_amm_key".to_string(),
1305 label: "mock_label".to_string(),
1306 input_mint: "PjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
1307 output_mint: WRAPPED_SOL_MINT.to_string(),
1308 in_amount: "1000".to_string(),
1309 out_amount: "1000".to_string(),
1310 fee_amount: "0".to_string(),
1311 fee_mint: "mock_fee_mint".to_string(),
1312 },
1313 }],
1314 prioritization_fee_lamports: 0,
1315 request_id: "mock_request_id".to_string(),
1316 slippage_bps: 0,
1317 })
1318 })
1319 });
1320
1321 jupiter_mock.expect_execute_ultra_order().returning(|_| {
1322 Box::pin(async {
1323 Ok(UltraExecuteResponse {
1324 signature: Some("2jg9xbGLtZRsiJBrDWQnz33JuLjDkiKSZuxZPdjJ3qrJbMeTEerXFAKynkPW63J88nq63cvosDNRsg9VqHtGixvP".to_string()),
1325 status: "success".to_string(),
1326 slot: Some("123456789".to_string()),
1327 error: None,
1328 code: 0,
1329 total_input_amount: Some("1000000".to_string()),
1330 total_output_amount: Some("1000000".to_string()),
1331 input_amount_result: Some("1000000".to_string()),
1332 output_amount_result: Some("1000000".to_string()),
1333 swap_events: Some(vec![SwapEvents {
1334 input_mint: "mock_input_mint".to_string(),
1335 output_mint: "mock_output_mint".to_string(),
1336 input_amount: "1000000".to_string(),
1337 output_amount: "1000000".to_string(),
1338 }]),
1339 })
1340 })
1341 });
1342
1343 let mut signer = MockSolanaSignTrait::new();
1344 let test_signature = Signature::from_str("2jg9xbGLtZRsiJBrDWQnz33JuLjDkiKSZuxZPdjJ3qrJbMeTEerXFAKynkPW63J88nq63cvosDNRsg9VqHtGixvP").unwrap();
1345
1346 signer
1347 .expect_sign()
1348 .times(1)
1349 .returning(move |_| Box::pin(async move { Ok(test_signature) }));
1350
1351 let provider_arc = Arc::new(raw_provider);
1352 let jupiter_arc = Arc::new(jupiter_mock);
1353 let signer_arc = Arc::new(signer);
1354
1355 let dex = Arc::new(
1356 create_network_dex_generic(
1357 &relayer_model,
1358 provider_arc.clone(),
1359 signer_arc.clone(),
1360 jupiter_arc.clone(),
1361 )
1362 .unwrap(),
1363 );
1364 let mut job_producer = MockJobProducerTrait::new();
1365 job_producer
1366 .expect_produce_send_notification_job()
1367 .times(1)
1368 .returning(|_, _| Box::pin(async { Ok(()) }));
1369
1370 let job_producer_arc = Arc::new(job_producer);
1371
1372 let ctx = TestCtx {
1373 relayer_model,
1374 mock_repo: mock_relayer_repo,
1375 provider: provider_arc.clone(),
1376 jupiter: jupiter_arc.clone(),
1377 signer: signer_arc.clone(),
1378 dex,
1379 job_producer: job_producer_arc.clone(),
1380 ..Default::default()
1381 };
1382 let solana_relayer = ctx.into_relayer().await;
1383
1384 let res = solana_relayer
1385 .handle_token_swap_request(create_test_relayer().id)
1386 .await
1387 .unwrap();
1388 assert_eq!(res.len(), 1);
1389 let swap = &res[0];
1390 assert_eq!(swap.source_amount, 10000000);
1391 assert_eq!(swap.destination_amount, 10);
1392 assert_eq!(swap.transaction_signature, test_signature.to_string());
1393 }
1394
1395 #[tokio::test]
1396 async fn test_handle_token_swap_request_no_swap_config() {
1397 let mut relayer_model = create_test_relayer();
1398
1399 let mut mock_relayer_repo = MockRelayerRepository::new();
1400 let id = relayer_model.id.clone();
1401 let cloned = relayer_model.clone();
1402 mock_relayer_repo
1403 .expect_get_by_id()
1404 .with(eq(id.clone()))
1405 .times(1)
1406 .returning(move |_| Ok(cloned.clone()));
1407
1408 relayer_model.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
1409 swap_config: Some(RelayerSolanaSwapConfig {
1410 strategy: Some(SolanaSwapStrategy::JupiterSwap),
1411 cron_schedule: None,
1412 min_balance_threshold: None,
1413 jupiter_swap_options: None,
1414 }),
1415 allowed_tokens: Some(vec![create_token_policy(
1416 "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
1417 Some(1),
1418 None,
1419 None,
1420 Some(50),
1421 )]),
1422 ..Default::default()
1423 });
1424 let mut job_producer = MockJobProducerTrait::new();
1425 job_producer.expect_produce_send_notification_job().times(0);
1426
1427 let job_producer_arc = Arc::new(job_producer);
1428
1429 let ctx = TestCtx {
1430 relayer_model,
1431 mock_repo: mock_relayer_repo,
1432 job_producer: job_producer_arc,
1433 ..Default::default()
1434 };
1435 let solana_relayer = ctx.into_relayer().await;
1436
1437 let res = solana_relayer.handle_token_swap_request(id).await;
1438 assert!(res.is_ok());
1439 assert!(res.unwrap().is_empty());
1440 }
1441
1442 #[tokio::test]
1443 async fn test_handle_token_swap_request_no_strategy() {
1444 let mut relayer_model: RelayerRepoModel = create_test_relayer();
1445
1446 let mut mock_relayer_repo = MockRelayerRepository::new();
1447 let id = relayer_model.id.clone();
1448 let cloned = relayer_model.clone();
1449 mock_relayer_repo
1450 .expect_get_by_id()
1451 .with(eq(id.clone()))
1452 .times(1)
1453 .returning(move |_| Ok(cloned.clone()));
1454
1455 relayer_model.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
1456 swap_config: Some(RelayerSolanaSwapConfig {
1457 strategy: None,
1458 cron_schedule: None,
1459 min_balance_threshold: Some(1),
1460 jupiter_swap_options: None,
1461 }),
1462 ..Default::default()
1463 });
1464
1465 let ctx = TestCtx {
1466 relayer_model,
1467 mock_repo: mock_relayer_repo,
1468 ..Default::default()
1469 };
1470 let solana_relayer = ctx.into_relayer().await;
1471
1472 let res = solana_relayer.handle_token_swap_request(id).await.unwrap();
1473 assert!(res.is_empty(), "should return empty when no strategy");
1474 }
1475
1476 #[tokio::test]
1477 async fn test_handle_token_swap_request_no_allowed_tokens() {
1478 let mut relayer_model: RelayerRepoModel = create_test_relayer();
1479 let mut mock_relayer_repo = MockRelayerRepository::new();
1480 let id = relayer_model.id.clone();
1481 let cloned = relayer_model.clone();
1482 mock_relayer_repo
1483 .expect_get_by_id()
1484 .with(eq(id.clone()))
1485 .times(1)
1486 .returning(move |_| Ok(cloned.clone()));
1487
1488 relayer_model.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
1489 swap_config: Some(RelayerSolanaSwapConfig {
1490 strategy: Some(SolanaSwapStrategy::JupiterSwap),
1491 cron_schedule: None,
1492 min_balance_threshold: Some(1),
1493 jupiter_swap_options: None,
1494 }),
1495 allowed_tokens: None,
1496 ..Default::default()
1497 });
1498
1499 let ctx = TestCtx {
1500 relayer_model,
1501 mock_repo: mock_relayer_repo,
1502 ..Default::default()
1503 };
1504 let solana_relayer = ctx.into_relayer().await;
1505
1506 let res = solana_relayer.handle_token_swap_request(id).await.unwrap();
1507 assert!(res.is_empty(), "should return empty when no allowed_tokens");
1508 }
1509
1510 #[tokio::test]
1511 async fn test_validate_rpc_success() {
1512 let mut raw_provider = MockSolanaProviderTrait::new();
1513 raw_provider
1514 .expect_get_latest_blockhash()
1515 .times(1)
1516 .returning(|| Box::pin(async { Ok(Hash::new_unique()) }));
1517
1518 let ctx = TestCtx {
1519 provider: Arc::new(raw_provider),
1520 ..Default::default()
1521 };
1522 let solana_relayer = ctx.into_relayer().await;
1523 let res = solana_relayer.validate_rpc().await;
1524
1525 assert!(
1526 res.is_ok(),
1527 "validate_rpc should succeed when blockhash fetch succeeds"
1528 );
1529 }
1530
1531 #[tokio::test]
1532 async fn test_validate_rpc_provider_error() {
1533 let mut raw_provider = MockSolanaProviderTrait::new();
1534 raw_provider
1535 .expect_get_latest_blockhash()
1536 .times(1)
1537 .returning(|| {
1538 Box::pin(async { Err(SolanaProviderError::RpcError("rpc failure".to_string())) })
1539 });
1540
1541 let ctx = TestCtx {
1542 provider: Arc::new(raw_provider),
1543 ..Default::default()
1544 };
1545
1546 let solana_relayer = ctx.into_relayer().await;
1547 let err = solana_relayer.validate_rpc().await.unwrap_err();
1548
1549 match err {
1550 RelayerError::ProviderError(msg) => {
1551 assert!(msg.contains("rpc failure"));
1552 }
1553 other => panic!("expected ProviderError, got {:?}", other),
1554 }
1555 }
1556
1557 #[tokio::test]
1558 async fn test_check_balance_no_swap_config() {
1559 let ctx = TestCtx::default();
1561 let solana_relayer = ctx.into_relayer().await;
1562
1563 assert!(solana_relayer
1565 .check_balance_and_trigger_token_swap_if_needed()
1566 .await
1567 .is_ok());
1568 }
1569
1570 #[tokio::test]
1571 async fn test_check_balance_no_threshold() {
1572 let mut ctx = TestCtx::default();
1574 let mut model = ctx.relayer_model.clone();
1575 model.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
1576 swap_config: Some(RelayerSolanaSwapConfig {
1577 strategy: Some(SolanaSwapStrategy::JupiterSwap),
1578 cron_schedule: None,
1579 min_balance_threshold: None,
1580 jupiter_swap_options: None,
1581 }),
1582 ..Default::default()
1583 });
1584 ctx.relayer_model = model;
1585 let solana_relayer = ctx.into_relayer().await;
1586
1587 assert!(solana_relayer
1588 .check_balance_and_trigger_token_swap_if_needed()
1589 .await
1590 .is_ok());
1591 }
1592
1593 #[tokio::test]
1594 async fn test_check_balance_above_threshold() {
1595 let mut raw_provider = MockSolanaProviderTrait::new();
1596 raw_provider
1597 .expect_get_balance()
1598 .times(1)
1599 .returning(|_| Box::pin(async { Ok(20_u64) }));
1600 let provider = Arc::new(raw_provider);
1601 let mut raw_job = MockJobProducerTrait::new();
1602 raw_job
1603 .expect_produce_solana_token_swap_request_job()
1604 .withf(move |req, _opts| req.relayer_id == "test-id")
1605 .times(0);
1606 let job_producer = Arc::new(raw_job);
1607
1608 let ctx = TestCtx {
1609 provider,
1610 job_producer,
1611 ..Default::default()
1612 };
1613 let mut model = ctx.relayer_model.clone();
1615 model.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
1616 swap_config: Some(RelayerSolanaSwapConfig {
1617 strategy: Some(SolanaSwapStrategy::JupiterSwap),
1618 cron_schedule: None,
1619 min_balance_threshold: Some(10),
1620 jupiter_swap_options: None,
1621 }),
1622 ..Default::default()
1623 });
1624 let mut ctx = ctx;
1625 ctx.relayer_model = model;
1626
1627 let solana_relayer = ctx.into_relayer().await;
1628 assert!(solana_relayer
1629 .check_balance_and_trigger_token_swap_if_needed()
1630 .await
1631 .is_ok());
1632 }
1633
1634 #[tokio::test]
1635 async fn test_check_balance_below_threshold_triggers_job() {
1636 let mut raw_provider = MockSolanaProviderTrait::new();
1637 raw_provider
1638 .expect_get_balance()
1639 .times(1)
1640 .returning(|_| Box::pin(async { Ok(5_u64) }));
1641
1642 let mut raw_job = MockJobProducerTrait::new();
1643 raw_job
1644 .expect_produce_solana_token_swap_request_job()
1645 .times(1)
1646 .returning(|_, _| Box::pin(async { Ok(()) }));
1647 let job_producer = Arc::new(raw_job);
1648
1649 let mut model = create_test_relayer();
1650 model.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
1651 swap_config: Some(RelayerSolanaSwapConfig {
1652 strategy: Some(SolanaSwapStrategy::JupiterSwap),
1653 cron_schedule: None,
1654 min_balance_threshold: Some(10),
1655 jupiter_swap_options: None,
1656 }),
1657 ..Default::default()
1658 });
1659
1660 let ctx = TestCtx {
1661 relayer_model: model,
1662 provider: Arc::new(raw_provider),
1663 job_producer,
1664 ..Default::default()
1665 };
1666
1667 let solana_relayer = ctx.into_relayer().await;
1668 assert!(solana_relayer
1669 .check_balance_and_trigger_token_swap_if_needed()
1670 .await
1671 .is_ok());
1672 }
1673
1674 #[tokio::test]
1675 async fn test_get_balance_success() {
1676 let mut raw_provider = MockSolanaProviderTrait::new();
1677 raw_provider
1678 .expect_get_balance()
1679 .times(1)
1680 .returning(|_| Box::pin(async { Ok(42_u64) }));
1681 let ctx = TestCtx {
1682 provider: Arc::new(raw_provider),
1683 ..Default::default()
1684 };
1685 let solana_relayer = ctx.into_relayer().await;
1686
1687 let res = solana_relayer.get_balance().await.unwrap();
1688
1689 assert_eq!(res.balance, 42_u128);
1690 assert_eq!(res.unit, SOLANA_SMALLEST_UNIT_NAME);
1691 }
1692
1693 #[tokio::test]
1694 async fn test_get_balance_provider_error() {
1695 let mut raw_provider = MockSolanaProviderTrait::new();
1696 raw_provider
1697 .expect_get_balance()
1698 .times(1)
1699 .returning(|_| Box::pin(async { Err(SolanaProviderError::RpcError("oops".into())) }));
1700 let ctx = TestCtx {
1701 provider: Arc::new(raw_provider),
1702 ..Default::default()
1703 };
1704 let solana_relayer = ctx.into_relayer().await;
1705
1706 let err = solana_relayer.get_balance().await.unwrap_err();
1707
1708 match err {
1709 RelayerError::UnderlyingSolanaProvider(err) => {
1710 assert!(err.to_string().contains("oops"));
1711 }
1712 other => panic!("expected ProviderError, got {:?}", other),
1713 }
1714 }
1715
1716 #[tokio::test]
1717 async fn test_validate_min_balance_success() {
1718 let mut raw_provider = MockSolanaProviderTrait::new();
1719 raw_provider
1720 .expect_get_balance()
1721 .times(1)
1722 .returning(|_| Box::pin(async { Ok(100_u64) }));
1723
1724 let mut model = create_test_relayer();
1725 model.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
1726 min_balance: Some(50),
1727 ..Default::default()
1728 });
1729
1730 let ctx = TestCtx {
1731 relayer_model: model,
1732 provider: Arc::new(raw_provider),
1733 ..Default::default()
1734 };
1735
1736 let solana_relayer = ctx.into_relayer().await;
1737 assert!(solana_relayer.validate_min_balance().await.is_ok());
1738 }
1739
1740 #[tokio::test]
1741 async fn test_validate_min_balance_insufficient() {
1742 let mut raw_provider = MockSolanaProviderTrait::new();
1743 raw_provider
1744 .expect_get_balance()
1745 .times(1)
1746 .returning(|_| Box::pin(async { Ok(10_u64) }));
1747
1748 let mut model = create_test_relayer();
1749 model.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
1750 min_balance: Some(50),
1751 ..Default::default()
1752 });
1753
1754 let ctx = TestCtx {
1755 relayer_model: model,
1756 provider: Arc::new(raw_provider),
1757 ..Default::default()
1758 };
1759
1760 let solana_relayer = ctx.into_relayer().await;
1761 let err = solana_relayer.validate_min_balance().await.unwrap_err();
1762 match err {
1763 RelayerError::InsufficientBalanceError(msg) => {
1764 assert_eq!(msg, "Insufficient balance");
1765 }
1766 other => panic!("expected InsufficientBalanceError, got {:?}", other),
1767 }
1768 }
1769
1770 #[tokio::test]
1771 async fn test_validate_min_balance_provider_error() {
1772 let mut raw_provider = MockSolanaProviderTrait::new();
1773 raw_provider
1774 .expect_get_balance()
1775 .times(1)
1776 .returning(|_| Box::pin(async { Err(SolanaProviderError::RpcError("fail".into())) }));
1777 let ctx = TestCtx {
1778 provider: Arc::new(raw_provider),
1779 ..Default::default()
1780 };
1781
1782 let solana_relayer = ctx.into_relayer().await;
1783 let err = solana_relayer.validate_min_balance().await.unwrap_err();
1784 match err {
1785 RelayerError::ProviderError(msg) => {
1786 assert!(msg.contains("fail"));
1787 }
1788 other => panic!("expected ProviderError, got {:?}", other),
1789 }
1790 }
1791
1792 #[tokio::test]
1793 async fn test_rpc_invalid_params() {
1794 let ctx = TestCtx::default();
1795 let solana_relayer = ctx.into_relayer().await;
1796
1797 let req = JsonRpcRequest {
1798 jsonrpc: "2.0".to_string(),
1799 params: NetworkRpcRequest::Solana(crate::models::SolanaRpcRequest::FeeEstimate(
1800 FeeEstimateRequestParams {
1801 transaction: EncodedSerializedTransaction::new("".to_string()),
1802 fee_token: "".to_string(),
1803 },
1804 )),
1805 id: Some(JsonRpcId::Number(1)),
1806 };
1807 let resp = solana_relayer.rpc(req).await.unwrap();
1808
1809 assert!(resp.error.is_some(), "expected an error object");
1810 let err = resp.error.unwrap();
1811 assert_eq!(err.code, -32601);
1812 assert_eq!(err.message, "INVALID_PARAMS");
1813 }
1814
1815 #[tokio::test]
1816 async fn test_rpc_success() {
1817 let ctx = TestCtx::default();
1818 let solana_relayer = ctx.into_relayer().await;
1819
1820 let req = JsonRpcRequest {
1821 jsonrpc: "2.0".to_string(),
1822 params: NetworkRpcRequest::Solana(crate::models::SolanaRpcRequest::GetFeaturesEnabled(
1823 GetFeaturesEnabledRequestParams {},
1824 )),
1825 id: Some(JsonRpcId::Number(1)),
1826 };
1827 let resp = solana_relayer.rpc(req).await.unwrap();
1828
1829 assert!(resp.error.is_none(), "error should be None");
1830 let data = resp.result.unwrap();
1831 let sol_res = match data {
1832 NetworkRpcResult::Solana(inner) => inner,
1833 other => panic!("expected Solana, got {:?}", other),
1834 };
1835 let features = match sol_res {
1836 SolanaRpcResult::GetFeaturesEnabled(f) => f,
1837 other => panic!("expected GetFeaturesEnabled, got {:?}", other),
1838 };
1839 assert_eq!(features.features, vec!["gasless".to_string()]);
1840 }
1841
1842 #[tokio::test]
1843 async fn test_initialize_relayer_disables_when_validation_fails() {
1844 let mut raw_provider = MockSolanaProviderTrait::new();
1845 let mut mock_repo = MockRelayerRepository::new();
1846 let mut job_producer = MockJobProducerTrait::new();
1847
1848 let mut relayer_model = create_test_relayer();
1849 relayer_model.system_disabled = false; relayer_model.notification_id = Some("test-notification-id".to_string());
1851
1852 raw_provider.expect_get_latest_blockhash().returning(|| {
1854 Box::pin(async { Err(SolanaProviderError::RpcError("RPC error".to_string())) })
1855 });
1856
1857 raw_provider
1858 .expect_get_balance()
1859 .returning(|_| Box::pin(async { Ok(1000000u64) })); let mut disabled_relayer = relayer_model.clone();
1863 disabled_relayer.system_disabled = true;
1864 mock_repo
1865 .expect_disable_relayer()
1866 .with(eq("test-relayer-id".to_string()), always())
1867 .returning(move |_, _| Ok(disabled_relayer.clone()));
1868
1869 job_producer
1871 .expect_produce_send_notification_job()
1872 .returning(|_, _| Box::pin(async { Ok(()) }));
1873
1874 job_producer
1876 .expect_produce_relayer_health_check_job()
1877 .returning(|_, _| Box::pin(async { Ok(()) }));
1878
1879 let ctx = TestCtx {
1880 relayer_model,
1881 mock_repo,
1882 provider: Arc::new(raw_provider),
1883 job_producer: Arc::new(job_producer),
1884 ..Default::default()
1885 };
1886
1887 let solana_relayer = ctx.into_relayer().await;
1888 let result = solana_relayer.initialize_relayer().await;
1889 assert!(result.is_ok());
1890 }
1891
1892 #[tokio::test]
1893 async fn test_initialize_relayer_enables_when_validation_passes_and_was_disabled() {
1894 let mut raw_provider = MockSolanaProviderTrait::new();
1895 let mut mock_repo = MockRelayerRepository::new();
1896
1897 let mut relayer_model = create_test_relayer();
1898 relayer_model.system_disabled = true; raw_provider
1902 .expect_get_latest_blockhash()
1903 .returning(|| Box::pin(async { Ok(Hash::new_unique()) }));
1904
1905 raw_provider
1906 .expect_get_balance()
1907 .returning(|_| Box::pin(async { Ok(1000000u64) })); let mut enabled_relayer = relayer_model.clone();
1911 enabled_relayer.system_disabled = false;
1912 mock_repo
1913 .expect_enable_relayer()
1914 .with(eq("test-relayer-id".to_string()))
1915 .returning(move |_| Ok(enabled_relayer.clone()));
1916
1917 let mut disabled_relayer = relayer_model.clone();
1919 disabled_relayer.system_disabled = true;
1920 mock_repo
1921 .expect_disable_relayer()
1922 .returning(move |_, _| Ok(disabled_relayer.clone()));
1923
1924 let ctx = TestCtx {
1925 relayer_model,
1926 mock_repo,
1927 provider: Arc::new(raw_provider),
1928 ..Default::default()
1929 };
1930
1931 let solana_relayer = ctx.into_relayer().await;
1932 let result = solana_relayer.initialize_relayer().await;
1933 assert!(result.is_ok());
1934 }
1935
1936 #[tokio::test]
1937 async fn test_initialize_relayer_no_action_when_enabled_and_validation_passes() {
1938 let mut raw_provider = MockSolanaProviderTrait::new();
1939 let mock_repo = MockRelayerRepository::new();
1940
1941 let mut relayer_model = create_test_relayer();
1942 relayer_model.system_disabled = false; raw_provider
1946 .expect_get_latest_blockhash()
1947 .returning(|| Box::pin(async { Ok(Hash::new_unique()) }));
1948
1949 raw_provider
1950 .expect_get_balance()
1951 .returning(|_| Box::pin(async { Ok(1000000u64) })); let ctx = TestCtx {
1954 relayer_model,
1955 mock_repo,
1956 provider: Arc::new(raw_provider),
1957 ..Default::default()
1958 };
1959
1960 let solana_relayer = ctx.into_relayer().await;
1961 let result = solana_relayer.initialize_relayer().await;
1962 assert!(result.is_ok());
1963 }
1964
1965 #[tokio::test]
1966 async fn test_initialize_relayer_sends_notification_when_disabled() {
1967 let mut raw_provider = MockSolanaProviderTrait::new();
1968 let mut mock_repo = MockRelayerRepository::new();
1969 let mut job_producer = MockJobProducerTrait::new();
1970
1971 let mut relayer_model = create_test_relayer();
1972 relayer_model.system_disabled = false; relayer_model.notification_id = Some("test-notification-id".to_string());
1974
1975 raw_provider
1977 .expect_get_latest_blockhash()
1978 .returning(|| Box::pin(async { Ok(Hash::new_unique()) }));
1979
1980 raw_provider
1981 .expect_get_balance()
1982 .returning(|_| Box::pin(async { Ok(100u64) })); let mut disabled_relayer = relayer_model.clone();
1986 disabled_relayer.system_disabled = true;
1987 mock_repo
1988 .expect_disable_relayer()
1989 .with(eq("test-relayer-id".to_string()), always())
1990 .returning(move |_, _| Ok(disabled_relayer.clone()));
1991
1992 job_producer
1994 .expect_produce_send_notification_job()
1995 .returning(|_, _| Box::pin(async { Ok(()) }));
1996
1997 job_producer
1999 .expect_produce_relayer_health_check_job()
2000 .returning(|_, _| Box::pin(async { Ok(()) }));
2001
2002 let ctx = TestCtx {
2003 relayer_model,
2004 mock_repo,
2005 provider: Arc::new(raw_provider),
2006 job_producer: Arc::new(job_producer),
2007 ..Default::default()
2008 };
2009
2010 let solana_relayer = ctx.into_relayer().await;
2011 let result = solana_relayer.initialize_relayer().await;
2012 assert!(result.is_ok());
2013 }
2014
2015 #[tokio::test]
2016 async fn test_initialize_relayer_no_notification_when_no_notification_id() {
2017 let mut raw_provider = MockSolanaProviderTrait::new();
2018 let mut mock_repo = MockRelayerRepository::new();
2019
2020 let mut relayer_model = create_test_relayer();
2021 relayer_model.system_disabled = false; relayer_model.notification_id = None; raw_provider.expect_get_latest_blockhash().returning(|| {
2026 Box::pin(async {
2027 Err(SolanaProviderError::RpcError(
2028 "RPC validation failed".to_string(),
2029 ))
2030 })
2031 });
2032
2033 raw_provider
2034 .expect_get_balance()
2035 .returning(|_| Box::pin(async { Ok(1000000u64) })); let mut disabled_relayer = relayer_model.clone();
2039 disabled_relayer.system_disabled = true;
2040 mock_repo
2041 .expect_disable_relayer()
2042 .with(eq("test-relayer-id".to_string()), always())
2043 .returning(move |_, _| Ok(disabled_relayer.clone()));
2044
2045 let mut job_producer = MockJobProducerTrait::new();
2048 job_producer
2049 .expect_produce_relayer_health_check_job()
2050 .returning(|_, _| Box::pin(async { Ok(()) }));
2051
2052 let ctx = TestCtx {
2053 relayer_model,
2054 mock_repo,
2055 provider: Arc::new(raw_provider),
2056 job_producer: Arc::new(job_producer),
2057 ..Default::default()
2058 };
2059
2060 let solana_relayer = ctx.into_relayer().await;
2061 let result = solana_relayer.initialize_relayer().await;
2062 assert!(result.is_ok());
2063 }
2064
2065 #[tokio::test]
2066 async fn test_initialize_relayer_policy_validation_fails() {
2067 let mut raw_provider = MockSolanaProviderTrait::new();
2068
2069 let mut relayer_model = create_test_relayer();
2070 relayer_model.system_disabled = false;
2071
2072 relayer_model.policies = RelayerNetworkPolicy::Solana(RelayerSolanaPolicy {
2074 allowed_tokens: Some(vec![SolanaAllowedTokensPolicy {
2075 mint: "InvalidMintAddress".to_string(),
2076 decimals: Some(9),
2077 symbol: Some("INVALID".to_string()),
2078 max_allowed_fee: Some(0),
2079 swap_config: None,
2080 }]),
2081 ..Default::default()
2082 });
2083
2084 raw_provider
2086 .expect_get_token_metadata_from_pubkey()
2087 .returning(|_| {
2088 Box::pin(async {
2089 Err(SolanaProviderError::RpcError("Token not found".to_string()))
2090 })
2091 });
2092
2093 let ctx = TestCtx {
2094 relayer_model,
2095 provider: Arc::new(raw_provider),
2096 ..Default::default()
2097 };
2098
2099 let solana_relayer = ctx.into_relayer().await;
2100 let result = solana_relayer.initialize_relayer().await;
2101
2102 assert!(result.is_err());
2104 match result.unwrap_err() {
2105 RelayerError::PolicyConfigurationError(msg) => {
2106 assert!(msg.contains("Error while processing allowed tokens policy"));
2107 }
2108 other => panic!("Expected PolicyConfigurationError, got {:?}", other),
2109 }
2110 }
2111}