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