kora_lib/rpc_server/method/
estimate_transaction_fee.rs

1use 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, // Base64 encoded serialized transaction
25    #[serde(default)]
26    pub fee_token: Option<String>,
27    /// Optional signer signer_key to ensure consistency across related RPC calls
28    #[serde(default, skip_serializing_if = "Option::is_none")]
29    pub signer_key: Option<String>,
30    /// Whether to verify signatures during simulation (defaults to true)
31    #[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    /// Public key of the signer used for fee estimation (for client consistency)
40    pub signer_pubkey: String,
41    /// Public key of the payment destination
42    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    // Calculate fee in token if requested
79    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}