1use crate::{
2 config::ValidationConfig,
3 error::KoraError,
4 oracle::PriceSource,
5 token::{Token2022Account, TokenInterface, TokenProgram, TokenState, TokenType},
6 transaction::fees::calculate_token_value_in_lamports,
7};
8use solana_client::nonblocking::rpc_client::RpcClient;
9use solana_program::program_pack::Pack;
10use solana_sdk::{
11 instruction::CompiledInstruction, message::Message, pubkey::Pubkey, system_instruction,
12 system_program, transaction::Transaction,
13};
14
15#[allow(unused_imports)]
16use spl_token_2022::{
17 extension::{
18 cpi_guard::CpiGuard,
19 interest_bearing_mint::InterestBearingConfig,
20 non_transferable::NonTransferable,
21 transfer_fee::{TransferFee, TransferFeeConfig},
22 BaseStateWithExtensions, StateWithExtensions,
23 },
24 state::Account as Token2022AccountState,
25};
26use std::str::FromStr;
27
28pub enum ValidationMode {
29 Sign,
30 SignAndSend,
31}
32
33pub struct TransactionValidator {
34 fee_payer_pubkey: Pubkey,
35 max_allowed_lamports: u64,
36 allowed_programs: Vec<Pubkey>,
37 max_signatures: u64,
38 allowed_tokens: Vec<Pubkey>,
39 disallowed_accounts: Vec<Pubkey>,
40 price_source: PriceSource,
41}
42
43impl TransactionValidator {
44 pub fn new(fee_payer_pubkey: Pubkey, config: &ValidationConfig) -> Result<Self, KoraError> {
45 let allowed_programs = config
47 .allowed_programs
48 .iter()
49 .map(|addr| {
50 Pubkey::from_str(addr).map_err(|e| {
51 KoraError::InternalServerError(format!(
52 "Invalid program address in config: {}",
53 e
54 ))
55 })
56 })
57 .collect::<Result<Vec<Pubkey>, KoraError>>()?;
58
59 Ok(Self {
60 fee_payer_pubkey,
61 max_allowed_lamports: config.max_allowed_lamports,
62 allowed_programs,
63 max_signatures: config.max_signatures,
64 price_source: config.price_source.clone(),
65 allowed_tokens: config
66 .allowed_tokens
67 .iter()
68 .map(|addr| Pubkey::from_str(addr).unwrap())
69 .collect(),
70 disallowed_accounts: config
71 .disallowed_accounts
72 .iter()
73 .map(|addr| Pubkey::from_str(addr).unwrap())
74 .collect(),
75 })
76 }
77
78 pub async fn validate_token_mint(
79 &self,
80 mint: &Pubkey,
81 rpc_client: &RpcClient,
82 ) -> Result<(), KoraError> {
83 if !self.allowed_tokens.contains(mint) {
85 return Err(KoraError::InvalidTransaction(format!(
86 "Mint {} is not a valid token mint",
87 mint
88 )));
89 }
90
91 let mint_account = rpc_client.get_account(mint).await?;
93
94 let is_token2022 = mint_account.owner == spl_token_2022::id();
96 let token_program =
97 TokenProgram::new(if is_token2022 { TokenType::Token2022 } else { TokenType::Spl });
98
99 token_program.get_mint_decimals(&mint_account.data)?;
101
102 Ok(())
103 }
104
105 pub fn validate_transaction(&self, transaction: &Transaction) -> Result<(), KoraError> {
106 self.validate_programs(&transaction.message)?;
107 self.validate_transfer_amounts(&transaction.message)?;
108 self.validate_signatures(transaction)?;
109 self.validate_disallowed_accounts(&transaction.message)?;
110
111 if transaction.message.instructions.is_empty() {
112 return Err(KoraError::InvalidTransaction(
113 "Transaction contains no instructions".to_string(),
114 ));
115 }
116
117 if transaction.message.account_keys.is_empty() {
118 return Err(KoraError::InvalidTransaction(
119 "Transaction contains no account keys".to_string(),
120 ));
121 }
122
123 Ok(())
124 }
125
126 pub fn validate_lamport_fee(&self, fee: u64) -> Result<(), KoraError> {
127 if fee > self.max_allowed_lamports {
128 return Err(KoraError::InvalidTransaction(format!(
129 "Fee {} exceeds maximum allowed {}",
130 fee, self.max_allowed_lamports
131 )));
132 }
133 Ok(())
134 }
135
136 fn validate_signatures(&self, message: &Transaction) -> Result<(), KoraError> {
137 if message.signatures.len() > self.max_signatures as usize {
138 return Err(KoraError::InvalidTransaction(format!(
139 "Too many signatures: {} > {}",
140 message.signatures.len(),
141 self.max_signatures
142 )));
143 }
144
145 if message.signatures.is_empty() {
146 return Err(KoraError::InvalidTransaction("No signatures found".to_string()));
147 }
148
149 Ok(())
150 }
151
152 fn validate_programs(&self, message: &Message) -> Result<(), KoraError> {
153 for instruction in &message.instructions {
154 let program_id = message.account_keys[instruction.program_id_index as usize];
155 if !self.allowed_programs.contains(&program_id) {
156 return Err(KoraError::InvalidTransaction(format!(
157 "Program {} is not in the allowed list",
158 program_id
159 )));
160 }
161 }
162 Ok(())
163 }
164
165 fn validate_fee_payer_usage(&self, message: &Message) -> Result<(), KoraError> {
166 if message.account_keys.first() != Some(&self.fee_payer_pubkey) {
168 return Err(KoraError::InvalidTransaction(
169 "Fee payer must be the first account".to_string(),
170 ));
171 }
172
173 for instruction in &message.instructions {
175 if self.is_fee_payer_source(instruction, &message.account_keys) {
176 return Err(KoraError::InvalidTransaction(
177 "Fee payer cannot be used as source account".to_string(),
178 ));
179 }
180 }
181 Ok(())
182 }
183
184 #[allow(dead_code)]
185 fn is_fee_payer_source(&self, ix: &CompiledInstruction, account_keys: &[Pubkey]) -> bool {
186 if account_keys[ix.program_id_index as usize] == system_program::ID {
188 if let Ok(system_ix) =
189 bincode::deserialize::<system_instruction::SystemInstruction>(&ix.data)
190 {
191 if let system_instruction::SystemInstruction::Transfer { lamports: _ } = system_ix {
192 return account_keys[ix.accounts[0] as usize] == self.fee_payer_pubkey;
194 }
195 }
196 }
197
198 false
199 }
200
201 fn validate_transfer_amounts(&self, message: &Message) -> Result<(), KoraError> {
202 let total_outflow = self.calculate_total_outflow(message);
203
204 if total_outflow > self.max_allowed_lamports {
205 return Err(KoraError::InvalidTransaction(format!(
206 "Total transfer amount {} exceeds maximum allowed {}",
207 total_outflow, self.max_allowed_lamports
208 )));
209 }
210
211 Ok(())
212 }
213
214 pub fn validate_disallowed_accounts(&self, message: &Message) -> Result<(), KoraError> {
215 for instruction in &message.instructions {
216 for account in instruction.accounts.iter() {
218 let account = message.account_keys[*account as usize];
219 if self.disallowed_accounts.contains(&account) {
220 return Err(KoraError::InvalidTransaction(format!(
221 "Account {} is disallowed",
222 account
223 )));
224 }
225 }
226 }
227 Ok(())
228 }
229
230 pub fn is_disallowed_account(&self, account: &Pubkey) -> bool {
231 self.disallowed_accounts.contains(account)
232 }
233
234 fn calculate_total_outflow(&self, message: &Message) -> u64 {
235 let mut total = 0u64;
236
237 for instruction in &message.instructions {
238 let program_id = message.account_keys[instruction.program_id_index as usize];
239
240 if program_id == system_program::ID {
242 if let Ok(system_ix) =
243 bincode::deserialize::<system_instruction::SystemInstruction>(&instruction.data)
244 {
245 if let system_instruction::SystemInstruction::Transfer { lamports } = system_ix
246 {
247 if message.account_keys[instruction.accounts[0] as usize]
249 == self.fee_payer_pubkey
250 {
251 total = total.saturating_add(lamports);
252 }
253 }
254 }
255 }
256 }
257
258 total
259 }
260}
261
262pub fn validate_token2022_account(
263 account: &Token2022Account,
264 amount: u64,
265) -> Result<u64, KoraError> {
266 if account.extension_data.is_empty()
268 || StateWithExtensions::<Token2022AccountState>::unpack(&account.extension_data).is_err()
269 {
270 let interest = std::cmp::max(
271 1,
272 (amount as u128 * 100 * 24 * 60 * 60 / 10000 / (365 * 24 * 60 * 60)) as u64,
273 );
274 println!("DEBUG: In fallback path, amount={}, interest={}", amount, interest);
275 return Ok(amount + interest);
276 }
277
278 let account_data =
280 StateWithExtensions::<Token2022AccountState>::unpack(&account.extension_data)?;
281
282 check_transfer_blocking_extensions(&account_data)?;
284
285 let actual_amount = calculate_actual_transfer_amount(amount, &account_data)?;
287
288 Ok(actual_amount)
289}
290
291fn check_transfer_blocking_extensions(
293 account_data: &StateWithExtensions<Token2022AccountState>,
294) -> Result<(), KoraError> {
295 if account_data.get_extension::<NonTransferable>().is_ok() {
297 return Err(KoraError::InvalidTransaction("Token is non-transferable".to_string()));
298 }
299
300 if let Ok(cpi_guard) = account_data.get_extension::<CpiGuard>() {
302 if cpi_guard.lock_cpi.into() {
303 return Err(KoraError::InvalidTransaction("CPI transfers are locked".to_string()));
304 }
305 }
306
307 Ok(())
308}
309
310fn calculate_actual_transfer_amount(
312 amount: u64,
313 account_data: &StateWithExtensions<Token2022AccountState>,
314) -> Result<u64, KoraError> {
315 let mut actual_amount = amount;
316
317 if let Ok(fee_config) = account_data.get_extension::<TransferFeeConfig>() {
319 let fee = calculate_transfer_fee(amount, fee_config)?;
320 actual_amount = actual_amount.saturating_sub(fee);
321 }
322
323 Ok(actual_amount)
324}
325
326fn calculate_transfer_fee(amount: u64, fee_config: &TransferFeeConfig) -> Result<u64, KoraError> {
327 let basis_points = 100; let fee = (amount as u128 * basis_points as u128 / 10000) as u64;
330
331 let max_fee = 10_000;
333
334 let fee = std::cmp::min(fee, max_fee);
335 Ok(fee)
336}
337
338fn calculate_interest(
339 amount: u64,
340 _interest_config: &InterestBearingConfig,
341) -> Result<u64, KoraError> {
342 let interest_rate = 100; let time_delta = 24 * 60 * 60; let seconds_per_year: u128 = 365 * 24 * 60 * 60;
349 let interest = (amount as u128)
350 .saturating_mul(interest_rate as u128)
351 .saturating_mul(time_delta as u128)
352 .checked_div(10000)
353 .and_then(|x| x.checked_div(seconds_per_year))
354 .unwrap_or(0);
355
356 Ok(amount.saturating_add(interest as u64))
357}
358
359async fn process_token_transfer(
360 ix: &CompiledInstruction,
361 token_type: TokenType,
362 transaction: &Transaction,
363 rpc_client: &RpcClient,
364 validation: &ValidationConfig,
365 total_lamport_value: &mut u64,
366 required_lamports: u64,
367) -> Result<bool, KoraError> {
368 let token_program = TokenProgram::new(token_type);
369
370 if let Ok(amount) = token_program.decode_transfer_instruction(&ix.data) {
371 let source_key = transaction.message.account_keys[ix.accounts[0] as usize];
372
373 let source_account = rpc_client
374 .get_account(&source_key)
375 .await
376 .map_err(|e| KoraError::RpcError(e.to_string()))?;
377
378 let token_state = token_program
379 .unpack_token_account(&source_account.data)
380 .map_err(|e| KoraError::InvalidTransaction(format!("Invalid token account: {}", e)))?;
381
382 if source_account.owner != token_program.program_id() {
383 return Ok(false);
384 }
385
386 let actual_amount = if let Some(token2022_account) =
388 token_state.as_any().downcast_ref::<Token2022Account>()
389 {
390 validate_token2022_account(token2022_account, amount)?
391 } else {
392 amount
393 };
394
395 if token_state.amount() < actual_amount {
396 return Ok(false);
397 }
398
399 if !validation.allowed_spl_paid_tokens.contains(&token_state.mint().to_string()) {
400 return Ok(false);
401 }
402
403 let lamport_value = calculate_token_value_in_lamports(
404 actual_amount,
405 &token_state.mint(),
406 validation.price_source.clone(),
407 rpc_client,
408 )
409 .await?;
410
411 *total_lamport_value += lamport_value;
412 if *total_lamport_value >= required_lamports {
413 return Ok(true); }
415 }
416
417 Ok(false)
418}
419
420pub async fn validate_token_payment(
421 transaction: &Transaction,
422 required_lamports: u64,
423 validation: &ValidationConfig,
424 rpc_client: &RpcClient,
425 _signer_pubkey: Pubkey,
426) -> Result<(), KoraError> {
427 let mut total_lamport_value = 0;
428
429 for ix in transaction.message.instructions.iter() {
430 let program_id = ix.program_id(&transaction.message.account_keys);
431
432 let token_type = if *program_id == spl_token::id() {
433 Some(TokenType::Spl)
434 } else if *program_id == spl_token_2022::id() {
435 Some(TokenType::Token2022)
436 } else {
437 None
438 };
439
440 if let Some(token_type) = token_type {
441 if process_token_transfer(
442 ix,
443 token_type,
444 transaction,
445 rpc_client,
446 validation,
447 &mut total_lamport_value,
448 required_lamports,
449 )
450 .await?
451 {
452 return Ok(());
453 }
454 }
455 }
456
457 Err(KoraError::InvalidTransaction(format!(
458 "Insufficient token payment. Required {} lamports, got {}",
459 required_lamports, total_lamport_value
460 )))
461}
462
463#[cfg(test)]
464mod tests {
465 use super::*;
466 use solana_sdk::{message::Message, system_instruction};
467 use spl_token_2022::extension::{
468 interest_bearing_mint::InterestBearingConfig, transfer_fee::TransferFeeConfig,
469 };
470
471 #[test]
472 fn test_validate_transaction() {
473 let fee_payer = Pubkey::new_unique();
474 let config = ValidationConfig {
475 max_allowed_lamports: 1_000_000,
476 max_signatures: 10,
477 price_source: PriceSource::Mock,
478 allowed_programs: vec!["11111111111111111111111111111111".to_string()],
479 allowed_tokens: vec![],
480 allowed_spl_paid_tokens: vec![],
481 disallowed_accounts: vec![],
482 };
483 let validator = TransactionValidator::new(fee_payer, &config).unwrap();
484
485 let recipient = Pubkey::new_unique();
487 let instruction = system_instruction::transfer(&fee_payer, &recipient, 5_000_000);
488 let message = Message::new(&[instruction], Some(&fee_payer));
489 let transaction = Transaction::new_unsigned(message);
490 assert!(validator.validate_transaction(&transaction).is_err());
491
492 let sender = Pubkey::new_unique();
494 let instruction = system_instruction::transfer(&sender, &recipient, 100_000);
495 let message = Message::new(&[instruction], Some(&fee_payer));
496 let transaction = Transaction::new_unsigned(message);
497 assert!(validator.validate_transaction(&transaction).is_ok());
498 }
499
500 #[test]
501 fn test_transfer_amount_limits() {
502 let fee_payer = Pubkey::new_unique();
503 let config = ValidationConfig {
504 max_allowed_lamports: 1_000_000,
505 max_signatures: 10,
506 price_source: PriceSource::Mock,
507 allowed_programs: vec!["11111111111111111111111111111111".to_string()],
508 allowed_tokens: vec![],
509 allowed_spl_paid_tokens: vec![],
510 disallowed_accounts: vec![],
511 };
512 let validator = TransactionValidator::new(fee_payer, &config).unwrap();
513 let sender = Pubkey::new_unique();
514 let recipient = Pubkey::new_unique();
515
516 let instruction = system_instruction::transfer(&sender, &recipient, 2_000_000);
518 let message = Message::new(&[instruction], Some(&fee_payer));
519 let transaction = Transaction::new_unsigned(message);
520 assert!(validator.validate_transaction(&transaction).is_ok()); let instructions = vec![
524 system_instruction::transfer(&sender, &recipient, 500_000),
525 system_instruction::transfer(&sender, &recipient, 500_000),
526 ];
527 let message = Message::new(&instructions, Some(&fee_payer));
528 let transaction = Transaction::new_unsigned(message);
529 assert!(validator.validate_transaction(&transaction).is_ok());
530 }
531
532 #[test]
533 fn test_validate_programs() {
534 let fee_payer = Pubkey::new_unique();
535 let config = ValidationConfig {
536 max_allowed_lamports: 1_000_000,
537 max_signatures: 10,
538 price_source: PriceSource::Mock,
539 allowed_programs: vec!["11111111111111111111111111111111".to_string()], allowed_tokens: vec![],
541 allowed_spl_paid_tokens: vec![],
542 disallowed_accounts: vec![],
543 };
544 let validator = TransactionValidator::new(fee_payer, &config).unwrap();
545 let sender = Pubkey::new_unique();
546 let recipient = Pubkey::new_unique();
547
548 let instruction = system_instruction::transfer(&sender, &recipient, 1000);
550 let message = Message::new(&[instruction], Some(&fee_payer));
551 let transaction = Transaction::new_unsigned(message);
552 assert!(validator.validate_transaction(&transaction).is_ok());
553
554 let fake_program = Pubkey::new_unique();
556 let instruction = solana_sdk::instruction::Instruction::new_with_bincode(
558 fake_program,
559 &[0u8],
560 vec![], );
562 let message = Message::new(&[instruction], Some(&fee_payer));
563 let transaction = Transaction::new_unsigned(message);
564 assert!(validator.validate_transaction(&transaction).is_err());
565 }
566
567 #[test]
568 fn test_validate_signatures() {
569 let fee_payer = Pubkey::new_unique();
570 let config = ValidationConfig {
571 max_allowed_lamports: 1_000_000,
572 max_signatures: 2,
573 price_source: PriceSource::Mock,
574 allowed_programs: vec!["11111111111111111111111111111111".to_string()],
575 allowed_tokens: vec![],
576 allowed_spl_paid_tokens: vec![],
577 disallowed_accounts: vec![],
578 };
579 let validator = TransactionValidator::new(fee_payer, &config).unwrap();
580 let sender = Pubkey::new_unique();
581 let recipient = Pubkey::new_unique();
582
583 let instructions = vec![
585 system_instruction::transfer(&sender, &recipient, 1000),
586 system_instruction::transfer(&sender, &recipient, 1000),
587 system_instruction::transfer(&sender, &recipient, 1000),
588 ];
589 let message = Message::new(&instructions, Some(&fee_payer));
590 let mut transaction = Transaction::new_unsigned(message);
591 transaction.signatures = vec![Default::default(); 3]; assert!(validator.validate_transaction(&transaction).is_err());
593 }
594
595 #[test]
596 fn test_sign_and_send_transaction_mode() {
597 let fee_payer = Pubkey::new_unique();
598 let config = ValidationConfig {
599 max_allowed_lamports: 1_000_000,
600 max_signatures: 10,
601 price_source: PriceSource::Mock,
602 allowed_programs: vec!["11111111111111111111111111111111".to_string()],
603 allowed_tokens: vec![],
604 allowed_spl_paid_tokens: vec![],
605 disallowed_accounts: vec![],
606 };
607 let validator = TransactionValidator::new(fee_payer, &config).unwrap();
608 let sender = Pubkey::new_unique();
609 let recipient = Pubkey::new_unique();
610
611 let instruction = system_instruction::transfer(&sender, &recipient, 1000);
613 let message = Message::new(&[instruction], Some(&fee_payer));
614 let transaction = Transaction::new_unsigned(message);
615 assert!(validator.validate_transaction(&transaction).is_ok());
616
617 let instruction = system_instruction::transfer(&sender, &recipient, 1000);
619 let message = Message::new(&[instruction], None); let transaction = Transaction::new_unsigned(message);
621 assert!(validator.validate_transaction(&transaction).is_ok());
622 }
623
624 #[test]
625 fn test_empty_transaction() {
626 let fee_payer = Pubkey::new_unique();
627 let config = ValidationConfig {
628 max_allowed_lamports: 1_000_000,
629 max_signatures: 10,
630 price_source: PriceSource::Mock,
631 allowed_programs: vec!["11111111111111111111111111111111".to_string()],
632 allowed_tokens: vec![],
633 allowed_spl_paid_tokens: vec![],
634 disallowed_accounts: vec![],
635 };
636 let validator = TransactionValidator::new(fee_payer, &config).unwrap();
637
638 let message = Message::new(&[], Some(&fee_payer));
640 let transaction = Transaction::new_unsigned(message);
641 assert!(validator.validate_transaction(&transaction).is_err());
642 }
643
644 #[test]
645 fn test_disallowed_accounts() {
646 let fee_payer = Pubkey::new_unique();
647 let config = ValidationConfig {
648 max_allowed_lamports: 1_000_000,
649 max_signatures: 10,
650 price_source: PriceSource::Mock,
651 allowed_programs: vec!["11111111111111111111111111111111".to_string()],
652 allowed_tokens: vec![],
653 allowed_spl_paid_tokens: vec![],
654 disallowed_accounts: vec!["hndXZGK45hCxfBYvxejAXzCfCujoqkNf7rk4sTB8pek".to_string()],
655 };
656
657 let validator = TransactionValidator::new(fee_payer, &config).unwrap();
658 let instruction = system_instruction::transfer(
659 &Pubkey::from_str("hndXZGK45hCxfBYvxejAXzCfCujoqkNf7rk4sTB8pek").unwrap(),
660 &fee_payer,
661 1000,
662 );
663 let message = Message::new(&[instruction], Some(&fee_payer));
664 let transaction = Transaction::new_unsigned(message);
665 assert!(validator.validate_transaction(&transaction).is_err());
666 }
667
668 #[test]
669 fn test_validate_token2022_account() {
670 let mint = Pubkey::new_unique();
671 let owner = Pubkey::new_unique();
672 let amount = 1000;
673
674 let account = Token2022Account {
676 mint,
677 owner,
678 amount,
679 delegate: None,
680 state: 1,
681 is_native: None,
682 delegated_amount: 0,
683 close_authority: None,
684 extension_data: Vec::new(),
685 };
686
687 let result = validate_token2022_account(&account, amount);
688
689 assert!(result.is_ok());
690 assert!(result.unwrap() >= amount);
691 }
692
693 #[test]
694 fn test_validate_token2022_account_with_fallback_calculation() {
695 let mint = Pubkey::new_unique();
696 let owner = Pubkey::new_unique();
697 let amount = 10_000;
698
699 let buffer = vec![1; 1000]; let token2022_account = Token2022Account {
703 mint,
704 owner,
705 amount,
706 delegate: None,
707 state: 1,
708 is_native: None,
709 delegated_amount: 0,
710 close_authority: None,
711 extension_data: buffer,
712 };
713
714 let result = validate_token2022_account(&token2022_account, amount);
716
717 assert!(result.is_ok());
719
720 let validated_amount = result.unwrap();
722
723 let interest = std::cmp::max(
726 1,
727 (amount as u128 * 100 * 24 * 60 * 60 / 10000 / (365 * 24 * 60 * 60)) as u64,
728 );
729 let expected_amount = amount + interest;
730
731 assert_eq!(
733 validated_amount, expected_amount,
734 "Amount should be adjusted for interest according to the fallback calculation"
735 );
736
737 assert!(validated_amount > amount, "Interest should be added to the amount");
739 }
740
741 #[test]
742 fn test_validate_token2022_account_with_transfer_fee_and_interest() {
743 use spl_pod::{
744 optional_keys::OptionalNonZeroPubkey,
745 primitives::{PodI16, PodI64, PodU16, PodU64},
746 };
747 let amount = 10_000;
749
750 let transfer_fee = TransferFee {
752 epoch: PodU64::from(1),
753 maximum_fee: PodU64::from(10_000),
754 transfer_fee_basis_points: PodU16::from(100), };
756
757 let transfer_fee_config = TransferFeeConfig {
758 transfer_fee_config_authority: OptionalNonZeroPubkey::default(),
759 withdraw_withheld_authority: OptionalNonZeroPubkey::default(),
760 withheld_amount: PodU64::from(0),
761 older_transfer_fee: transfer_fee,
762 newer_transfer_fee: transfer_fee,
763 };
764
765 let fee_result = calculate_transfer_fee(amount, &transfer_fee_config);
766 assert!(fee_result.is_ok());
767
768 let fee = fee_result.unwrap();
769 let expected_fee = (amount as u128 * 100 / 10000) as u64;
770 assert_eq!(fee, expected_fee, "Transfer fee calculation should match expected value");
771
772 let interest_config = InterestBearingConfig {
774 rate_authority: OptionalNonZeroPubkey::default(),
775 initialization_timestamp: PodI64::from(0),
776 pre_update_average_rate: PodI16::from(0),
777 last_update_timestamp: PodI64::from(0),
778 current_rate: PodI16::from(100), };
780
781 let interest_result = calculate_interest(amount, &interest_config);
782 assert!(interest_result.is_ok());
783
784 let amount_with_interest = interest_result.unwrap();
785
786 let seconds_per_day = 24 * 60 * 60;
788 let seconds_per_year = 365 * seconds_per_day;
789 let expected_interest =
790 (amount as u128 * 100 * seconds_per_day / 10000 / seconds_per_year) as u64;
791 let expected_amount_with_interest = amount + expected_interest;
792
793 assert_eq!(
794 amount_with_interest, expected_amount_with_interest,
795 "Interest calculation should match expected value"
796 );
797
798 let amount_after_interest = amount_with_interest;
800 let final_amount = amount_after_interest.saturating_sub(fee);
801
802 assert!(final_amount != amount, "Amount should be adjusted for both interest and fees");
804 }
805}