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