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