kora_lib/rpc_server/method/
estimate_bundle_fee.rs1use crate::{
2 bundle::{BundleError, BundleProcessingMode, 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 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub sign_only_indices: Option<Vec<usize>>,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
39pub struct EstimateBundleFeeResponse {
40 pub fee_in_lamports: u64,
41 pub fee_in_token: Option<u64>,
42 pub signer_pubkey: String,
44 pub payment_address: String,
46}
47
48pub async fn estimate_bundle_fee(
49 rpc_client: &Arc<RpcClient>,
50 request: EstimateBundleFeeRequest,
51) -> Result<EstimateBundleFeeResponse, KoraError> {
52 let config = &get_config()?;
53
54 if !config.kora.bundle.enabled {
55 return Err(BundleError::Jito(JitoError::NotEnabled).into());
56 }
57
58 BundleValidator::validate_jito_bundle_size(&request.transactions)?;
60
61 let (transactions_to_process, _index_to_position) =
63 BundleProcessor::extract_transactions_to_process(
64 &request.transactions,
65 request.sign_only_indices,
66 )?;
67
68 let signer = get_request_signer_with_signer_key(request.signer_key.as_deref())?;
69 let fee_payer = signer.pubkey();
70 let payment_destination = config.kora.get_payment_address(&fee_payer)?;
71
72 let sig_verify = request.sig_verify || config.kora.force_sig_verify;
73 let processor = BundleProcessor::process_bundle(
74 &transactions_to_process,
75 fee_payer,
76 &payment_destination,
77 config,
78 rpc_client,
79 sig_verify,
80 None,
81 BundleProcessingMode::SkipUsage,
82 )
83 .await?;
84
85 let fee_in_lamports = processor.total_required_lamports;
86
87 let fee_in_token = FeeConfigUtil::calculate_fee_in_token(
89 fee_in_lamports,
90 request.fee_token.as_deref(),
91 rpc_client,
92 config,
93 )
94 .await?;
95
96 Ok(EstimateBundleFeeResponse {
97 fee_in_lamports,
98 fee_in_token,
99 signer_pubkey: fee_payer.to_string(),
100 payment_address: payment_destination.to_string(),
101 })
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 use crate::{
108 config::TransactionPluginType,
109 tests::{
110 common::{setup_or_get_test_signer, setup_or_get_test_usage_limiter, RpcMockBuilder},
111 config_mock::{mock_state::setup_config_mock, ConfigMockBuilder},
112 transaction_mock::create_mock_encoded_transaction,
113 },
114 };
115
116 #[tokio::test]
117 async fn test_estimate_bundle_fee_empty_bundle() {
118 let _m = ConfigMockBuilder::new().with_bundle_enabled(true).build_and_setup();
119 let _ = setup_or_get_test_signer();
120
121 let rpc_client = Arc::new(RpcMockBuilder::new().build());
122
123 let request = EstimateBundleFeeRequest {
124 transactions: vec![],
125 fee_token: None,
126 signer_key: None,
127 sig_verify: true,
128 sign_only_indices: None,
129 };
130
131 let result = estimate_bundle_fee(&rpc_client, request).await;
132
133 assert!(result.is_err(), "Should fail with empty bundle");
134 let err = result.unwrap_err();
135 assert!(matches!(err, KoraError::InvalidTransaction(_)));
136 }
137
138 #[tokio::test]
139 async fn test_estimate_bundle_fee_disabled() {
140 let _m = ConfigMockBuilder::new().with_bundle_enabled(false).build_and_setup();
141 let _ = setup_or_get_test_signer();
142
143 let rpc_client = Arc::new(RpcMockBuilder::new().build());
144
145 let request = EstimateBundleFeeRequest {
146 transactions: vec!["some_tx".to_string()],
147 fee_token: None,
148 signer_key: None,
149 sig_verify: true,
150 sign_only_indices: None,
151 };
152
153 let result = estimate_bundle_fee(&rpc_client, request).await;
154
155 assert!(result.is_err(), "Should fail when bundles disabled");
156 let err = result.unwrap_err();
157 assert!(matches!(err, KoraError::JitoError(_)));
158 if let KoraError::JitoError(msg) = err {
159 assert!(msg.contains("not enabled"));
160 }
161 }
162
163 #[tokio::test]
164 async fn test_estimate_bundle_fee_too_large() {
165 let _m = ConfigMockBuilder::new().with_bundle_enabled(true).build_and_setup();
166 let _ = setup_or_get_test_signer();
167
168 let rpc_client = Arc::new(RpcMockBuilder::new().build());
169
170 let request = EstimateBundleFeeRequest {
171 transactions: vec!["tx".to_string(); 6],
172 fee_token: None,
173 signer_key: None,
174 sig_verify: true,
175 sign_only_indices: None,
176 };
177
178 let result = estimate_bundle_fee(&rpc_client, request).await;
179
180 assert!(result.is_err(), "Should fail with too many transactions");
181 let err = result.unwrap_err();
182 assert!(matches!(err, KoraError::JitoError(_)));
183 if let KoraError::JitoError(msg) = err {
184 assert!(msg.contains("maximum size"));
185 }
186 }
187
188 #[tokio::test]
189 async fn test_estimate_bundle_fee_invalid_signer_key() {
190 let _m = ConfigMockBuilder::new().with_bundle_enabled(true).build_and_setup();
191 let _ = setup_or_get_test_signer();
192
193 let rpc_client = Arc::new(RpcMockBuilder::new().build());
194
195 let request = EstimateBundleFeeRequest {
196 transactions: vec!["some_tx".to_string()],
197 fee_token: None,
198 signer_key: Some("invalid_pubkey".to_string()),
199 sig_verify: true,
200 sign_only_indices: None,
201 };
202
203 let result = estimate_bundle_fee(&rpc_client, request).await;
204
205 assert!(result.is_err(), "Should fail with invalid signer key");
206 let err = result.unwrap_err();
207 assert!(matches!(err, KoraError::ValidationError(_)));
208 }
209
210 #[tokio::test]
211 async fn test_estimate_bundle_fee_exactly_max_size() {
212 let _m = ConfigMockBuilder::new()
213 .with_bundle_enabled(true)
214 .with_usage_limit_enabled(false)
215 .build_and_setup();
216 let _ = setup_or_get_test_signer();
217 let _ = setup_or_get_test_usage_limiter().await;
218
219 let rpc_client =
220 Arc::new(RpcMockBuilder::new().with_fee_estimate(5000).with_simulation().build());
221
222 let transactions: Vec<String> = (0..5).map(|_| create_mock_encoded_transaction()).collect();
224
225 let request = EstimateBundleFeeRequest {
226 transactions,
227 fee_token: None,
228 signer_key: None,
229 sig_verify: true,
230 sign_only_indices: None,
231 };
232
233 let result = estimate_bundle_fee(&rpc_client, request).await;
234
235 assert!(result.is_ok(), "Should succeed with valid transactions");
236 let response = result.unwrap();
237 assert!(response.fee_in_lamports > 0);
238 assert!(!response.signer_pubkey.is_empty());
239 assert!(!response.payment_address.is_empty());
240 }
241
242 #[tokio::test]
243 async fn test_estimate_bundle_fee_single_transaction() {
244 let _m = ConfigMockBuilder::new()
245 .with_bundle_enabled(true)
246 .with_usage_limit_enabled(false)
247 .build_and_setup();
248 let _ = setup_or_get_test_signer();
249 let _ = setup_or_get_test_usage_limiter().await;
250
251 let rpc_client =
252 Arc::new(RpcMockBuilder::new().with_fee_estimate(5000).with_simulation().build());
253
254 let request = EstimateBundleFeeRequest {
256 transactions: vec![create_mock_encoded_transaction()],
257 fee_token: None,
258 signer_key: None,
259 sig_verify: true,
260 sign_only_indices: None,
261 };
262
263 let result = estimate_bundle_fee(&rpc_client, request).await;
264
265 assert!(result.is_ok(), "Should succeed with valid transaction");
266 let response = result.unwrap();
267 assert!(response.fee_in_lamports > 0);
268 assert!(!response.signer_pubkey.is_empty());
269 assert!(!response.payment_address.is_empty());
270 }
271
272 #[tokio::test]
273 async fn test_estimate_bundle_fee_sig_verify_default() {
274 let json = r#"{"transactions": ["tx1"]}"#;
276 let request: EstimateBundleFeeRequest = serde_json::from_str(json).unwrap();
277
278 assert!(!request.sig_verify, "sig_verify should default to false");
279 assert!(request.signer_key.is_none());
280 }
281
282 #[tokio::test]
283 async fn test_estimate_bundle_fee_request_deserialization() {
284 let json = r#"{
285 "transactions": ["tx1", "tx2"],
286 "fee_token": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
287 "signer_key": "11111111111111111111111111111111",
288 "sig_verify": false
289 }"#;
290 let request: EstimateBundleFeeRequest = serde_json::from_str(json).unwrap();
291
292 assert_eq!(request.transactions.len(), 2);
293 assert_eq!(
294 request.fee_token,
295 Some("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string())
296 );
297 assert_eq!(request.signer_key, Some("11111111111111111111111111111111".to_string()));
298 assert!(!request.sig_verify);
299 }
300
301 #[tokio::test]
302 async fn test_estimate_bundle_fee_skips_plugins() {
303 let mut config = ConfigMockBuilder::new()
304 .with_bundle_enabled(true)
305 .with_usage_limit_enabled(false)
306 .build();
307 config.kora.plugins.enabled = vec![TransactionPluginType::GasSwap];
308 let _m = setup_config_mock(config);
309
310 let _ = setup_or_get_test_signer();
311 let _ = setup_or_get_test_usage_limiter().await;
312
313 let rpc_client =
314 Arc::new(RpcMockBuilder::new().with_fee_estimate(5000).with_simulation().build());
315
316 let request = EstimateBundleFeeRequest {
318 transactions: vec![create_mock_encoded_transaction()],
319 fee_token: None,
320 signer_key: None,
321 sig_verify: false,
322 sign_only_indices: None,
323 };
324
325 let result = estimate_bundle_fee(&rpc_client, request).await;
326 assert!(result.is_ok(), "estimateBundleFee should skip transaction plugins");
327 }
328}