kora_lib/rpc_server/method/
sign_bundle.rs1use crate::{
2 bundle::{BundleError, BundleProcessingMode, BundleProcessor, JitoError},
3 rpc_server::middleware_utils::default_sig_verify,
4 transaction::TransactionUtil,
5 validator::bundle_validator::BundleValidator,
6 KoraError,
7};
8use serde::{Deserialize, Serialize};
9use solana_client::nonblocking::rpc_client::RpcClient;
10use solana_keychain::SolanaSigner;
11use std::sync::Arc;
12use utoipa::ToSchema;
13
14#[cfg(not(test))]
15use crate::state::{get_config, get_request_signer_with_signer_key};
16
17#[cfg(test)]
18use crate::state::get_request_signer_with_signer_key;
19#[cfg(test)]
20use crate::tests::config_mock::mock_state::get_config;
21
22#[derive(Debug, Deserialize, ToSchema)]
23pub struct SignBundleRequest {
24 pub transactions: Vec<String>,
26 #[serde(default, skip_serializing_if = "Option::is_none")]
28 pub signer_key: Option<String>,
29 #[serde(default = "default_sig_verify")]
31 pub sig_verify: bool,
32 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub user_id: Option<String>,
35 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub sign_only_indices: Option<Vec<usize>>,
38}
39
40#[derive(Debug, Serialize, ToSchema)]
41pub struct SignBundleResponse {
42 pub signed_transactions: Vec<String>,
44 pub signer_pubkey: String,
46}
47
48pub async fn sign_bundle(
49 rpc_client: &Arc<RpcClient>,
50 request: SignBundleRequest,
51) -> Result<SignBundleResponse, 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::CheckUsage(request.user_id.as_deref()),
80 )
81 .await?;
82
83 let signed_resolved = processor.sign_all(&signer, &fee_payer, rpc_client).await?;
84
85 let encoded_signed: Vec<String> = signed_resolved
87 .iter()
88 .map(|r| TransactionUtil::encode_versioned_transaction(&r.transaction))
89 .collect::<Result<Vec<_>, _>>()?;
90
91 let signed_transactions = BundleProcessor::merge_signed_transactions(
93 &request.transactions,
94 encoded_signed,
95 &index_to_position,
96 );
97
98 Ok(SignBundleResponse { signed_transactions, signer_pubkey: fee_payer.to_string() })
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104 use crate::{
105 fee::price::{PriceConfig, PriceModel},
106 tests::{
107 common::{setup_or_get_test_signer, setup_or_get_test_usage_limiter, RpcMockBuilder},
108 config_mock::{ConfigMockBuilder, ValidationConfigBuilder},
109 },
110 transaction::TransactionUtil,
111 };
112 use solana_message::{Message, VersionedMessage};
113 use solana_sdk::pubkey::Pubkey;
114 use solana_system_interface::instruction::transfer;
115
116 #[tokio::test]
117 async fn test_sign_bundle_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 = SignBundleRequest {
124 transactions: vec![],
125 signer_key: None,
126 sig_verify: true,
127 user_id: None,
128 sign_only_indices: None,
129 };
130
131 let result = sign_bundle(&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_sign_bundle_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 = SignBundleRequest {
146 transactions: vec!["some_tx".to_string()],
147 signer_key: None,
148 sig_verify: true,
149 user_id: None,
150 sign_only_indices: None,
151 };
152
153 let result = sign_bundle(&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_sign_bundle_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 = SignBundleRequest {
171 transactions: vec!["tx".to_string(); 6],
172 signer_key: None,
173 sig_verify: true,
174 user_id: None,
175 sign_only_indices: None,
176 };
177
178 let result = sign_bundle(&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_sign_bundle_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 = SignBundleRequest {
196 transactions: vec!["some_tx".to_string()],
197 signer_key: Some("invalid_pubkey".to_string()),
198 sig_verify: true,
199 user_id: None,
200 sign_only_indices: None,
201 };
202
203 let result = sign_bundle(&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_sign_bundle_exactly_max_size() {
212 let mut validation = ValidationConfigBuilder::new()
213 .with_allowed_programs(vec!["11111111111111111111111111111111".to_string()])
214 .build();
215 validation.price = PriceConfig { model: PriceModel::Free };
216 let _m = ConfigMockBuilder::new()
217 .with_bundle_enabled(true)
218 .with_usage_limit_enabled(false)
219 .with_validation(validation)
220 .build_and_setup();
221 let signer_pubkey = setup_or_get_test_signer();
222 let _ = setup_or_get_test_usage_limiter().await;
223
224 let rpc_client = Arc::new(
225 RpcMockBuilder::new()
226 .with_fee_estimate(5000)
227 .with_blockhash()
228 .with_simulation()
229 .build(),
230 );
231
232 let transactions: Vec<String> = (0..5)
235 .map(|_| {
236 let ix = transfer(&Pubkey::new_unique(), &Pubkey::new_unique(), 1000000000);
237 let message = VersionedMessage::Legacy(Message::new(&[ix], Some(&signer_pubkey)));
238 let transaction = TransactionUtil::new_unsigned_versioned_transaction(message);
239 TransactionUtil::encode_versioned_transaction(&transaction).unwrap()
240 })
241 .collect();
242
243 let request = SignBundleRequest {
245 transactions,
246 signer_key: Some(signer_pubkey.to_string()),
247 sig_verify: true,
248 user_id: None,
249 sign_only_indices: None,
250 };
251
252 let result = sign_bundle(&rpc_client, request).await;
253
254 assert!(result.is_ok(), "Should succeed with valid transactions");
255 let response = result.unwrap();
256 assert_eq!(response.signed_transactions.len(), 5);
257 assert!(!response.signer_pubkey.is_empty());
258 }
259
260 #[tokio::test]
261 async fn test_sign_bundle_single_transaction() {
262 let mut validation = ValidationConfigBuilder::new()
263 .with_allowed_programs(vec!["11111111111111111111111111111111".to_string()])
264 .build();
265 validation.price = PriceConfig { model: PriceModel::Free };
266 let _m = ConfigMockBuilder::new()
267 .with_bundle_enabled(true)
268 .with_usage_limit_enabled(false)
269 .with_validation(validation)
270 .build_and_setup();
271 let signer_pubkey = setup_or_get_test_signer();
272 let _ = setup_or_get_test_usage_limiter().await;
273
274 let rpc_client = Arc::new(
275 RpcMockBuilder::new()
276 .with_fee_estimate(5000)
277 .with_blockhash()
278 .with_simulation()
279 .build(),
280 );
281
282 let ix = transfer(&Pubkey::new_unique(), &Pubkey::new_unique(), 1000000000);
284 let message = VersionedMessage::Legacy(Message::new(&[ix], Some(&signer_pubkey)));
285 let transaction = TransactionUtil::new_unsigned_versioned_transaction(message);
286 let encoded_tx = TransactionUtil::encode_versioned_transaction(&transaction).unwrap();
287
288 let request = SignBundleRequest {
290 transactions: vec![encoded_tx],
291 signer_key: Some(signer_pubkey.to_string()),
292 sig_verify: true,
293 user_id: None,
294 sign_only_indices: None,
295 };
296
297 let result = sign_bundle(&rpc_client, request).await;
298
299 assert!(result.is_ok(), "Should succeed with valid transaction");
300 let response = result.unwrap();
301 assert_eq!(response.signed_transactions.len(), 1);
302 assert!(!response.signer_pubkey.is_empty());
303 }
304
305 #[tokio::test]
306 async fn test_sign_bundle_sig_verify_default() {
307 let json = r#"{"transactions": ["tx1"]}"#;
309 let request: SignBundleRequest = serde_json::from_str(json).unwrap();
310
311 assert!(!request.sig_verify, "sig_verify should default to false");
312 assert!(request.signer_key.is_none());
313 }
314
315 #[tokio::test]
316 async fn test_sign_bundle_request_deserialization() {
317 let json = r#"{
318 "transactions": ["tx1", "tx2"],
319 "signer_key": "11111111111111111111111111111111",
320 "sig_verify": false,
321 "user_id": "test-user-456"
322 }"#;
323 let request: SignBundleRequest = serde_json::from_str(json).unwrap();
324
325 assert_eq!(request.transactions.len(), 2);
326 assert_eq!(request.signer_key, Some("11111111111111111111111111111111".to_string()));
327 assert!(!request.sig_verify);
328 assert_eq!(request.user_id, Some("test-user-456".to_string()));
329 assert!(request.sign_only_indices.is_none());
330 }
331
332 #[tokio::test]
333 async fn test_sign_bundle_request_deserialization_with_sign_only_indices() {
334 let json = r#"{
335 "transactions": ["tx1", "tx2", "tx3"],
336 "signer_key": "11111111111111111111111111111111",
337 "sig_verify": false,
338 "sign_only_indices": [0, 2]
339 }"#;
340 let request: SignBundleRequest = serde_json::from_str(json).unwrap();
341
342 assert_eq!(request.transactions.len(), 3);
343 assert_eq!(request.signer_key, Some("11111111111111111111111111111111".to_string()));
344 assert!(!request.sig_verify);
345 assert_eq!(request.sign_only_indices, Some(vec![0, 2]));
346 }
347}