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 processor = BundleProcessor::process_bundle(
73 &transactions_to_process,
74 fee_payer,
75 &payment_destination,
76 config,
77 rpc_client,
78 request.sig_verify,
79 BundleProcessingMode::SkipUsage,
80 )
81 .await?;
82
83 let fee_in_lamports = processor.total_required_lamports;
84
85 let fee_in_token = FeeConfigUtil::calculate_fee_in_token(
87 fee_in_lamports,
88 request.fee_token.as_deref(),
89 rpc_client,
90 config,
91 )
92 .await?;
93
94 Ok(EstimateBundleFeeResponse {
95 fee_in_lamports,
96 fee_in_token,
97 signer_pubkey: fee_payer.to_string(),
98 payment_address: payment_destination.to_string(),
99 })
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use crate::tests::{
106 common::{setup_or_get_test_signer, setup_or_get_test_usage_limiter, RpcMockBuilder},
107 config_mock::ConfigMockBuilder,
108 transaction_mock::create_mock_encoded_transaction,
109 };
110
111 #[tokio::test]
112 async fn test_estimate_bundle_fee_empty_bundle() {
113 let _m = ConfigMockBuilder::new().with_bundle_enabled(true).build_and_setup();
114 let _ = setup_or_get_test_signer();
115
116 let rpc_client = Arc::new(RpcMockBuilder::new().build());
117
118 let request = EstimateBundleFeeRequest {
119 transactions: vec![],
120 fee_token: None,
121 signer_key: None,
122 sig_verify: true,
123 sign_only_indices: None,
124 };
125
126 let result = estimate_bundle_fee(&rpc_client, request).await;
127
128 assert!(result.is_err(), "Should fail with empty bundle");
129 let err = result.unwrap_err();
130 assert!(matches!(err, KoraError::InvalidTransaction(_)));
131 }
132
133 #[tokio::test]
134 async fn test_estimate_bundle_fee_disabled() {
135 let _m = ConfigMockBuilder::new().with_bundle_enabled(false).build_and_setup();
136 let _ = setup_or_get_test_signer();
137
138 let rpc_client = Arc::new(RpcMockBuilder::new().build());
139
140 let request = EstimateBundleFeeRequest {
141 transactions: vec!["some_tx".to_string()],
142 fee_token: None,
143 signer_key: None,
144 sig_verify: true,
145 sign_only_indices: None,
146 };
147
148 let result = estimate_bundle_fee(&rpc_client, request).await;
149
150 assert!(result.is_err(), "Should fail when bundles disabled");
151 let err = result.unwrap_err();
152 assert!(matches!(err, KoraError::JitoError(_)));
153 if let KoraError::JitoError(msg) = err {
154 assert!(msg.contains("not enabled"));
155 }
156 }
157
158 #[tokio::test]
159 async fn test_estimate_bundle_fee_too_large() {
160 let _m = ConfigMockBuilder::new().with_bundle_enabled(true).build_and_setup();
161 let _ = setup_or_get_test_signer();
162
163 let rpc_client = Arc::new(RpcMockBuilder::new().build());
164
165 let request = EstimateBundleFeeRequest {
166 transactions: vec!["tx".to_string(); 6],
167 fee_token: None,
168 signer_key: None,
169 sig_verify: true,
170 sign_only_indices: None,
171 };
172
173 let result = estimate_bundle_fee(&rpc_client, request).await;
174
175 assert!(result.is_err(), "Should fail with too many transactions");
176 let err = result.unwrap_err();
177 assert!(matches!(err, KoraError::JitoError(_)));
178 if let KoraError::JitoError(msg) = err {
179 assert!(msg.contains("maximum size"));
180 }
181 }
182
183 #[tokio::test]
184 async fn test_estimate_bundle_fee_invalid_signer_key() {
185 let _m = ConfigMockBuilder::new().with_bundle_enabled(true).build_and_setup();
186 let _ = setup_or_get_test_signer();
187
188 let rpc_client = Arc::new(RpcMockBuilder::new().build());
189
190 let request = EstimateBundleFeeRequest {
191 transactions: vec!["some_tx".to_string()],
192 fee_token: None,
193 signer_key: Some("invalid_pubkey".to_string()),
194 sig_verify: true,
195 sign_only_indices: None,
196 };
197
198 let result = estimate_bundle_fee(&rpc_client, request).await;
199
200 assert!(result.is_err(), "Should fail with invalid signer key");
201 let err = result.unwrap_err();
202 assert!(matches!(err, KoraError::ValidationError(_)));
203 }
204
205 #[tokio::test]
206 async fn test_estimate_bundle_fee_exactly_max_size() {
207 let _m = ConfigMockBuilder::new()
208 .with_bundle_enabled(true)
209 .with_usage_limit_enabled(false)
210 .build_and_setup();
211 let _ = setup_or_get_test_signer();
212 let _ = setup_or_get_test_usage_limiter().await;
213
214 let rpc_client =
215 Arc::new(RpcMockBuilder::new().with_fee_estimate(5000).with_simulation().build());
216
217 let transactions: Vec<String> = (0..5).map(|_| create_mock_encoded_transaction()).collect();
219
220 let request = EstimateBundleFeeRequest {
221 transactions,
222 fee_token: None,
223 signer_key: None,
224 sig_verify: true,
225 sign_only_indices: None,
226 };
227
228 let result = estimate_bundle_fee(&rpc_client, request).await;
229
230 assert!(result.is_ok(), "Should succeed with valid transactions");
231 let response = result.unwrap();
232 assert!(response.fee_in_lamports > 0);
233 assert!(!response.signer_pubkey.is_empty());
234 assert!(!response.payment_address.is_empty());
235 }
236
237 #[tokio::test]
238 async fn test_estimate_bundle_fee_single_transaction() {
239 let _m = ConfigMockBuilder::new()
240 .with_bundle_enabled(true)
241 .with_usage_limit_enabled(false)
242 .build_and_setup();
243 let _ = setup_or_get_test_signer();
244 let _ = setup_or_get_test_usage_limiter().await;
245
246 let rpc_client =
247 Arc::new(RpcMockBuilder::new().with_fee_estimate(5000).with_simulation().build());
248
249 let request = EstimateBundleFeeRequest {
251 transactions: vec![create_mock_encoded_transaction()],
252 fee_token: None,
253 signer_key: None,
254 sig_verify: true,
255 sign_only_indices: None,
256 };
257
258 let result = estimate_bundle_fee(&rpc_client, request).await;
259
260 assert!(result.is_ok(), "Should succeed with valid transaction");
261 let response = result.unwrap();
262 assert!(response.fee_in_lamports > 0);
263 assert!(!response.signer_pubkey.is_empty());
264 assert!(!response.payment_address.is_empty());
265 }
266
267 #[tokio::test]
268 async fn test_estimate_bundle_fee_sig_verify_default() {
269 let json = r#"{"transactions": ["tx1"]}"#;
271 let request: EstimateBundleFeeRequest = serde_json::from_str(json).unwrap();
272
273 assert!(!request.sig_verify, "sig_verify should default to false");
274 assert!(request.signer_key.is_none());
275 }
276
277 #[tokio::test]
278 async fn test_estimate_bundle_fee_request_deserialization() {
279 let json = r#"{
280 "transactions": ["tx1", "tx2"],
281 "fee_token": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
282 "signer_key": "11111111111111111111111111111111",
283 "sig_verify": false
284 }"#;
285 let request: EstimateBundleFeeRequest = serde_json::from_str(json).unwrap();
286
287 assert_eq!(request.transactions.len(), 2);
288 assert_eq!(
289 request.fee_token,
290 Some("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string())
291 );
292 assert_eq!(request.signer_key, Some("11111111111111111111111111111111".to_string()));
293 assert!(!request.sig_verify);
294 }
295}