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