kora_lib/rpc_server/method/
estimate_bundle_fee.rs1use crate::{
2 bundle::{BundleError, BundleProcessor, JitoError},
3 error::KoraError,
4 fee::fee::FeeConfigUtil,
5 rpc_server::middleware_utils::default_sig_verify,
6 state::get_request_signer_with_signer_key,
7 validator::bundle_validator::BundleValidator,
8};
9use serde::{Deserialize, Serialize};
10use solana_client::nonblocking::rpc_client::RpcClient;
11use solana_keychain::SolanaSigner;
12use std::sync::Arc;
13use utoipa::ToSchema;
14
15#[cfg(not(test))]
16use crate::state::get_config;
17
18#[cfg(test)]
19use crate::tests::config_mock::mock_state::get_config;
20
21#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
22pub struct EstimateBundleFeeRequest {
23 pub transactions: Vec<String>,
25 #[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 EstimateBundleFeeResponse {
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_bundle_fee(
46 rpc_client: &Arc<RpcClient>,
47 request: EstimateBundleFeeRequest,
48) -> Result<EstimateBundleFeeResponse, KoraError> {
49 let config = &get_config()?;
50
51 if !config.kora.bundle.enabled {
52 return Err(BundleError::Jito(JitoError::NotEnabled).into());
53 }
54
55 BundleValidator::validate_jito_bundle_size(&request.transactions)?;
56
57 let signer = get_request_signer_with_signer_key(request.signer_key.as_deref())?;
58 let fee_payer = signer.pubkey();
59 let payment_destination = config.kora.get_payment_address(&fee_payer)?;
60
61 let processor = BundleProcessor::process_bundle(
62 &request.transactions,
63 fee_payer,
64 &payment_destination,
65 config,
66 rpc_client,
67 request.sig_verify,
68 )
69 .await?;
70
71 let fee_in_lamports = processor.total_required_lamports;
72
73 let fee_in_token = FeeConfigUtil::calculate_fee_in_token(
75 fee_in_lamports,
76 request.fee_token.as_deref(),
77 rpc_client,
78 config,
79 )
80 .await?;
81
82 Ok(EstimateBundleFeeResponse {
83 fee_in_lamports,
84 fee_in_token,
85 signer_pubkey: fee_payer.to_string(),
86 payment_address: payment_destination.to_string(),
87 })
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93 use crate::tests::{
94 common::{setup_or_get_test_signer, setup_or_get_test_usage_limiter, RpcMockBuilder},
95 config_mock::ConfigMockBuilder,
96 transaction_mock::create_mock_encoded_transaction,
97 };
98
99 #[tokio::test]
100 async fn test_estimate_bundle_fee_empty_bundle() {
101 let _m = ConfigMockBuilder::new().with_bundle_enabled(true).build_and_setup();
102 let _ = setup_or_get_test_signer();
103
104 let rpc_client = Arc::new(RpcMockBuilder::new().build());
105
106 let request = EstimateBundleFeeRequest {
107 transactions: vec![],
108 fee_token: None,
109 signer_key: None,
110 sig_verify: true,
111 };
112
113 let result = estimate_bundle_fee(&rpc_client, request).await;
114
115 assert!(result.is_err(), "Should fail with empty bundle");
116 let err = result.unwrap_err();
117 assert!(matches!(err, KoraError::InvalidTransaction(_)));
118 }
119
120 #[tokio::test]
121 async fn test_estimate_bundle_fee_disabled() {
122 let _m = ConfigMockBuilder::new().with_bundle_enabled(false).build_and_setup();
123 let _ = setup_or_get_test_signer();
124
125 let rpc_client = Arc::new(RpcMockBuilder::new().build());
126
127 let request = EstimateBundleFeeRequest {
128 transactions: vec!["some_tx".to_string()],
129 fee_token: None,
130 signer_key: None,
131 sig_verify: true,
132 };
133
134 let result = estimate_bundle_fee(&rpc_client, request).await;
135
136 assert!(result.is_err(), "Should fail when bundles disabled");
137 let err = result.unwrap_err();
138 assert!(matches!(err, KoraError::JitoError(_)));
139 if let KoraError::JitoError(msg) = err {
140 assert!(msg.contains("not enabled"));
141 }
142 }
143
144 #[tokio::test]
145 async fn test_estimate_bundle_fee_too_large() {
146 let _m = ConfigMockBuilder::new().with_bundle_enabled(true).build_and_setup();
147 let _ = setup_or_get_test_signer();
148
149 let rpc_client = Arc::new(RpcMockBuilder::new().build());
150
151 let request = EstimateBundleFeeRequest {
152 transactions: vec!["tx".to_string(); 6],
153 fee_token: None,
154 signer_key: None,
155 sig_verify: true,
156 };
157
158 let result = estimate_bundle_fee(&rpc_client, request).await;
159
160 assert!(result.is_err(), "Should fail with too many transactions");
161 let err = result.unwrap_err();
162 assert!(matches!(err, KoraError::JitoError(_)));
163 if let KoraError::JitoError(msg) = err {
164 assert!(msg.contains("maximum size"));
165 }
166 }
167
168 #[tokio::test]
169 async fn test_estimate_bundle_fee_invalid_signer_key() {
170 let _m = ConfigMockBuilder::new().with_bundle_enabled(true).build_and_setup();
171 let _ = setup_or_get_test_signer();
172
173 let rpc_client = Arc::new(RpcMockBuilder::new().build());
174
175 let request = EstimateBundleFeeRequest {
176 transactions: vec!["some_tx".to_string()],
177 fee_token: None,
178 signer_key: Some("invalid_pubkey".to_string()),
179 sig_verify: true,
180 };
181
182 let result = estimate_bundle_fee(&rpc_client, request).await;
183
184 assert!(result.is_err(), "Should fail with invalid signer key");
185 let err = result.unwrap_err();
186 assert!(matches!(err, KoraError::ValidationError(_)));
187 }
188
189 #[tokio::test]
190 async fn test_estimate_bundle_fee_exactly_max_size() {
191 let _m = ConfigMockBuilder::new()
192 .with_bundle_enabled(true)
193 .with_usage_limit_enabled(false)
194 .build_and_setup();
195 let _ = setup_or_get_test_signer();
196 let _ = setup_or_get_test_usage_limiter().await;
197
198 let rpc_client =
199 Arc::new(RpcMockBuilder::new().with_fee_estimate(5000).with_simulation().build());
200
201 let transactions: Vec<String> = (0..5).map(|_| create_mock_encoded_transaction()).collect();
203
204 let request = EstimateBundleFeeRequest {
205 transactions,
206 fee_token: None,
207 signer_key: None,
208 sig_verify: true,
209 };
210
211 let result = estimate_bundle_fee(&rpc_client, request).await;
212
213 assert!(result.is_ok(), "Should succeed with valid transactions");
214 let response = result.unwrap();
215 assert!(response.fee_in_lamports > 0);
216 assert!(!response.signer_pubkey.is_empty());
217 assert!(!response.payment_address.is_empty());
218 }
219
220 #[tokio::test]
221 async fn test_estimate_bundle_fee_single_transaction() {
222 let _m = ConfigMockBuilder::new()
223 .with_bundle_enabled(true)
224 .with_usage_limit_enabled(false)
225 .build_and_setup();
226 let _ = setup_or_get_test_signer();
227 let _ = setup_or_get_test_usage_limiter().await;
228
229 let rpc_client =
230 Arc::new(RpcMockBuilder::new().with_fee_estimate(5000).with_simulation().build());
231
232 let request = EstimateBundleFeeRequest {
234 transactions: vec![create_mock_encoded_transaction()],
235 fee_token: None,
236 signer_key: None,
237 sig_verify: true,
238 };
239
240 let result = estimate_bundle_fee(&rpc_client, request).await;
241
242 assert!(result.is_ok(), "Should succeed with valid transaction");
243 let response = result.unwrap();
244 assert!(response.fee_in_lamports > 0);
245 assert!(!response.signer_pubkey.is_empty());
246 assert!(!response.payment_address.is_empty());
247 }
248
249 #[tokio::test]
250 async fn test_estimate_bundle_fee_sig_verify_default() {
251 let json = r#"{"transactions": ["tx1"]}"#;
253 let request: EstimateBundleFeeRequest = serde_json::from_str(json).unwrap();
254
255 assert!(!request.sig_verify, "sig_verify should default to false");
256 assert!(request.signer_key.is_none());
257 }
258
259 #[tokio::test]
260 async fn test_estimate_bundle_fee_request_deserialization() {
261 let json = r#"{
262 "transactions": ["tx1", "tx2"],
263 "fee_token": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
264 "signer_key": "11111111111111111111111111111111",
265 "sig_verify": false
266 }"#;
267 let request: EstimateBundleFeeRequest = serde_json::from_str(json).unwrap();
268
269 assert_eq!(request.transactions.len(), 2);
270 assert_eq!(
271 request.fee_token,
272 Some("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string())
273 );
274 assert_eq!(request.signer_key, Some("11111111111111111111111111111111".to_string()));
275 assert!(!request.sig_verify);
276 }
277}