1use std::str::FromStr;
2
3use crate::{
4 constant::{ESTIMATED_LAMPORTS_FOR_PAYMENT_INSTRUCTION, LAMPORTS_PER_SIGNATURE},
5 error::KoraError,
6 fee::price::PriceModel,
7 oracle::PriceSource,
8 token::{
9 spl_token_2022::Token2022Mint,
10 token::{TokenType, TokenUtil},
11 TokenState,
12 },
13 transaction::{
14 ParsedSPLInstructionData, ParsedSPLInstructionType, ParsedSystemInstructionData,
15 ParsedSystemInstructionType, VersionedTransactionResolved,
16 },
17};
18
19#[cfg(not(test))]
20use {crate::cache::CacheUtil, crate::state::get_config};
21
22#[cfg(test)]
23use crate::tests::{cache_mock::MockCacheUtil as CacheUtil, config_mock::mock_state::get_config};
24use solana_client::nonblocking::rpc_client::RpcClient;
25use solana_message::VersionedMessage;
26use solana_sdk::pubkey::Pubkey;
27
28#[derive(Debug, Clone)]
29pub struct TotalFeeCalculation {
30 pub total_fee_lamports: u64,
31 pub base_fee: u64,
32 pub kora_signature_fee: u64,
33 pub fee_payer_outflow: u64,
34 pub payment_instruction_fee: u64,
35 pub transfer_fee_amount: u64,
36}
37
38impl TotalFeeCalculation {
39 pub fn new(
40 total_fee_lamports: u64,
41 base_fee: u64,
42 kora_signature_fee: u64,
43 fee_payer_outflow: u64,
44 payment_instruction_fee: u64,
45 transfer_fee_amount: u64,
46 ) -> Self {
47 Self {
48 total_fee_lamports,
49 base_fee,
50 kora_signature_fee,
51 fee_payer_outflow,
52 payment_instruction_fee,
53 transfer_fee_amount,
54 }
55 }
56
57 pub fn new_fixed(total_fee_lamports: u64) -> Self {
58 Self {
59 total_fee_lamports,
60 base_fee: 0,
61 kora_signature_fee: 0,
62 fee_payer_outflow: 0,
63 payment_instruction_fee: 0,
64 transfer_fee_amount: 0,
65 }
66 }
67
68 pub fn get_total_fee_lamports(&self) -> Result<u64, KoraError> {
69 self.base_fee
70 .checked_add(self.kora_signature_fee)
71 .and_then(|sum| sum.checked_add(self.fee_payer_outflow))
72 .and_then(|sum| sum.checked_add(self.payment_instruction_fee))
73 .and_then(|sum| sum.checked_add(self.transfer_fee_amount))
74 .ok_or_else(|| {
75 log::error!("Fee calculation overflow: base_fee={}, kora_signature_fee={}, fee_payer_outflow={}, payment_instruction_fee={}, transfer_fee_amount={}",
76 self.base_fee, self.kora_signature_fee, self.fee_payer_outflow, self.payment_instruction_fee, self.transfer_fee_amount);
77 KoraError::ValidationError("Fee calculation overflow".to_string())
78 })
79 }
80}
81
82pub struct FeeConfigUtil {}
83
84impl FeeConfigUtil {
85 fn is_fee_payer_in_signers(
86 transaction: &VersionedTransactionResolved,
87 fee_payer: &Pubkey,
88 ) -> Result<bool, KoraError> {
89 let all_account_keys = &transaction.all_account_keys;
90 let transaction_inner = &transaction.transaction;
91
92 Ok(match &transaction_inner.message {
94 VersionedMessage::Legacy(legacy_message) => {
95 let num_signers = legacy_message.header.num_required_signatures as usize;
96 all_account_keys.iter().take(num_signers).any(|key| *key == *fee_payer)
97 }
98 VersionedMessage::V0(v0_message) => {
99 let num_signers = v0_message.header.num_required_signatures as usize;
100 all_account_keys.iter().take(num_signers).any(|key| *key == *fee_payer)
101 }
102 })
103 }
104
105 async fn get_payment_instruction_info(
108 rpc_client: &RpcClient,
109 destination_address: &Pubkey,
110 payment_destination: &Pubkey,
111 skip_missing_accounts: bool,
112 ) -> Result<Option<Box<dyn TokenState + Send + Sync>>, KoraError> {
113 let destination_account =
115 match CacheUtil::get_account(rpc_client, destination_address, false).await {
116 Ok(account) => account,
117 Err(_) if skip_missing_accounts => {
118 return Ok(None);
119 }
120 Err(e) => {
121 return Err(e);
122 }
123 };
124
125 let token_program = TokenType::get_token_program_from_owner(&destination_account.owner)?;
126 let token_account = token_program.unpack_token_account(&destination_account.data)?;
127
128 if token_account.owner() == *payment_destination {
130 Ok(Some(token_account))
131 } else {
132 Ok(None)
133 }
134 }
135
136 async fn analyze_payment_instructions(
139 resolved_transaction: &mut VersionedTransactionResolved,
140 rpc_client: &RpcClient,
141 fee_payer: &Pubkey,
142 ) -> Result<(bool, u64), KoraError> {
143 let config = get_config()?;
144 let payment_destination = config.kora.get_payment_address(fee_payer)?;
145 let mut has_payment = false;
146 let mut total_transfer_fees = 0u64;
147
148 let parsed_spl_instructions = resolved_transaction.get_or_parse_spl_instructions()?;
149
150 for instruction in parsed_spl_instructions
151 .get(&ParsedSPLInstructionType::SplTokenTransfer)
152 .unwrap_or(&vec![])
153 {
154 if let ParsedSPLInstructionData::SplTokenTransfer {
155 mint,
156 amount,
157 is_2022,
158 destination_address,
159 ..
160 } = instruction
161 {
162 let payment_info = Self::get_payment_instruction_info(
164 rpc_client,
165 destination_address,
166 &payment_destination,
167 true, )
169 .await?;
170
171 if payment_info.is_some() {
172 has_payment = true;
173
174 if *is_2022 {
176 if let Some(mint_pubkey) = mint {
177 let mint_account =
178 CacheUtil::get_account(rpc_client, mint_pubkey, true).await?;
179
180 let token_program =
181 TokenType::get_token_program_from_owner(&mint_account.owner)?;
182 let mint_state =
183 token_program.unpack_mint(mint_pubkey, &mint_account.data)?;
184
185 if let Some(token2022_mint) =
186 mint_state.as_any().downcast_ref::<Token2022Mint>()
187 {
188 let current_epoch = rpc_client.get_epoch_info().await?.epoch;
189
190 if let Some(fee_amount) =
191 token2022_mint.calculate_transfer_fee(*amount, current_epoch)?
192 {
193 total_transfer_fees = total_transfer_fees
194 .checked_add(fee_amount)
195 .ok_or_else(|| {
196 log::error!(
197 "Transfer fee accumulation overflow: total={}, new_fee={}",
198 total_transfer_fees,
199 fee_amount
200 );
201 KoraError::ValidationError(
202 "Transfer fee accumulation overflow".to_string(),
203 )
204 })?;
205 }
206 }
207 }
208 }
209 }
210 }
211 }
212
213 Ok((has_payment, total_transfer_fees))
214 }
215
216 async fn estimate_transaction_fee(
217 rpc_client: &RpcClient,
218 transaction: &mut VersionedTransactionResolved,
219 fee_payer: &Pubkey,
220 is_payment_required: bool,
221 ) -> Result<TotalFeeCalculation, KoraError> {
222 let base_fee =
224 TransactionFeeUtil::get_estimate_fee_resolved(rpc_client, transaction).await?;
225
226 let mut kora_signature_fee = 0u64;
231 if !FeeConfigUtil::is_fee_payer_in_signers(transaction, fee_payer)? {
232 kora_signature_fee = LAMPORTS_PER_SIGNATURE;
233 }
234
235 let config = get_config()?;
237 let fee_payer_outflow = FeeConfigUtil::calculate_fee_payer_outflow(
238 fee_payer,
239 transaction,
240 rpc_client,
241 &config.validation.price_source,
242 )
243 .await?;
244
245 let (has_payment, transfer_fee_config_amount) =
247 FeeConfigUtil::analyze_payment_instructions(transaction, rpc_client, fee_payer).await?;
248
249 let fee_for_payment_instruction = if is_payment_required && !has_payment {
251 ESTIMATED_LAMPORTS_FOR_PAYMENT_INSTRUCTION
252 } else {
253 0
254 };
255
256 let total_fee_lamports = base_fee
257 .checked_add(kora_signature_fee)
258 .and_then(|sum| sum.checked_add(fee_payer_outflow))
259 .and_then(|sum| sum.checked_add(fee_for_payment_instruction))
260 .and_then(|sum| sum.checked_add(transfer_fee_config_amount))
261 .ok_or_else(|| {
262 log::error!("Fee calculation overflow: base_fee={}, kora_signature_fee={}, fee_payer_outflow={}, payment_instruction_fee={}, transfer_fee_amount={}",
263 base_fee, kora_signature_fee, fee_payer_outflow, fee_for_payment_instruction, transfer_fee_config_amount);
264 KoraError::ValidationError("Fee calculation overflow".to_string())
265 })?;
266
267 Ok(TotalFeeCalculation {
268 total_fee_lamports,
269 base_fee,
270 kora_signature_fee,
271 fee_payer_outflow,
272 payment_instruction_fee: fee_for_payment_instruction,
273 transfer_fee_amount: transfer_fee_config_amount,
274 })
275 }
276
277 pub async fn estimate_kora_fee(
279 rpc_client: &RpcClient,
280 transaction: &mut VersionedTransactionResolved,
281 fee_payer: &Pubkey,
282 is_payment_required: bool,
283 price_source: PriceSource,
284 ) -> Result<TotalFeeCalculation, KoraError> {
285 let config = get_config()?;
286
287 match &config.validation.price.model {
288 PriceModel::Free => Ok(TotalFeeCalculation::new_fixed(0)),
289 PriceModel::Fixed { strict, .. } => {
290 let fixed_fee_lamports = config
291 .validation
292 .price
293 .get_required_lamports_with_fixed(rpc_client, price_source)
294 .await?;
295
296 if *strict {
297 let fee_calculation = Self::estimate_transaction_fee(
298 rpc_client,
299 transaction,
300 fee_payer,
301 is_payment_required,
302 )
303 .await?;
304
305 Ok(TotalFeeCalculation::new(
306 fixed_fee_lamports,
307 fee_calculation.base_fee,
308 fee_calculation.kora_signature_fee,
309 fee_calculation.fee_payer_outflow,
310 fee_calculation.payment_instruction_fee,
311 fee_calculation.transfer_fee_amount,
312 ))
313 } else {
314 Ok(TotalFeeCalculation::new_fixed(fixed_fee_lamports))
315 }
316 }
317 PriceModel::Margin { .. } => {
318 let fee_calculation = Self::estimate_transaction_fee(
320 rpc_client,
321 transaction,
322 fee_payer,
323 is_payment_required,
324 )
325 .await?;
326
327 let total_fee_lamports = config
328 .validation
329 .price
330 .get_required_lamports_with_margin(fee_calculation.total_fee_lamports)
331 .await?;
332
333 Ok(TotalFeeCalculation::new(
334 total_fee_lamports,
335 fee_calculation.base_fee,
336 fee_calculation.kora_signature_fee,
337 fee_calculation.fee_payer_outflow,
338 fee_calculation.payment_instruction_fee,
339 fee_calculation.transfer_fee_amount,
340 ))
341 }
342 }
343 }
344
345 pub async fn calculate_fee_in_token(
347 rpc_client: &RpcClient,
348 fee_in_lamports: u64,
349 fee_token: Option<&str>,
350 ) -> Result<Option<u64>, KoraError> {
351 if let Some(fee_token) = fee_token {
352 let token_mint = Pubkey::from_str(fee_token).map_err(|_| {
353 KoraError::InvalidTransaction("Invalid fee token mint address".to_string())
354 })?;
355
356 let config = get_config()?;
357 let validation_config = &config.validation;
358
359 if !validation_config.supports_token(fee_token) {
360 return Err(KoraError::InvalidRequest(format!(
361 "Token {fee_token} is not supported"
362 )));
363 }
364
365 let fee_value_in_token = TokenUtil::calculate_lamports_value_in_token(
366 fee_in_lamports,
367 &token_mint,
368 &validation_config.price_source,
369 rpc_client,
370 )
371 .await?;
372
373 Ok(Some(fee_value_in_token))
374 } else {
375 Ok(None)
376 }
377 }
378
379 pub async fn calculate_fee_payer_outflow(
382 fee_payer_pubkey: &Pubkey,
383 transaction: &mut VersionedTransactionResolved,
384 rpc_client: &RpcClient,
385 price_source: &PriceSource,
386 ) -> Result<u64, KoraError> {
387 let mut total = 0u64;
388
389 let parsed_system_instructions = transaction.get_or_parse_system_instructions()?;
391
392 for instruction in parsed_system_instructions
393 .get(&ParsedSystemInstructionType::SystemTransfer)
394 .unwrap_or(&vec![])
395 {
396 if let ParsedSystemInstructionData::SystemTransfer { lamports, sender, receiver } =
397 instruction
398 {
399 if *sender == *fee_payer_pubkey {
400 total = total.checked_add(*lamports).ok_or_else(|| {
401 log::error!("Outflow calculation overflow in SystemTransfer");
402 KoraError::ValidationError("Outflow calculation overflow".to_string())
403 })?;
404 }
405 if *receiver == *fee_payer_pubkey {
406 total = total.saturating_sub(*lamports);
407 }
408 }
409 }
410
411 for instruction in parsed_system_instructions
412 .get(&ParsedSystemInstructionType::SystemCreateAccount)
413 .unwrap_or(&vec![])
414 {
415 if let ParsedSystemInstructionData::SystemCreateAccount { lamports, payer } =
416 instruction
417 {
418 if *payer == *fee_payer_pubkey {
419 total = total.checked_add(*lamports).ok_or_else(|| {
420 log::error!("Outflow calculation overflow in SystemCreateAccount");
421 KoraError::ValidationError("Outflow calculation overflow".to_string())
422 })?;
423 }
424 }
425 }
426
427 for instruction in parsed_system_instructions
428 .get(&ParsedSystemInstructionType::SystemWithdrawNonceAccount)
429 .unwrap_or(&vec![])
430 {
431 if let ParsedSystemInstructionData::SystemWithdrawNonceAccount {
432 lamports,
433 nonce_authority,
434 recipient,
435 } = instruction
436 {
437 if *nonce_authority == *fee_payer_pubkey {
438 total = total.checked_add(*lamports).ok_or_else(|| {
439 log::error!("Outflow calculation overflow in SystemWithdrawNonceAccount");
440 KoraError::ValidationError("Outflow calculation overflow".to_string())
441 })?;
442 }
443 if *recipient == *fee_payer_pubkey {
444 total = total.saturating_sub(*lamports);
445 }
446 }
447 }
448
449 let spl_instructions = transaction.get_or_parse_spl_instructions()?;
451 let empty_vec = vec![];
452 let spl_transfers =
453 spl_instructions.get(&ParsedSPLInstructionType::SplTokenTransfer).unwrap_or(&empty_vec);
454
455 if !spl_transfers.is_empty() {
456 let spl_outflow = TokenUtil::calculate_spl_transfers_value_in_lamports(
457 spl_transfers,
458 fee_payer_pubkey,
459 price_source,
460 rpc_client,
461 )
462 .await?;
463
464 total = total.checked_add(spl_outflow).ok_or_else(|| {
465 log::error!("Fee payer outflow overflow: sol={}, spl={}", total, spl_outflow);
466 KoraError::ValidationError("Fee payer outflow calculation overflow".to_string())
467 })?;
468 }
469
470 Ok(total)
471 }
472}
473
474pub struct TransactionFeeUtil {}
475
476impl TransactionFeeUtil {
477 pub async fn get_estimate_fee(
478 rpc_client: &RpcClient,
479 message: &VersionedMessage,
480 ) -> Result<u64, KoraError> {
481 match message {
482 VersionedMessage::Legacy(message) => rpc_client.get_fee_for_message(message).await,
483 VersionedMessage::V0(message) => rpc_client.get_fee_for_message(message).await,
484 }
485 .map_err(|e| KoraError::RpcError(e.to_string()))
486 }
487
488 pub async fn get_estimate_fee_resolved(
490 rpc_client: &RpcClient,
491 resolved_transaction: &VersionedTransactionResolved,
492 ) -> Result<u64, KoraError> {
493 let message = &resolved_transaction.transaction.message;
494
495 match message {
496 VersionedMessage::Legacy(message) => {
497 rpc_client.get_fee_for_message(message).await
499 }
500 VersionedMessage::V0(v0_message) => rpc_client.get_fee_for_message(v0_message).await,
501 }
502 .map_err(|e| KoraError::RpcError(e.to_string()))
503 }
504}
505
506#[cfg(test)]
507mod tests {
508 use super::*;
509 use crate::{
510 constant::{ESTIMATED_LAMPORTS_FOR_PAYMENT_INSTRUCTION, LAMPORTS_PER_SIGNATURE},
511 fee::fee::{FeeConfigUtil, TransactionFeeUtil},
512 tests::{
513 common::{
514 create_mock_rpc_client_with_account, create_mock_token_account,
515 setup_or_get_test_config, setup_or_get_test_signer,
516 },
517 config_mock::ConfigMockBuilder,
518 rpc_mock::RpcMockBuilder,
519 },
520 token::{interface::TokenInterface, spl_token::TokenProgram},
521 transaction::TransactionUtil,
522 };
523 use solana_message::{v0, Message, VersionedMessage};
524 use solana_sdk::{
525 account::Account,
526 hash::Hash,
527 instruction::Instruction,
528 pubkey::Pubkey,
529 signature::{Keypair, Signer},
530 };
531 use solana_system_interface::{
532 instruction::{
533 create_account, create_account_with_seed, transfer, transfer_with_seed,
534 withdraw_nonce_account,
535 },
536 program::ID as SYSTEM_PROGRAM_ID,
537 };
538 use spl_associated_token_account_interface::address::get_associated_token_address;
539
540 #[test]
541 fn test_is_fee_payer_in_signers_legacy_fee_payer_is_signer() {
542 let fee_payer = setup_or_get_test_signer();
543 let other_signer = Keypair::new();
544 let recipient = Keypair::new();
545
546 let instruction = transfer(&other_signer.pubkey(), &recipient.pubkey(), 1000);
547
548 let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
549
550 let resolved_transaction =
551 TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
552
553 assert!(FeeConfigUtil::is_fee_payer_in_signers(&resolved_transaction, &fee_payer).unwrap());
554 }
555
556 #[test]
557 fn test_is_fee_payer_in_signers_legacy_fee_payer_not_signer() {
558 let fee_payer_pubkey = setup_or_get_test_signer();
559 let sender = Keypair::new();
560 let recipient = Keypair::new();
561
562 let instruction = transfer(&sender.pubkey(), &recipient.pubkey(), 1000);
563
564 let message =
565 VersionedMessage::Legacy(Message::new(&[instruction], Some(&sender.pubkey())));
566
567 let resolved_transaction =
568 TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
569
570 assert!(!FeeConfigUtil::is_fee_payer_in_signers(&resolved_transaction, &fee_payer_pubkey)
571 .unwrap());
572 }
573
574 #[test]
575 fn test_is_fee_payer_in_signers_v0_fee_payer_is_signer() {
576 let fee_payer = setup_or_get_test_signer();
577 let other_signer = Keypair::new();
578 let recipient = Keypair::new();
579
580 let v0_message = v0::Message::try_compile(
581 &fee_payer,
582 &[transfer(&other_signer.pubkey(), &recipient.pubkey(), 1000)],
583 &[],
584 Hash::default(),
585 )
586 .expect("Failed to compile V0 message");
587
588 let message = VersionedMessage::V0(v0_message);
589 let resolved_transaction =
590 TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
591
592 assert!(FeeConfigUtil::is_fee_payer_in_signers(&resolved_transaction, &fee_payer).unwrap());
593 }
594
595 #[test]
596 fn test_is_fee_payer_in_signers_v0_fee_payer_not_signer() {
597 let fee_payer_pubkey = setup_or_get_test_signer();
598 let sender = Keypair::new();
599 let recipient = Keypair::new();
600
601 let v0_message = v0::Message::try_compile(
602 &sender.pubkey(),
603 &[transfer(&sender.pubkey(), &recipient.pubkey(), 1000)],
604 &[],
605 Hash::default(),
606 )
607 .expect("Failed to compile V0 message");
608
609 let message = VersionedMessage::V0(v0_message);
610 let resolved_transaction =
611 TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
612
613 assert!(!FeeConfigUtil::is_fee_payer_in_signers(&resolved_transaction, &fee_payer_pubkey)
614 .unwrap());
615 }
616
617 #[tokio::test]
618 async fn test_calculate_fee_payer_outflow_transfer() {
619 setup_or_get_test_config();
620 let mocked_rpc_client = RpcMockBuilder::new().build();
621 let fee_payer = Pubkey::new_unique();
622 let recipient = Pubkey::new_unique();
623
624 let transfer_instruction = transfer(&fee_payer, &recipient, 100_000);
626 let message =
627 VersionedMessage::Legacy(Message::new(&[transfer_instruction], Some(&fee_payer)));
628 let mut resolved_transaction =
629 TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
630
631 let outflow = FeeConfigUtil::calculate_fee_payer_outflow(
632 &fee_payer,
633 &mut resolved_transaction,
634 &mocked_rpc_client,
635 &crate::oracle::PriceSource::Mock,
636 )
637 .await
638 .unwrap();
639 assert_eq!(outflow, 100_000, "Transfer from fee payer should add to outflow");
640
641 let sender = Pubkey::new_unique();
643 let transfer_instruction = transfer(&sender, &fee_payer, 50_000);
644 let message =
645 VersionedMessage::Legacy(Message::new(&[transfer_instruction], Some(&fee_payer)));
646 let mut resolved_transaction =
647 TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
648 let outflow = FeeConfigUtil::calculate_fee_payer_outflow(
649 &fee_payer,
650 &mut resolved_transaction,
651 &mocked_rpc_client,
652 &crate::oracle::PriceSource::Mock,
653 )
654 .await
655 .unwrap();
656 assert_eq!(outflow, 0, "Transfer to fee payer should subtract from outflow (saturating)");
657
658 let other_sender = Pubkey::new_unique();
660 let transfer_instruction = transfer(&other_sender, &recipient, 500_000);
661 let message =
662 VersionedMessage::Legacy(Message::new(&[transfer_instruction], Some(&fee_payer)));
663 let mut resolved_transaction =
664 TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
665 let outflow = FeeConfigUtil::calculate_fee_payer_outflow(
666 &fee_payer,
667 &mut resolved_transaction,
668 &mocked_rpc_client,
669 &crate::oracle::PriceSource::Mock,
670 )
671 .await
672 .unwrap();
673 assert_eq!(outflow, 0, "Transfer from other account should not affect outflow");
674 }
675
676 #[tokio::test]
677 async fn test_calculate_fee_payer_outflow_transfer_with_seed() {
678 setup_or_get_test_config();
679 let mocked_rpc_client = RpcMockBuilder::new().build();
680 let fee_payer = Pubkey::new_unique();
681 let recipient = Pubkey::new_unique();
682
683 let transfer_instruction = transfer_with_seed(
685 &fee_payer,
686 &fee_payer,
687 "test_seed".to_string(),
688 &SYSTEM_PROGRAM_ID,
689 &recipient,
690 150_000,
691 );
692 let message =
693 VersionedMessage::Legacy(Message::new(&[transfer_instruction], Some(&fee_payer)));
694 let mut resolved_transaction =
695 TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
696 let outflow = FeeConfigUtil::calculate_fee_payer_outflow(
697 &fee_payer,
698 &mut resolved_transaction,
699 &mocked_rpc_client,
700 &crate::oracle::PriceSource::Mock,
701 )
702 .await
703 .unwrap();
704 assert_eq!(outflow, 150_000, "TransferWithSeed from fee payer should add to outflow");
705
706 let other_sender = Pubkey::new_unique();
708 let transfer_instruction = transfer_with_seed(
709 &other_sender,
710 &other_sender,
711 "test_seed".to_string(),
712 &SYSTEM_PROGRAM_ID,
713 &fee_payer,
714 75_000,
715 );
716 let message =
717 VersionedMessage::Legacy(Message::new(&[transfer_instruction], Some(&fee_payer)));
718 let mut resolved_transaction =
719 TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
720 let outflow = FeeConfigUtil::calculate_fee_payer_outflow(
721 &fee_payer,
722 &mut resolved_transaction,
723 &mocked_rpc_client,
724 &crate::oracle::PriceSource::Mock,
725 )
726 .await
727 .unwrap();
728 assert_eq!(
729 outflow, 0,
730 "TransferWithSeed to fee payer should subtract from outflow (saturating)"
731 );
732 }
733
734 #[tokio::test]
735 async fn test_calculate_fee_payer_outflow_create_account() {
736 setup_or_get_test_config();
737 let mocked_rpc_client = RpcMockBuilder::new().build();
738 let fee_payer = Pubkey::new_unique();
739 let new_account = Pubkey::new_unique();
740
741 let create_instruction =
743 create_account(&fee_payer, &new_account, 200_000, 100, &SYSTEM_PROGRAM_ID);
744 let message =
745 VersionedMessage::Legacy(Message::new(&[create_instruction], Some(&fee_payer)));
746 let mut resolved_transaction =
747 TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
748 let outflow = FeeConfigUtil::calculate_fee_payer_outflow(
749 &fee_payer,
750 &mut resolved_transaction,
751 &mocked_rpc_client,
752 &crate::oracle::PriceSource::Mock,
753 )
754 .await
755 .unwrap();
756 assert_eq!(outflow, 200_000, "CreateAccount funded by fee payer should add to outflow");
757
758 let other_funder = Pubkey::new_unique();
760 let create_instruction =
761 create_account(&other_funder, &new_account, 1_000_000, 100, &SYSTEM_PROGRAM_ID);
762 let message =
763 VersionedMessage::Legacy(Message::new(&[create_instruction], Some(&fee_payer)));
764 let mut resolved_transaction =
765 TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
766 let outflow = FeeConfigUtil::calculate_fee_payer_outflow(
767 &fee_payer,
768 &mut resolved_transaction,
769 &mocked_rpc_client,
770 &crate::oracle::PriceSource::Mock,
771 )
772 .await
773 .unwrap();
774 assert_eq!(outflow, 0, "CreateAccount funded by other account should not affect outflow");
775 }
776
777 #[tokio::test]
778 async fn test_calculate_fee_payer_outflow_create_account_with_seed() {
779 setup_or_get_test_config();
780 let mocked_rpc_client = RpcMockBuilder::new().build();
781 let fee_payer = Pubkey::new_unique();
782 let new_account = Pubkey::new_unique();
783
784 let create_instruction = create_account_with_seed(
786 &fee_payer,
787 &new_account,
788 &fee_payer,
789 "test_seed",
790 300_000,
791 100,
792 &SYSTEM_PROGRAM_ID,
793 );
794 let message =
795 VersionedMessage::Legacy(Message::new(&[create_instruction], Some(&fee_payer)));
796 let mut resolved_transaction =
797 TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
798 let outflow = FeeConfigUtil::calculate_fee_payer_outflow(
799 &fee_payer,
800 &mut resolved_transaction,
801 &mocked_rpc_client,
802 &crate::oracle::PriceSource::Mock,
803 )
804 .await
805 .unwrap();
806 assert_eq!(
807 outflow, 300_000,
808 "CreateAccountWithSeed funded by fee payer should add to outflow"
809 );
810 }
811
812 #[tokio::test]
813 async fn test_calculate_fee_payer_outflow_nonce_withdraw() {
814 setup_or_get_test_config();
815 let mocked_rpc_client = RpcMockBuilder::new().build();
816 let nonce_account = Pubkey::new_unique();
817 let fee_payer = Pubkey::new_unique();
818 let recipient = Pubkey::new_unique();
819
820 let withdraw_instruction =
822 withdraw_nonce_account(&nonce_account, &fee_payer, &recipient, 50_000);
823 let message =
824 VersionedMessage::Legacy(Message::new(&[withdraw_instruction], Some(&fee_payer)));
825 let mut resolved_transaction =
826 TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
827 let outflow = FeeConfigUtil::calculate_fee_payer_outflow(
828 &fee_payer,
829 &mut resolved_transaction,
830 &mocked_rpc_client,
831 &crate::oracle::PriceSource::Mock,
832 )
833 .await
834 .unwrap();
835 assert_eq!(
836 outflow, 50_000,
837 "WithdrawNonceAccount from fee payer nonce should add to outflow"
838 );
839
840 let nonce_account = Pubkey::new_unique();
842 let withdraw_instruction =
843 withdraw_nonce_account(&nonce_account, &fee_payer, &fee_payer, 25_000);
844 let message =
845 VersionedMessage::Legacy(Message::new(&[withdraw_instruction], Some(&fee_payer)));
846 let mut resolved_transaction =
847 TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
848 let outflow = FeeConfigUtil::calculate_fee_payer_outflow(
849 &fee_payer,
850 &mut resolved_transaction,
851 &mocked_rpc_client,
852 &crate::oracle::PriceSource::Mock,
853 )
854 .await
855 .unwrap();
856 assert_eq!(
857 outflow, 0,
858 "WithdrawNonceAccount to fee payer should subtract from outflow (saturating)"
859 );
860 }
861
862 #[tokio::test]
863 async fn test_calculate_fee_payer_outflow_multiple_instructions() {
864 setup_or_get_test_config();
865 let mocked_rpc_client = RpcMockBuilder::new().build();
866 let fee_payer = Pubkey::new_unique();
867 let recipient = Pubkey::new_unique();
868 let sender = Pubkey::new_unique();
869 let new_account = Pubkey::new_unique();
870
871 let instructions = vec![
873 transfer(&fee_payer, &recipient, 100_000), transfer(&sender, &fee_payer, 30_000), create_account(&fee_payer, &new_account, 50_000, 100, &SYSTEM_PROGRAM_ID), ];
877 let message = VersionedMessage::Legacy(Message::new(&instructions, Some(&fee_payer)));
878 let mut resolved_transaction =
879 TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
880 let outflow = FeeConfigUtil::calculate_fee_payer_outflow(
881 &fee_payer,
882 &mut resolved_transaction,
883 &mocked_rpc_client,
884 &crate::oracle::PriceSource::Mock,
885 )
886 .await
887 .unwrap();
888 assert_eq!(
889 outflow, 120_000,
890 "Multiple instructions should sum correctly: 100000 - 30000 + 50000 = 120000"
891 );
892 }
893
894 #[tokio::test]
895 async fn test_calculate_fee_payer_outflow_non_system_program() {
896 setup_or_get_test_config();
897 let mocked_rpc_client = RpcMockBuilder::new().build();
898 let fee_payer = Pubkey::new_unique();
899 let fake_program = Pubkey::new_unique();
900
901 let instruction = Instruction::new_with_bincode(
903 fake_program,
904 &[0u8],
905 vec![], );
907 let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
908 let mut resolved_transaction =
909 TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
910 let outflow = FeeConfigUtil::calculate_fee_payer_outflow(
911 &fee_payer,
912 &mut resolved_transaction,
913 &mocked_rpc_client,
914 &crate::oracle::PriceSource::Mock,
915 )
916 .await
917 .unwrap();
918 assert_eq!(outflow, 0, "Non-system program should not affect outflow");
919 }
920
921 #[tokio::test]
922 async fn test_analyze_payment_instructions_with_payment() {
923 let _m = ConfigMockBuilder::new().build_and_setup();
924 let cache_ctx = CacheUtil::get_account_context();
925 cache_ctx.checkpoint();
926 let signer = setup_or_get_test_signer();
927 let mint = Pubkey::new_unique();
928
929 let mocked_account = create_mock_token_account(&signer, &mint);
930 let mocked_rpc_client = create_mock_rpc_client_with_account(&mocked_account);
931
932 cache_ctx.expect().times(1).returning(move |_, _, _| Ok(mocked_account.clone()));
934
935 let sender = Keypair::new();
936
937 let sender_token_account = get_associated_token_address(&sender.pubkey(), &mint);
938 let payment_token_account = get_associated_token_address(&signer, &mint);
939
940 let transfer_instruction = TokenProgram::new()
941 .create_transfer_instruction(
942 &sender_token_account,
943 &payment_token_account,
944 &sender.pubkey(),
945 1000,
946 )
947 .unwrap();
948
949 let message = VersionedMessage::Legacy(Message::new(&[transfer_instruction], None));
951 let mut resolved_transaction =
952 TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
953
954 let (has_payment, transfer_fees) = FeeConfigUtil::analyze_payment_instructions(
955 &mut resolved_transaction,
956 &mocked_rpc_client,
957 &signer,
958 )
959 .await
960 .unwrap();
961
962 assert!(has_payment, "Should detect payment instruction");
963 assert_eq!(transfer_fees, 0, "Should have no transfer fees for SPL token");
964 }
965
966 #[tokio::test]
967 async fn test_analyze_payment_instructions_without_payment() {
968 let signer = setup_or_get_test_signer();
969 setup_or_get_test_config();
970 let mocked_rpc_client = create_mock_rpc_client_with_account(&Account::default());
971
972 let sender = Keypair::new();
973 let recipient = Pubkey::new_unique();
974
975 let sol_transfer = transfer(&sender.pubkey(), &recipient, 100_000);
977
978 let message = VersionedMessage::Legacy(Message::new(&[sol_transfer], None));
980 let mut resolved_transaction =
981 TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
982
983 let (has_payment, transfer_fees) = FeeConfigUtil::analyze_payment_instructions(
984 &mut resolved_transaction,
985 &mocked_rpc_client,
986 &signer,
987 )
988 .await
989 .unwrap();
990
991 assert!(!has_payment, "Should not detect payment instruction");
992 assert_eq!(transfer_fees, 0, "Should have no transfer fees");
993 }
994
995 #[tokio::test]
996 async fn test_analyze_payment_instructions_with_wrong_destination() {
997 let _m = ConfigMockBuilder::new().build_and_setup();
998 let cache_ctx = CacheUtil::get_account_context();
999 cache_ctx.checkpoint();
1000 let signer = setup_or_get_test_signer();
1001 let sender = Keypair::new();
1002 let mint = Pubkey::new_unique();
1003
1004 let mocked_account = create_mock_token_account(&sender.pubkey(), &mint);
1005 let mocked_rpc_client = create_mock_rpc_client_with_account(&mocked_account);
1006
1007 cache_ctx.expect().times(1).returning(move |_, _, _| Ok(mocked_account.clone()));
1009
1010 let sender_token_account = get_associated_token_address(&sender.pubkey(), &mint);
1012 let recipient_token_account = get_associated_token_address(&sender.pubkey(), &mint);
1013
1014 let transfer_instruction = TokenProgram::new()
1016 .create_transfer_instruction(
1017 &sender_token_account,
1018 &recipient_token_account,
1019 &sender.pubkey(),
1020 1000,
1021 )
1022 .unwrap();
1023
1024 let message = VersionedMessage::Legacy(Message::new(&[transfer_instruction], None));
1026 let mut resolved_transaction =
1027 TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1028
1029 let (has_payment, transfer_fees) = FeeConfigUtil::analyze_payment_instructions(
1030 &mut resolved_transaction,
1031 &mocked_rpc_client,
1032 &signer,
1033 )
1034 .await
1035 .unwrap();
1036
1037 assert!(!has_payment, "Should not detect payment to wrong destination");
1038 assert_eq!(transfer_fees, 0, "Should have no transfer fees");
1039 }
1040
1041 #[tokio::test]
1042 async fn test_estimate_transaction_fee_basic() {
1043 let _m = ConfigMockBuilder::new().build_and_setup();
1044
1045 let fee_payer = Keypair::new();
1046 let recipient = Pubkey::new_unique();
1047
1048 let mocked_rpc_client = RpcMockBuilder::new().with_fee_estimate(5000).build();
1050
1051 let transfer_instruction = transfer(&fee_payer.pubkey(), &recipient, 100_000);
1053 let message = VersionedMessage::Legacy(Message::new(
1054 &[transfer_instruction],
1055 Some(&fee_payer.pubkey()),
1056 ));
1057 let mut resolved_transaction =
1058 TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1059
1060 let result = FeeConfigUtil::estimate_transaction_fee(
1061 &mocked_rpc_client,
1062 &mut resolved_transaction,
1063 &fee_payer.pubkey(),
1064 false,
1065 )
1066 .await
1067 .unwrap();
1068
1069 assert_eq!(result.total_fee_lamports, 105_000, "Should return base fee + outflow");
1071 }
1072
1073 #[tokio::test]
1074 async fn test_estimate_transaction_fee_kora_signer_not_in_signers() {
1075 let _m = ConfigMockBuilder::new().build_and_setup();
1076
1077 let sender = Keypair::new();
1078 let kora_fee_payer = Keypair::new();
1079 let recipient = Pubkey::new_unique();
1080
1081 let mocked_rpc_client = RpcMockBuilder::new().with_fee_estimate(5000).build();
1082
1083 let transfer_instruction = transfer(&sender.pubkey(), &recipient, 100_000);
1085 let message =
1086 VersionedMessage::Legacy(Message::new(&[transfer_instruction], Some(&sender.pubkey())));
1087 let mut resolved_transaction =
1088 TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1089
1090 let result = FeeConfigUtil::estimate_transaction_fee(
1091 &mocked_rpc_client,
1092 &mut resolved_transaction,
1093 &kora_fee_payer.pubkey(),
1094 false,
1095 )
1096 .await
1097 .unwrap();
1098
1099 assert_eq!(
1101 result.total_fee_lamports,
1102 5000 + LAMPORTS_PER_SIGNATURE,
1103 "Should add Kora signature fee"
1104 );
1105 }
1106
1107 #[tokio::test]
1108 async fn test_estimate_transaction_fee_with_payment_required() {
1109 let _m = ConfigMockBuilder::new().build_and_setup();
1110 let cache_ctx = CacheUtil::get_account_context();
1111 cache_ctx.checkpoint();
1112
1113 let fee_payer = Keypair::new();
1114 let recipient = Pubkey::new_unique();
1115
1116 let mocked_rpc_client = RpcMockBuilder::new().with_fee_estimate(5000).build();
1117
1118 let transfer_instruction = transfer(&fee_payer.pubkey(), &recipient, 100_000);
1120 let message = VersionedMessage::Legacy(Message::new(
1121 &[transfer_instruction],
1122 Some(&fee_payer.pubkey()),
1123 ));
1124 let mut resolved_transaction =
1125 TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1126
1127 let result = FeeConfigUtil::estimate_transaction_fee(
1128 &mocked_rpc_client,
1129 &mut resolved_transaction,
1130 &fee_payer.pubkey(),
1131 true, )
1133 .await
1134 .unwrap();
1135
1136 let expected = 5000 + 100_000 + ESTIMATED_LAMPORTS_FOR_PAYMENT_INSTRUCTION;
1138 assert_eq!(
1139 result.total_fee_lamports, expected,
1140 "Should include payment instruction fee when required"
1141 );
1142 }
1143
1144 #[tokio::test]
1145 async fn test_analyze_payment_instructions_with_multiple_payments() {
1146 let _m = ConfigMockBuilder::new().build_and_setup();
1147 let cache_ctx = CacheUtil::get_account_context();
1148 cache_ctx.checkpoint();
1149 let signer = setup_or_get_test_signer();
1150 let mint = Pubkey::new_unique();
1151
1152 let mocked_account = create_mock_token_account(&signer, &mint);
1153 let mocked_rpc_client = create_mock_rpc_client_with_account(&mocked_account);
1154
1155 cache_ctx.expect().times(2).returning(move |_, _, _| Ok(mocked_account.clone()));
1156
1157 let sender = Keypair::new();
1158 let sender_token_account = get_associated_token_address(&sender.pubkey(), &mint);
1159 let payment_token_account = get_associated_token_address(&signer, &mint);
1160
1161 let transfer_1 = TokenProgram::new()
1162 .create_transfer_instruction(
1163 &sender_token_account,
1164 &payment_token_account,
1165 &sender.pubkey(),
1166 500,
1167 )
1168 .unwrap();
1169
1170 let transfer_2 = TokenProgram::new()
1171 .create_transfer_instruction(
1172 &sender_token_account,
1173 &payment_token_account,
1174 &sender.pubkey(),
1175 500,
1176 )
1177 .unwrap();
1178
1179 let message = VersionedMessage::Legacy(Message::new(&[transfer_1, transfer_2], None));
1180 let mut resolved_transaction =
1181 TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1182
1183 let (has_payment, transfer_fees) = FeeConfigUtil::analyze_payment_instructions(
1184 &mut resolved_transaction,
1185 &mocked_rpc_client,
1186 &signer,
1187 )
1188 .await
1189 .unwrap();
1190
1191 assert!(has_payment, "Should detect payment instructions");
1192 assert_eq!(transfer_fees, 0, "Should have no transfer fees for SPL tokens");
1193 }
1194
1195 #[tokio::test]
1196 async fn test_transaction_fee_util_get_estimate_fee_legacy() {
1197 let mocked_rpc_client = RpcMockBuilder::new().with_fee_estimate(7500).build();
1198
1199 let fee_payer = Keypair::new();
1200 let recipient = Pubkey::new_unique();
1201 let transfer_instruction = transfer(&fee_payer.pubkey(), &recipient, 50_000);
1202
1203 let legacy_message = Message::new(&[transfer_instruction], Some(&fee_payer.pubkey()));
1204 let versioned_message = VersionedMessage::Legacy(legacy_message);
1205
1206 let result = TransactionFeeUtil::get_estimate_fee(&mocked_rpc_client, &versioned_message)
1207 .await
1208 .unwrap();
1209
1210 assert_eq!(result, 7500, "Should return mocked base fee for legacy message");
1211 }
1212
1213 #[tokio::test]
1214 async fn test_transaction_fee_util_get_estimate_fee_v0() {
1215 let mocked_rpc_client = RpcMockBuilder::new().with_fee_estimate(12500).build();
1216
1217 let fee_payer = Keypair::new();
1218 let recipient = Pubkey::new_unique();
1219 let transfer_instruction = transfer(&fee_payer.pubkey(), &recipient, 50_000);
1220
1221 let v0_message = v0::Message::try_compile(
1222 &fee_payer.pubkey(),
1223 &[transfer_instruction],
1224 &[],
1225 Hash::default(),
1226 )
1227 .expect("Failed to compile V0 message");
1228
1229 let versioned_message = VersionedMessage::V0(v0_message);
1230
1231 let result = TransactionFeeUtil::get_estimate_fee(&mocked_rpc_client, &versioned_message)
1232 .await
1233 .unwrap();
1234
1235 assert_eq!(result, 12500, "Should return mocked base fee for V0 message");
1236 }
1237}