kora_lib/rpc_server/method/
estimate_transaction_fee.rs1use solana_keychain::SolanaSigner;
2use std::sync::Arc;
3use utoipa::ToSchema;
4
5use crate::{
6 error::KoraError,
7 fee::fee::FeeConfigUtil,
8 rpc_server::middleware_utils::default_sig_verify,
9 state::get_request_signer_with_signer_key,
10 transaction::{TransactionUtil, VersionedTransactionResolved},
11};
12
13use serde::{Deserialize, Serialize};
14use solana_client::nonblocking::rpc_client::RpcClient;
15
16#[cfg(not(test))]
17use crate::state::get_config;
18
19#[cfg(test)]
20use crate::tests::config_mock::mock_state::get_config;
21
22#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
23pub struct EstimateTransactionFeeRequest {
24 pub transaction: String, #[serde(default)]
26 pub fee_token: Option<String>,
27 #[serde(default, skip_serializing_if = "Option::is_none")]
29 pub signer_key: Option<String>,
30 #[serde(default = "default_sig_verify")]
32 pub sig_verify: bool,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
36pub struct EstimateTransactionFeeResponse {
37 pub fee_in_lamports: u64,
38 pub fee_in_token: Option<u64>,
39 pub signer_pubkey: String,
41 pub payment_address: String,
43}
44
45pub async fn estimate_transaction_fee(
46 rpc_client: &Arc<RpcClient>,
47 request: EstimateTransactionFeeRequest,
48) -> Result<EstimateTransactionFeeResponse, KoraError> {
49 let transaction = TransactionUtil::decode_b64_transaction(&request.transaction)?;
50
51 let signer = get_request_signer_with_signer_key(request.signer_key.as_deref())?;
52 let config = &get_config()?;
53 let payment_destination = config.kora.get_payment_address(&signer.pubkey())?;
54
55 let validation_config = &config.validation;
56 let fee_payer = signer.pubkey();
57
58 let mut resolved_transaction = VersionedTransactionResolved::from_transaction(
59 &transaction,
60 config,
61 rpc_client,
62 request.sig_verify,
63 )
64 .await?;
65
66 let fee_calculation = FeeConfigUtil::estimate_kora_fee(
67 &mut resolved_transaction,
68 &fee_payer,
69 validation_config.is_payment_required(),
70 rpc_client,
71 config,
72 )
73 .await?;
74
75 let fee_in_lamports = fee_calculation.total_fee_lamports;
76
77 #[allow(clippy::needless_borrow)]
78 let fee_in_token = FeeConfigUtil::calculate_fee_in_token(
80 fee_in_lamports,
81 request.fee_token.as_deref(),
82 rpc_client,
83 &config,
84 )
85 .await?;
86
87 Ok(EstimateTransactionFeeResponse {
88 fee_in_lamports,
89 fee_in_token,
90 signer_pubkey: fee_payer.to_string(),
91 payment_address: payment_destination.to_string(),
92 })
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98 use crate::tests::{
99 common::{setup_or_get_test_config, setup_or_get_test_signer, RpcMockBuilder},
100 transaction_mock::create_mock_encoded_transaction,
101 };
102
103 #[tokio::test]
104 async fn test_estimate_transaction_fee_decode_error() {
105 let _ = setup_or_get_test_config();
106 let _ = setup_or_get_test_signer();
107
108 let rpc_client = Arc::new(RpcMockBuilder::new().build());
109
110 let request = EstimateTransactionFeeRequest {
111 transaction: "invalid_base64!@#$".to_string(),
112 fee_token: None,
113 signer_key: None,
114 sig_verify: true,
115 };
116
117 let result = estimate_transaction_fee(&rpc_client, request).await;
118
119 assert!(result.is_err(), "Should fail with decode error");
120 }
121
122 #[tokio::test]
123 async fn test_estimate_transaction_fee_invalid_signer_key() {
124 let _ = setup_or_get_test_config();
125 let _ = setup_or_get_test_signer();
126
127 let rpc_client = Arc::new(RpcMockBuilder::new().build());
128
129 let request = EstimateTransactionFeeRequest {
130 transaction: create_mock_encoded_transaction(),
131 fee_token: None,
132 signer_key: Some("invalid_pubkey".to_string()),
133 sig_verify: true,
134 };
135
136 let result = estimate_transaction_fee(&rpc_client, request).await;
137
138 assert!(result.is_err(), "Should fail with invalid signer key");
139 let error = result.unwrap_err();
140 assert!(matches!(error, KoraError::ValidationError(_)), "Should return ValidationError");
141 }
142
143 #[tokio::test]
144 async fn test_estimate_transaction_fee_invalid_token_mint() {
145 let _ = setup_or_get_test_config();
146 let _ = setup_or_get_test_signer();
147
148 let rpc_client = Arc::new(RpcMockBuilder::new().build());
149
150 let request = EstimateTransactionFeeRequest {
151 transaction: create_mock_encoded_transaction(),
152 fee_token: Some("invalid_mint_address".to_string()),
153 signer_key: None,
154 sig_verify: true,
155 };
156
157 let result = estimate_transaction_fee(&rpc_client, request).await;
158
159 assert!(result.is_err(), "Should fail with invalid token mint");
160 let error = result.unwrap_err();
161
162 assert!(
163 matches!(error, KoraError::InvalidTransaction(_)),
164 "Should return InvalidTransaction error due to invalid mint parsing"
165 );
166 }
167}