1use std::collections::HashMap;
2
3use crate::{
13 constants::{DEFAULT_SOLANA_MAX_TX_DATA_SIZE, DEFAULT_SOLANA_MIN_BALANCE},
14 domain::{SolanaTokenProgram, TokenInstruction as SolanaTokenInstruction},
15 models::RelayerSolanaPolicy,
16 services::SolanaProviderTrait,
17};
18use log::info;
19use solana_client::rpc_response::RpcSimulateTransactionResult;
20use solana_sdk::{
21 commitment_config::CommitmentConfig, pubkey::Pubkey, system_instruction::SystemInstruction,
22 transaction::Transaction,
23};
24use solana_system_interface::program;
25use thiserror::Error;
26
27#[derive(Debug, Error)]
28#[allow(dead_code)]
29pub enum SolanaTransactionValidationError {
30 #[error("Failed to decode transaction: {0}")]
31 DecodeError(String),
32 #[error("Failed to deserialize transaction: {0}")]
33 DeserializeError(String),
34 #[error("Validation error: {0}")]
35 SigningError(String),
36 #[error("Simulation error: {0}")]
37 SimulationError(String),
38 #[error("Policy violation: {0}")]
39 PolicyViolation(String),
40 #[error("Blockhash {0} is expired")]
41 ExpiredBlockhash(String),
42 #[error("Validation error: {0}")]
43 ValidationError(String),
44 #[error("Fee payer error: {0}")]
45 FeePayer(String),
46 #[error("Insufficient funds: {0}")]
47 InsufficientFunds(String),
48 #[error("Insufficient balance: {0}")]
49 InsufficientBalance(String),
50}
51
52#[allow(dead_code)]
53pub struct SolanaTransactionValidator {}
54
55#[allow(dead_code)]
56impl SolanaTransactionValidator {
57 pub fn validate_allowed_token(
58 token_mint: &str,
59 policy: &RelayerSolanaPolicy,
60 ) -> Result<(), SolanaTransactionValidationError> {
61 let allowed_token = policy.get_allowed_token_entry(token_mint);
62 if allowed_token.is_none() {
63 return Err(SolanaTransactionValidationError::PolicyViolation(format!(
64 "Token {} not allowed for transfers",
65 token_mint
66 )));
67 }
68
69 Ok(())
70 }
71
72 pub fn validate_fee_payer(
74 tx: &Transaction,
75 relayer_pubkey: &Pubkey,
76 ) -> Result<(), SolanaTransactionValidationError> {
77 let fee_payer = tx.message.account_keys.first().ok_or_else(|| {
79 SolanaTransactionValidationError::FeePayer("No fee payer account found".to_string())
80 })?;
81
82 if fee_payer != relayer_pubkey {
84 return Err(SolanaTransactionValidationError::PolicyViolation(format!(
85 "Fee payer {} does not match relayer address {}",
86 fee_payer, relayer_pubkey
87 )));
88 }
89
90 if tx.message.header.num_required_signatures < 1 {
92 return Err(SolanaTransactionValidationError::FeePayer(
93 "Fee payer must be a signer".to_string(),
94 ));
95 }
96
97 Ok(())
98 }
99
100 pub async fn validate_blockhash<T: SolanaProviderTrait>(
102 tx: &Transaction,
103 provider: &T,
104 ) -> Result<(), SolanaTransactionValidationError> {
105 let blockhash = tx.message.recent_blockhash;
106
107 let is_valid = provider
109 .is_blockhash_valid(&blockhash, CommitmentConfig::confirmed())
110 .await
111 .map_err(|e| {
112 SolanaTransactionValidationError::ValidationError(format!(
113 "Failed to check blockhash validity: {}",
114 e
115 ))
116 })?;
117
118 if !is_valid {
119 return Err(SolanaTransactionValidationError::ExpiredBlockhash(format!(
120 "Blockhash {} is no longer valid",
121 blockhash
122 )));
123 }
124
125 Ok(())
126 }
127
128 pub fn validate_max_signatures(
130 tx: &Transaction,
131 policy: &RelayerSolanaPolicy,
132 ) -> Result<(), SolanaTransactionValidationError> {
133 let num_signatures = tx.message.header.num_required_signatures;
134
135 let Some(max_signatures) = policy.max_signatures else {
136 return Ok(());
137 };
138
139 if num_signatures > max_signatures {
140 return Err(SolanaTransactionValidationError::PolicyViolation(format!(
141 "Transaction requires {} signatures, which exceeds maximum allowed {}",
142 num_signatures, max_signatures
143 )));
144 }
145
146 Ok(())
147 }
148
149 pub fn validate_allowed_programs(
151 tx: &Transaction,
152 policy: &RelayerSolanaPolicy,
153 ) -> Result<(), SolanaTransactionValidationError> {
154 if let Some(allowed_programs) = &policy.allowed_programs {
155 for program_id in tx
156 .message
157 .instructions
158 .iter()
159 .map(|ix| tx.message.account_keys[ix.program_id_index as usize])
160 {
161 if !allowed_programs.contains(&program_id.to_string()) {
162 return Err(SolanaTransactionValidationError::PolicyViolation(format!(
163 "Program {} not allowed",
164 program_id
165 )));
166 }
167 }
168 }
169
170 Ok(())
171 }
172
173 pub fn validate_allowed_account(
174 account: &str,
175 policy: &RelayerSolanaPolicy,
176 ) -> Result<(), SolanaTransactionValidationError> {
177 if let Some(allowed_accounts) = &policy.allowed_accounts {
178 if !allowed_accounts.contains(&account.to_string()) {
179 return Err(SolanaTransactionValidationError::PolicyViolation(format!(
180 "Account {} not allowed",
181 account
182 )));
183 }
184 }
185
186 Ok(())
187 }
188
189 pub fn validate_tx_allowed_accounts(
191 tx: &Transaction,
192 policy: &RelayerSolanaPolicy,
193 ) -> Result<(), SolanaTransactionValidationError> {
194 if let Some(allowed_accounts) = &policy.allowed_accounts {
195 for account_key in &tx.message.account_keys {
196 info!("Checking account {}", account_key);
197 if !allowed_accounts.contains(&account_key.to_string()) {
198 return Err(SolanaTransactionValidationError::PolicyViolation(format!(
199 "Account {} not allowed",
200 account_key
201 )));
202 }
203 }
204 }
205
206 Ok(())
207 }
208
209 pub fn validate_disallowed_account(
210 account: &str,
211 policy: &RelayerSolanaPolicy,
212 ) -> Result<(), SolanaTransactionValidationError> {
213 if let Some(disallowed_accounts) = &policy.disallowed_accounts {
214 if disallowed_accounts.contains(&account.to_string()) {
215 return Err(SolanaTransactionValidationError::PolicyViolation(format!(
216 "Account {} not allowed",
217 account
218 )));
219 }
220 }
221
222 Ok(())
223 }
224
225 pub fn validate_tx_disallowed_accounts(
227 tx: &Transaction,
228 policy: &RelayerSolanaPolicy,
229 ) -> Result<(), SolanaTransactionValidationError> {
230 let Some(disallowed_accounts) = &policy.disallowed_accounts else {
231 return Ok(());
232 };
233
234 for account_key in &tx.message.account_keys {
235 if disallowed_accounts.contains(&account_key.to_string()) {
236 return Err(SolanaTransactionValidationError::PolicyViolation(format!(
237 "Account {} is explicitly disallowed",
238 account_key
239 )));
240 }
241 }
242
243 Ok(())
244 }
245
246 pub fn validate_data_size(
248 tx: &Transaction,
249 config: &RelayerSolanaPolicy,
250 ) -> Result<(), SolanaTransactionValidationError> {
251 let max_size: usize = config
252 .max_tx_data_size
253 .unwrap_or(DEFAULT_SOLANA_MAX_TX_DATA_SIZE)
254 .into();
255 let tx_bytes = bincode::serialize(tx)
256 .map_err(|e| SolanaTransactionValidationError::DeserializeError(e.to_string()))?;
257
258 if tx_bytes.len() > max_size {
259 return Err(SolanaTransactionValidationError::PolicyViolation(format!(
260 "Transaction size {} exceeds maximum allowed {}",
261 tx_bytes.len(),
262 max_size
263 )));
264 }
265 Ok(())
266 }
267
268 pub async fn validate_lamports_transfers(
270 tx: &Transaction,
271 relayer_account: &Pubkey,
272 ) -> Result<(), SolanaTransactionValidationError> {
273 for (ix_index, ix) in tx.message.instructions.iter().enumerate() {
275 let program_id = tx.message.account_keys[ix.program_id_index as usize];
276
277 #[allow(clippy::collapsible_match)]
279 if program_id == program::id() {
280 if let Ok(system_ix) = bincode::deserialize::<SystemInstruction>(&ix.data) {
281 if let SystemInstruction::Transfer { .. } = system_ix {
282 let source_index = ix.accounts.first().ok_or_else(|| {
285 SolanaTransactionValidationError::ValidationError(format!(
286 "Missing source account in instruction {}",
287 ix_index
288 ))
289 })?;
290 let source_pubkey = &tx.message.account_keys[*source_index as usize];
291
292 if source_pubkey == relayer_account {
294 return Err(SolanaTransactionValidationError::PolicyViolation(
295 "Lamports transfers are not allowed from the relayer account"
296 .to_string(),
297 ));
298 }
299 }
300 }
301 }
302 }
303 Ok(())
304 }
305
306 pub fn validate_max_fee(
308 amount: u64,
309 policy: &RelayerSolanaPolicy,
310 ) -> Result<(), SolanaTransactionValidationError> {
311 if let Some(max_amount) = policy.max_allowed_fee_lamports {
312 if amount > max_amount {
313 return Err(SolanaTransactionValidationError::PolicyViolation(format!(
314 "Fee amount {} exceeds max allowed fee amount {}",
315 amount, max_amount
316 )));
317 }
318 }
319
320 Ok(())
321 }
322
323 pub async fn validate_sufficient_relayer_balance(
325 fee: u64,
326 relayer_address: &str,
327 policy: &RelayerSolanaPolicy,
328 provider: &impl SolanaProviderTrait,
329 ) -> Result<(), SolanaTransactionValidationError> {
330 let balance = provider
331 .get_balance(relayer_address)
332 .await
333 .map_err(|e| SolanaTransactionValidationError::ValidationError(e.to_string()))?;
334
335 let min_balance = policy.min_balance.unwrap_or(DEFAULT_SOLANA_MIN_BALANCE);
337 let required_balance = fee + min_balance;
338
339 if balance < required_balance {
340 return Err(SolanaTransactionValidationError::InsufficientBalance(format!(
341 "Insufficient relayer balance. Required: {}, Available: {}, Fee: {}, Min balance: {}",
342 required_balance, balance, fee, min_balance
343 )));
344 }
345
346 Ok(())
347 }
348
349 pub async fn validate_token_transfers(
351 tx: &Transaction,
352 policy: &RelayerSolanaPolicy,
353 provider: &impl SolanaProviderTrait,
354 relayer_account: &Pubkey,
355 ) -> Result<(), SolanaTransactionValidationError> {
356 let allowed_tokens = match &policy.allowed_tokens {
357 Some(tokens) if !tokens.is_empty() => tokens,
358 _ => return Ok(()), };
360
361 let mut account_transfers: HashMap<Pubkey, u64> = HashMap::new();
363 let mut account_balances: HashMap<Pubkey, u64> = HashMap::new();
364
365 for ix in &tx.message.instructions {
366 let program_id = tx.message.account_keys[ix.program_id_index as usize];
367
368 if !SolanaTokenProgram::is_token_program(&program_id) {
369 continue;
370 }
371
372 let token_ix = match SolanaTokenProgram::unpack_instruction(&program_id, &ix.data) {
373 Ok(ix) => ix,
374 Err(_) => continue, };
376
377 match token_ix {
379 SolanaTokenInstruction::Transfer { amount }
380 | SolanaTokenInstruction::TransferChecked { amount, .. } => {
381 let source_index = ix.accounts[0] as usize;
383 let source_pubkey = &tx.message.account_keys[source_index];
384
385 if !tx.message.is_maybe_writable(source_index, None) {
387 return Err(SolanaTransactionValidationError::ValidationError(
388 "Source account must be writable".to_string(),
389 ));
390 }
391 if tx.message.is_signer(source_index) {
392 return Err(SolanaTransactionValidationError::ValidationError(
393 "Source account must not be signer".to_string(),
394 ));
395 }
396
397 if source_pubkey == relayer_account {
398 return Err(SolanaTransactionValidationError::PolicyViolation(
399 "Relayer account cannot be source".to_string(),
400 ));
401 }
402
403 let dest_index = match token_ix {
404 SolanaTokenInstruction::TransferChecked { .. } => ix.accounts[2] as usize,
405 _ => ix.accounts[1] as usize,
406 };
407 let destination_pubkey = &tx.message.account_keys[dest_index];
408
409 if !tx.message.is_maybe_writable(dest_index, None) {
411 return Err(SolanaTransactionValidationError::ValidationError(
412 "Destination account must be writable".to_string(),
413 ));
414 }
415 if tx.message.is_signer(dest_index) {
416 return Err(SolanaTransactionValidationError::ValidationError(
417 "Destination account must not be signer".to_string(),
418 ));
419 }
420
421 let owner_index = match token_ix {
422 SolanaTokenInstruction::TransferChecked { .. } => ix.accounts[3] as usize,
423 _ => ix.accounts[2] as usize,
424 };
425 if !tx.message.is_signer(owner_index) {
427 return Err(SolanaTransactionValidationError::ValidationError(format!(
428 "Owner must be signer {}",
429 &tx.message.account_keys[owner_index]
430 )));
431 }
432
433 if !account_balances.contains_key(source_pubkey) {
435 let source_account = provider
436 .get_account_from_pubkey(source_pubkey)
437 .await
438 .map_err(|e| {
439 SolanaTransactionValidationError::ValidationError(e.to_string())
440 })?;
441
442 let token_account =
443 SolanaTokenProgram::unpack_account(&program_id, &source_account)
444 .map_err(|e| {
445 SolanaTransactionValidationError::ValidationError(format!(
446 "Invalid token account: {}",
447 e
448 ))
449 })?;
450
451 if token_account.is_frozen {
452 return Err(SolanaTransactionValidationError::PolicyViolation(
453 "Token account is frozen".to_string(),
454 ));
455 }
456
457 let token_config = allowed_tokens
458 .iter()
459 .find(|t| t.mint == token_account.mint.to_string());
460
461 if token_config.is_none() {
463 return Err(SolanaTransactionValidationError::PolicyViolation(
464 format!("Token {} not allowed for transfers", token_account.mint),
465 ));
466 }
467 account_balances.insert(*source_pubkey, token_account.amount);
469
470 if let (
472 Some(config),
473 SolanaTokenInstruction::TransferChecked { decimals, .. },
474 ) = (token_config, &token_ix)
475 {
476 if Some(*decimals) != config.decimals {
477 return Err(SolanaTransactionValidationError::ValidationError(
478 format!(
479 "Invalid decimals: expected {:?}, got {}",
480 config.decimals, decimals
481 ),
482 ));
483 }
484 }
485
486 if destination_pubkey == relayer_account {
488 if let Some(config) = token_config {
490 if let Some(max_fee) = config.max_allowed_fee {
491 if amount > max_fee {
492 return Err(
493 SolanaTransactionValidationError::PolicyViolation(
494 format!(
495 "Transfer amount {} exceeds max fee \
496 allowed {} for token {}",
497 amount, max_fee, token_account.mint
498 ),
499 ),
500 );
501 }
502 }
503 }
504 }
505 }
506
507 *account_transfers.entry(*source_pubkey).or_insert(0) += amount;
508 }
509 _ => {
510 for account in ix.accounts.iter() {
513 let account_index = *account as usize;
514 if account_index < tx.message.account_keys.len() {
515 let pubkey = &tx.message.account_keys[account_index];
516 if pubkey == relayer_account
517 && tx.message.is_maybe_writable(account_index, None)
518 && !tx.message.is_signer(account_index)
519 {
520 return Err(SolanaTransactionValidationError::PolicyViolation(
522 "Relayer account cannot be used as writable account in token instructions".to_string(),
523 ));
524 }
525 }
526 }
527 }
528 }
529 }
530
531 for (account, total_transfer) in account_transfers {
533 let balance = *account_balances.get(&account).unwrap();
534
535 if balance < total_transfer {
536 return Err(SolanaTransactionValidationError::ValidationError(
537 format!(
538 "Insufficient balance for cumulative transfers: account {} has balance {} but requires {} across all instructions",
539 account, balance, total_transfer
540 ),
541 ));
542 }
543 }
544 Ok(())
545 }
546
547 pub async fn simulate_transaction<T: SolanaProviderTrait>(
549 tx: &Transaction,
550 provider: &T,
551 ) -> Result<RpcSimulateTransactionResult, SolanaTransactionValidationError> {
552 let new_tx = Transaction::new_unsigned(tx.message.clone());
553
554 provider
555 .simulate_transaction(&new_tx)
556 .await
557 .map_err(|e| SolanaTransactionValidationError::SimulationError(e.to_string()))
558 }
559}
560
561#[cfg(test)]
562mod tests {
563 use crate::{
564 models::{relayer::SolanaAllowedTokensSwapConfig, SolanaAllowedTokensPolicy},
565 services::{MockSolanaProviderTrait, SolanaProviderError},
566 };
567
568 use super::*;
569 use mockall::predicate::*;
570 use solana_sdk::{
571 instruction::{AccountMeta, Instruction},
572 message::Message,
573 program_pack::Pack,
574 signature::{Keypair, Signer},
575 };
576 use solana_system_interface::{instruction, program};
577 use spl_token::{instruction as token_instruction, state::Account};
578
579 fn setup_token_transfer_test(
580 transfer_amount: Option<u64>,
581 ) -> (
582 Transaction,
583 RelayerSolanaPolicy,
584 MockSolanaProviderTrait,
585 Keypair, Pubkey, Pubkey, Pubkey, ) {
590 let owner = Keypair::new();
591 let mint = Pubkey::new_unique();
592 let source = Pubkey::new_unique();
593 let destination = Pubkey::new_unique();
594
595 let transfer_ix = token_instruction::transfer(
597 &spl_token::id(),
598 &source,
599 &destination,
600 &owner.pubkey(),
601 &[],
602 transfer_amount.unwrap_or(100),
603 )
604 .unwrap();
605
606 let message = Message::new(&[transfer_ix], Some(&owner.pubkey()));
607 let mut transaction = Transaction::new_unsigned(message);
608
609 if let Some(owner_index) = transaction
611 .message
612 .account_keys
613 .iter()
614 .position(|&pubkey| pubkey == owner.pubkey())
615 {
616 transaction.message.header.num_required_signatures = (owner_index + 1) as u8;
617 transaction.message.header.num_readonly_signed_accounts = 1;
618 }
619
620 let policy = RelayerSolanaPolicy {
621 allowed_tokens: Some(vec![SolanaAllowedTokensPolicy {
622 mint: mint.to_string(),
623 decimals: Some(9),
624 symbol: Some("USDC".to_string()),
625 max_allowed_fee: Some(100),
626 swap_config: Some(SolanaAllowedTokensSwapConfig {
627 ..Default::default()
628 }),
629 }]),
630 ..Default::default()
631 };
632
633 let mut mock_provider = MockSolanaProviderTrait::new();
634
635 let token_account = Account {
637 mint,
638 owner: owner.pubkey(),
639 amount: 999,
640 state: spl_token::state::AccountState::Initialized,
641 ..Default::default()
642 };
643 let mut account_data = vec![0; Account::LEN];
644 Account::pack(token_account, &mut account_data).unwrap();
645
646 mock_provider
647 .expect_get_account_from_pubkey()
648 .returning(move |_| {
649 let local_account_data = account_data.clone();
650 Box::pin(async move {
651 Ok(solana_sdk::account::Account {
652 lamports: 1000000,
653 data: local_account_data,
654 owner: spl_token::id(),
655 executable: false,
656 rent_epoch: 0,
657 })
658 })
659 });
660
661 (
662 transaction,
663 policy,
664 mock_provider,
665 owner,
666 mint,
667 source,
668 destination,
669 )
670 }
671
672 fn create_test_transaction(fee_payer: &Pubkey) -> Transaction {
673 let recipient = Pubkey::new_unique();
674 let instruction = instruction::transfer(fee_payer, &recipient, 1000);
675 let message = Message::new(&[instruction], Some(fee_payer));
676 Transaction::new_unsigned(message)
677 }
678
679 #[test]
680 fn test_validate_fee_payer_success() {
681 let relayer_keypair = Keypair::new();
682 let relayer_address = relayer_keypair.pubkey();
683 let tx = create_test_transaction(&relayer_address);
684
685 let result = SolanaTransactionValidator::validate_fee_payer(&tx, &relayer_address);
686
687 assert!(result.is_ok());
688 }
689
690 #[test]
691 fn test_validate_fee_payer_mismatch() {
692 let wrong_keypair = Keypair::new();
693 let relayer_address = Keypair::new().pubkey();
694
695 let tx = create_test_transaction(&wrong_keypair.pubkey());
696
697 let result = SolanaTransactionValidator::validate_fee_payer(&tx, &relayer_address);
698 assert!(matches!(
699 result.unwrap_err(),
700 SolanaTransactionValidationError::PolicyViolation(_)
701 ));
702 }
703
704 #[tokio::test]
705 async fn test_validate_blockhash_valid() {
706 let transaction = create_test_transaction(&Keypair::new().pubkey());
707 let mut mock_provider = MockSolanaProviderTrait::new();
708
709 mock_provider
710 .expect_is_blockhash_valid()
711 .with(
712 eq(transaction.message.recent_blockhash),
713 eq(CommitmentConfig::confirmed()),
714 )
715 .returning(|_, _| Box::pin(async { Ok(true) }));
716
717 let result =
718 SolanaTransactionValidator::validate_blockhash(&transaction, &mock_provider).await;
719
720 assert!(result.is_ok());
721 }
722
723 #[tokio::test]
724 async fn test_validate_blockhash_expired() {
725 let transaction = create_test_transaction(&Keypair::new().pubkey());
726 let mut mock_provider = MockSolanaProviderTrait::new();
727
728 mock_provider
729 .expect_is_blockhash_valid()
730 .returning(|_, _| Box::pin(async { Ok(false) }));
731
732 let result =
733 SolanaTransactionValidator::validate_blockhash(&transaction, &mock_provider).await;
734
735 assert!(matches!(
736 result.unwrap_err(),
737 SolanaTransactionValidationError::ExpiredBlockhash(_)
738 ));
739 }
740
741 #[tokio::test]
742 async fn test_validate_blockhash_provider_error() {
743 let transaction = create_test_transaction(&Keypair::new().pubkey());
744 let mut mock_provider = MockSolanaProviderTrait::new();
745
746 mock_provider.expect_is_blockhash_valid().returning(|_, _| {
747 Box::pin(async { Err(SolanaProviderError::RpcError("RPC error".to_string())) })
748 });
749
750 let result =
751 SolanaTransactionValidator::validate_blockhash(&transaction, &mock_provider).await;
752
753 assert!(matches!(
754 result.unwrap_err(),
755 SolanaTransactionValidationError::ValidationError(_)
756 ));
757 }
758
759 #[test]
760 fn test_validate_max_signatures_within_limit() {
761 let transaction = create_test_transaction(&Keypair::new().pubkey());
762 let policy = RelayerSolanaPolicy {
763 max_signatures: Some(2),
764 ..Default::default()
765 };
766
767 let result = SolanaTransactionValidator::validate_max_signatures(&transaction, &policy);
768 assert!(result.is_ok());
769 }
770
771 #[test]
772 fn test_validate_max_signatures_exceeds_limit() {
773 let transaction = create_test_transaction(&Keypair::new().pubkey());
774 let policy = RelayerSolanaPolicy {
775 max_signatures: Some(0),
776 ..Default::default()
777 };
778
779 let result = SolanaTransactionValidator::validate_max_signatures(&transaction, &policy);
780 assert!(matches!(
781 result.unwrap_err(),
782 SolanaTransactionValidationError::PolicyViolation(_)
783 ));
784 }
785
786 #[test]
787 fn test_validate_max_signatures_no_limit() {
788 let transaction = create_test_transaction(&Keypair::new().pubkey());
789 let policy = RelayerSolanaPolicy {
790 max_signatures: None,
791 ..Default::default()
792 };
793
794 let result = SolanaTransactionValidator::validate_max_signatures(&transaction, &policy);
795 assert!(result.is_ok());
796 }
797
798 #[test]
799 fn test_validate_max_signatures_exact_limit() {
800 let transaction = create_test_transaction(&Keypair::new().pubkey());
801 let policy = RelayerSolanaPolicy {
802 max_signatures: Some(1),
803 ..Default::default()
804 };
805
806 let result = SolanaTransactionValidator::validate_max_signatures(&transaction, &policy);
807 assert!(result.is_ok());
808 }
809
810 #[test]
811 fn test_validate_allowed_programs_success() {
812 let payer = Keypair::new();
813 let tx = create_test_transaction(&payer.pubkey());
814 let policy = RelayerSolanaPolicy {
815 allowed_programs: Some(vec![program::id().to_string()]),
816 ..Default::default()
817 };
818
819 let result = SolanaTransactionValidator::validate_allowed_programs(&tx, &policy);
820 assert!(result.is_ok());
821 }
822
823 #[test]
824 fn test_validate_allowed_programs_disallowed() {
825 let payer = Keypair::new();
826 let tx = create_test_transaction(&payer.pubkey());
827
828 let policy = RelayerSolanaPolicy {
829 allowed_programs: Some(vec![Pubkey::new_unique().to_string()]),
830 ..Default::default()
831 };
832
833 let result = SolanaTransactionValidator::validate_allowed_programs(&tx, &policy);
834 assert!(matches!(
835 result.unwrap_err(),
836 SolanaTransactionValidationError::PolicyViolation(_)
837 ));
838 }
839
840 #[test]
841 fn test_validate_allowed_programs_no_restrictions() {
842 let payer = Keypair::new();
843 let tx = create_test_transaction(&payer.pubkey());
844
845 let policy = RelayerSolanaPolicy {
846 allowed_programs: None,
847 ..Default::default()
848 };
849
850 let result = SolanaTransactionValidator::validate_allowed_programs(&tx, &policy);
851 assert!(result.is_ok());
852 }
853
854 #[test]
855 fn test_validate_allowed_programs_multiple_instructions() {
856 let payer = Keypair::new();
857 let recipient = Pubkey::new_unique();
858
859 let ix1 = instruction::transfer(&payer.pubkey(), &recipient, 1000);
860 let ix2 = instruction::transfer(&payer.pubkey(), &recipient, 2000);
861 let message = Message::new(&[ix1, ix2], Some(&payer.pubkey()));
862 let tx = Transaction::new_unsigned(message);
863
864 let policy = RelayerSolanaPolicy {
865 allowed_programs: Some(vec![program::id().to_string()]),
866 ..Default::default()
867 };
868
869 let result = SolanaTransactionValidator::validate_allowed_programs(&tx, &policy);
870 assert!(result.is_ok());
871 }
872
873 #[test]
874 fn test_validate_tx_allowed_accounts_success() {
875 let payer = Keypair::new();
876 let recipient = Pubkey::new_unique();
877
878 let ix = instruction::transfer(&payer.pubkey(), &recipient, 1000);
879 let message = Message::new(&[ix], Some(&payer.pubkey()));
880 let tx = Transaction::new_unsigned(message);
881
882 let policy = RelayerSolanaPolicy {
883 allowed_accounts: Some(vec![
884 payer.pubkey().to_string(),
885 recipient.to_string(),
886 program::id().to_string(),
887 ]),
888 ..Default::default()
889 };
890
891 let result = SolanaTransactionValidator::validate_tx_allowed_accounts(&tx, &policy);
892 assert!(result.is_ok());
893 }
894
895 #[test]
896 fn test_validate_tx_allowed_accounts_disallowed() {
897 let payer = Keypair::new();
898
899 let tx = create_test_transaction(&payer.pubkey());
900
901 let policy = RelayerSolanaPolicy {
902 allowed_accounts: Some(vec![payer.pubkey().to_string()]),
903 ..Default::default()
904 };
905
906 let result = SolanaTransactionValidator::validate_tx_allowed_accounts(&tx, &policy);
907 assert!(matches!(
908 result.unwrap_err(),
909 SolanaTransactionValidationError::PolicyViolation(_)
910 ));
911 }
912
913 #[test]
914 fn test_validate_tx_allowed_accounts_no_restrictions() {
915 let tx = create_test_transaction(&Keypair::new().pubkey());
916
917 let policy = RelayerSolanaPolicy {
918 allowed_accounts: None,
919 ..Default::default()
920 };
921
922 let result = SolanaTransactionValidator::validate_tx_allowed_accounts(&tx, &policy);
923 assert!(result.is_ok());
924 }
925
926 #[test]
927 fn test_validate_tx_allowed_accounts_system_program() {
928 let payer = Keypair::new();
929 let tx = create_test_transaction(&payer.pubkey());
930
931 let policy = RelayerSolanaPolicy {
932 allowed_accounts: Some(vec![payer.pubkey().to_string(), program::id().to_string()]),
933 ..Default::default()
934 };
935
936 let result = SolanaTransactionValidator::validate_tx_allowed_accounts(&tx, &policy);
937 assert!(matches!(
938 result.unwrap_err(),
939 SolanaTransactionValidationError::PolicyViolation(_)
940 ));
941 }
942
943 #[test]
944 fn test_validate_tx_disallowed_accounts_success() {
945 let payer = Keypair::new();
946
947 let tx = create_test_transaction(&payer.pubkey());
948
949 let policy = RelayerSolanaPolicy {
950 disallowed_accounts: Some(vec![Pubkey::new_unique().to_string()]),
951 ..Default::default()
952 };
953
954 let result = SolanaTransactionValidator::validate_tx_disallowed_accounts(&tx, &policy);
955 assert!(result.is_ok());
956 }
957
958 #[test]
959 fn test_validate_tx_disallowed_accounts_blocked() {
960 let payer = Keypair::new();
961 let recipient = Pubkey::new_unique();
962
963 let ix = instruction::transfer(&payer.pubkey(), &recipient, 1000);
964 let message = Message::new(&[ix], Some(&payer.pubkey()));
965 let tx = Transaction::new_unsigned(message);
966
967 let policy = RelayerSolanaPolicy {
968 disallowed_accounts: Some(vec![recipient.to_string()]),
969 ..Default::default()
970 };
971
972 let result = SolanaTransactionValidator::validate_tx_disallowed_accounts(&tx, &policy);
973 assert!(matches!(
974 result.unwrap_err(),
975 SolanaTransactionValidationError::PolicyViolation(_)
976 ));
977 }
978
979 #[test]
980 fn test_validate_tx_disallowed_accounts_no_restrictions() {
981 let tx = create_test_transaction(&Keypair::new().pubkey());
982
983 let policy = RelayerSolanaPolicy {
984 disallowed_accounts: None,
985 ..Default::default()
986 };
987
988 let result = SolanaTransactionValidator::validate_tx_disallowed_accounts(&tx, &policy);
989 assert!(result.is_ok());
990 }
991
992 #[test]
993 fn test_validate_tx_disallowed_accounts_system_program() {
994 let payer = Keypair::new();
995 let tx = create_test_transaction(&payer.pubkey());
996
997 let policy = RelayerSolanaPolicy {
998 disallowed_accounts: Some(vec![program::id().to_string()]),
999 ..Default::default()
1000 };
1001
1002 let result = SolanaTransactionValidator::validate_tx_disallowed_accounts(&tx, &policy);
1003 assert!(matches!(
1004 result.unwrap_err(),
1005 SolanaTransactionValidationError::PolicyViolation(_)
1006 ));
1007 }
1008
1009 #[test]
1010 fn test_validate_data_size_within_limit() {
1011 let payer = Keypair::new();
1012 let tx = create_test_transaction(&payer.pubkey());
1013
1014 let policy = RelayerSolanaPolicy {
1015 max_tx_data_size: Some(1500),
1016 ..Default::default()
1017 };
1018
1019 let result = SolanaTransactionValidator::validate_data_size(&tx, &policy);
1020 assert!(result.is_ok());
1021 }
1022
1023 #[test]
1024 fn test_validate_data_size_exceeds_limit() {
1025 let payer = Keypair::new();
1026 let tx = create_test_transaction(&payer.pubkey());
1027
1028 let policy = RelayerSolanaPolicy {
1029 max_tx_data_size: Some(10),
1030 ..Default::default()
1031 };
1032
1033 let result = SolanaTransactionValidator::validate_data_size(&tx, &policy);
1034 assert!(matches!(
1035 result.unwrap_err(),
1036 SolanaTransactionValidationError::PolicyViolation(_)
1037 ));
1038 }
1039
1040 #[test]
1041 fn test_validate_data_size_large_instruction() {
1042 let payer = Keypair::new();
1043 let recipient = Pubkey::new_unique();
1044
1045 let large_data = vec![0u8; 1000];
1046 let ix = Instruction::new_with_bytes(
1047 program::id(),
1048 &large_data,
1049 vec![
1050 AccountMeta::new(payer.pubkey(), true),
1051 AccountMeta::new(recipient, false),
1052 ],
1053 );
1054
1055 let message = Message::new(&[ix], Some(&payer.pubkey()));
1056 let tx = Transaction::new_unsigned(message);
1057
1058 let policy = RelayerSolanaPolicy {
1059 max_tx_data_size: Some(500),
1060 ..Default::default()
1061 };
1062
1063 let result = SolanaTransactionValidator::validate_data_size(&tx, &policy);
1064 assert!(matches!(
1065 result.unwrap_err(),
1066 SolanaTransactionValidationError::PolicyViolation(_)
1067 ));
1068 }
1069
1070 #[test]
1071 fn test_validate_data_size_multiple_instructions() {
1072 let payer = Keypair::new();
1073 let recipient = Pubkey::new_unique();
1074
1075 let ix1 = instruction::transfer(&payer.pubkey(), &recipient, 1000);
1076 let ix2 = instruction::transfer(&payer.pubkey(), &recipient, 2000);
1077 let message = Message::new(&[ix1, ix2], Some(&payer.pubkey()));
1078 let tx = Transaction::new_unsigned(message);
1079
1080 let policy = RelayerSolanaPolicy {
1081 max_tx_data_size: Some(1500),
1082 ..Default::default()
1083 };
1084
1085 let result = SolanaTransactionValidator::validate_data_size(&tx, &policy);
1086 assert!(result.is_ok());
1087 }
1088
1089 #[tokio::test]
1090 async fn test_simulate_transaction_success() {
1091 let transaction = create_test_transaction(&Keypair::new().pubkey());
1092 let mut mock_provider = MockSolanaProviderTrait::new();
1093
1094 mock_provider
1095 .expect_simulate_transaction()
1096 .with(eq(transaction.clone()))
1097 .returning(move |_| {
1098 let simulation_result = RpcSimulateTransactionResult {
1099 err: None,
1100 logs: Some(vec!["Program log: success".to_string()]),
1101 accounts: None,
1102 units_consumed: Some(100000),
1103 return_data: None,
1104 inner_instructions: None,
1105 replacement_blockhash: None,
1106 loaded_accounts_data_size: None,
1107 };
1108 Box::pin(async { Ok(simulation_result) })
1109 });
1110
1111 let result =
1112 SolanaTransactionValidator::simulate_transaction(&transaction, &mock_provider).await;
1113
1114 assert!(result.is_ok());
1115 let simulation = result.unwrap();
1116 assert!(simulation.err.is_none());
1117 assert_eq!(simulation.units_consumed, Some(100000));
1118 }
1119
1120 #[tokio::test]
1121 async fn test_simulate_transaction_failure() {
1122 let transaction = create_test_transaction(&Keypair::new().pubkey());
1123 let mut mock_provider = MockSolanaProviderTrait::new();
1124
1125 mock_provider.expect_simulate_transaction().returning(|_| {
1126 Box::pin(async {
1127 Err(SolanaProviderError::RpcError(
1128 "Simulation failed".to_string(),
1129 ))
1130 })
1131 });
1132
1133 let result =
1134 SolanaTransactionValidator::simulate_transaction(&transaction, &mock_provider).await;
1135
1136 assert!(matches!(
1137 result.unwrap_err(),
1138 SolanaTransactionValidationError::SimulationError(_)
1139 ));
1140 }
1141
1142 #[tokio::test]
1143 async fn test_validate_token_transfers_success() {
1144 let (tx, policy, provider, ..) = setup_token_transfer_test(Some(100));
1145
1146 let result = SolanaTransactionValidator::validate_token_transfers(
1147 &tx,
1148 &policy,
1149 &provider,
1150 &Pubkey::new_unique(),
1151 )
1152 .await;
1153
1154 assert!(result.is_ok());
1155 }
1156
1157 #[tokio::test]
1158 async fn test_validate_token_transfers_insufficient_balance() {
1159 let (tx, policy, provider, ..) = setup_token_transfer_test(Some(2000));
1160
1161 let result = SolanaTransactionValidator::validate_token_transfers(
1162 &tx,
1163 &policy,
1164 &provider,
1165 &Pubkey::new_unique(),
1166 )
1167 .await;
1168
1169 match result {
1170 Err(SolanaTransactionValidationError::ValidationError(msg)) => {
1171 assert!(
1172 msg.contains("Insufficient balance for cumulative transfers: account "),
1173 "Unexpected error message: {}",
1174 msg
1175 );
1176 assert!(
1177 msg.contains("has balance 999 but requires 2000 across all instructions"),
1178 "Unexpected error message: {}",
1179 msg
1180 );
1181 }
1182 other => panic!(
1183 "Expected ValidationError for insufficient balance, got {:?}",
1184 other
1185 ),
1186 }
1187 }
1188
1189 #[tokio::test]
1190 async fn test_validate_token_transfers_relayer_max_fee() {
1191 let (tx, policy, provider, _owner, _mint, _source, destination) =
1192 setup_token_transfer_test(Some(500));
1193
1194 let result = SolanaTransactionValidator::validate_token_transfers(
1195 &tx,
1196 &policy,
1197 &provider,
1198 &destination,
1199 )
1200 .await;
1201
1202 match result {
1203 Err(SolanaTransactionValidationError::PolicyViolation(msg)) => {
1204 assert!(
1205 msg.contains("Transfer amount 500 exceeds max fee allowed 100"),
1206 "Unexpected error message: {}",
1207 msg
1208 );
1209 }
1210 other => panic!(
1211 "Expected ValidationError for insufficient balance, got {:?}",
1212 other
1213 ),
1214 }
1215 }
1216
1217 #[tokio::test]
1218 async fn test_validate_token_transfers_relayer_max_fee_not_applied_for_secondary_accounts() {
1219 let (tx, policy, provider, ..) = setup_token_transfer_test(Some(500));
1220
1221 let result = SolanaTransactionValidator::validate_token_transfers(
1222 &tx,
1223 &policy,
1224 &provider,
1225 &Pubkey::new_unique(),
1226 )
1227 .await;
1228
1229 assert!(result.is_ok());
1230 }
1231
1232 #[tokio::test]
1233 async fn test_validate_token_transfers_disallowed_token() {
1234 let (tx, mut policy, provider, ..) = setup_token_transfer_test(Some(100));
1235
1236 policy.allowed_tokens = Some(vec![SolanaAllowedTokensPolicy {
1237 mint: Pubkey::new_unique().to_string(), decimals: Some(9),
1239 symbol: Some("USDT".to_string()),
1240 max_allowed_fee: None,
1241 swap_config: Some(SolanaAllowedTokensSwapConfig {
1242 ..Default::default()
1243 }),
1244 }]);
1245
1246 let result = SolanaTransactionValidator::validate_token_transfers(
1247 &tx,
1248 &policy,
1249 &provider,
1250 &Pubkey::new_unique(),
1251 )
1252 .await;
1253
1254 match result {
1255 Err(SolanaTransactionValidationError::PolicyViolation(msg)) => {
1256 assert!(
1257 msg.contains("not allowed for transfers"),
1258 "Error message '{}' should contain 'not allowed for transfers'",
1259 msg
1260 );
1261 }
1262 other => panic!("Expected PolicyViolation error, got {:?}", other),
1263 }
1264 }
1265}