kora_lib/rpc_server/method/
sign_bundle.rs1use crate::{
2 bundle::{BundleError, BundleProcessingMode, BundleProcessor, JitoError},
3 plugin::PluginExecutionContext,
4 rpc_server::middleware_utils::default_sig_verify,
5 transaction::TransactionUtil,
6 validator::bundle_validator::BundleValidator,
7 KoraError,
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, get_request_signer_with_signer_key};
17
18#[cfg(test)]
19use crate::state::get_request_signer_with_signer_key;
20#[cfg(test)]
21use crate::tests::config_mock::mock_state::get_config;
22
23#[derive(Debug, Deserialize, ToSchema)]
24pub struct SignBundleRequest {
25 pub transactions: Vec<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 user_id: Option<String>,
36 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub sign_only_indices: Option<Vec<usize>>,
39}
40
41#[derive(Debug, Serialize, ToSchema)]
42pub struct SignBundleResponse {
43 pub signed_transactions: Vec<String>,
45 pub signer_pubkey: String,
47}
48
49pub async fn sign_bundle(
50 rpc_client: &Arc<RpcClient>,
51 request: SignBundleRequest,
52) -> Result<SignBundleResponse, KoraError> {
53 let config = &get_config()?;
54
55 if !config.kora.bundle.enabled {
56 return Err(BundleError::Jito(JitoError::NotEnabled).into());
57 }
58
59 BundleValidator::validate_jito_bundle_size(&request.transactions)?;
61
62 let (transactions_to_process, index_to_position) =
64 BundleProcessor::extract_transactions_to_process(
65 &request.transactions,
66 request.sign_only_indices,
67 )?;
68
69 let signer = get_request_signer_with_signer_key(request.signer_key.as_deref())?;
70 let fee_payer = signer.pubkey();
71 let payment_destination = config.kora.get_payment_address(&fee_payer)?;
72
73 let sig_verify = request.sig_verify || config.kora.force_sig_verify;
74 let processor = BundleProcessor::process_bundle(
75 &transactions_to_process,
76 fee_payer,
77 &payment_destination,
78 config,
79 rpc_client,
80 sig_verify,
81 Some(PluginExecutionContext::SignBundle),
82 BundleProcessingMode::CheckUsage(request.user_id.as_deref()),
83 )
84 .await?;
85
86 let signed_resolved =
87 processor.sign_all(&signer, &fee_payer, rpc_client, config, false).await?;
88
89 let encoded_signed: Vec<String> = signed_resolved
91 .iter()
92 .map(|r| TransactionUtil::encode_versioned_transaction(&r.transaction))
93 .collect::<Result<Vec<_>, _>>()?;
94
95 let signed_transactions = BundleProcessor::merge_signed_transactions(
97 &request.transactions,
98 encoded_signed,
99 &index_to_position,
100 );
101
102 Ok(SignBundleResponse { signed_transactions, signer_pubkey: fee_payer.to_string() })
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use crate::{
109 fee::price::{PriceConfig, PriceModel},
110 tests::{
111 common::{setup_or_get_test_signer, setup_or_get_test_usage_limiter, RpcMockBuilder},
112 config_mock::{ConfigMockBuilder, ValidationConfigBuilder},
113 },
114 transaction::TransactionUtil,
115 };
116 use solana_message::{Message, VersionedMessage};
117 use solana_sdk::pubkey::Pubkey;
118 use solana_system_interface::instruction::transfer;
119
120 #[tokio::test]
121 async fn test_sign_bundle_empty_bundle() {
122 let _m = ConfigMockBuilder::new().with_bundle_enabled(true).build_and_setup();
123 let _ = setup_or_get_test_signer();
124
125 let rpc_client = Arc::new(RpcMockBuilder::new().build());
126
127 let request = SignBundleRequest {
128 transactions: vec![],
129 signer_key: None,
130 sig_verify: true,
131 user_id: None,
132 sign_only_indices: None,
133 };
134
135 let result = sign_bundle(&rpc_client, request).await;
136
137 assert!(result.is_err(), "Should fail with empty bundle");
138 let err = result.unwrap_err();
139 assert!(matches!(err, KoraError::InvalidTransaction(_)));
140 }
141
142 #[tokio::test]
143 async fn test_sign_bundle_disabled() {
144 let _m = ConfigMockBuilder::new().with_bundle_enabled(false).build_and_setup();
145 let _ = setup_or_get_test_signer();
146
147 let rpc_client = Arc::new(RpcMockBuilder::new().build());
148
149 let request = SignBundleRequest {
150 transactions: vec!["some_tx".to_string()],
151 signer_key: None,
152 sig_verify: true,
153 user_id: None,
154 sign_only_indices: None,
155 };
156
157 let result = sign_bundle(&rpc_client, request).await;
158
159 assert!(result.is_err(), "Should fail when bundles disabled");
160 let err = result.unwrap_err();
161 assert!(matches!(err, KoraError::JitoError(_)));
162 if let KoraError::JitoError(msg) = err {
163 assert!(msg.contains("not enabled"));
164 }
165 }
166
167 #[tokio::test]
168 async fn test_sign_bundle_too_large() {
169 let _m = ConfigMockBuilder::new().with_bundle_enabled(true).build_and_setup();
170 let _ = setup_or_get_test_signer();
171
172 let rpc_client = Arc::new(RpcMockBuilder::new().build());
173
174 let request = SignBundleRequest {
175 transactions: vec!["tx".to_string(); 6],
176 signer_key: None,
177 sig_verify: true,
178 user_id: None,
179 sign_only_indices: None,
180 };
181
182 let result = sign_bundle(&rpc_client, request).await;
183
184 assert!(result.is_err(), "Should fail with too many transactions");
185 let err = result.unwrap_err();
186 assert!(matches!(err, KoraError::JitoError(_)));
187 if let KoraError::JitoError(msg) = err {
188 assert!(msg.contains("maximum size"));
189 }
190 }
191
192 #[tokio::test]
193 async fn test_sign_bundle_invalid_signer_key() {
194 let _m = ConfigMockBuilder::new().with_bundle_enabled(true).build_and_setup();
195 let _ = setup_or_get_test_signer();
196
197 let rpc_client = Arc::new(RpcMockBuilder::new().build());
198
199 let request = SignBundleRequest {
200 transactions: vec!["some_tx".to_string()],
201 signer_key: Some("invalid_pubkey".to_string()),
202 sig_verify: true,
203 user_id: None,
204 sign_only_indices: None,
205 };
206
207 let result = sign_bundle(&rpc_client, request).await;
208
209 assert!(result.is_err(), "Should fail with invalid signer key");
210 let err = result.unwrap_err();
211 assert!(matches!(err, KoraError::ValidationError(_)));
212 }
213
214 #[tokio::test]
215 async fn test_sign_bundle_exactly_max_size() {
216 let mut validation = ValidationConfigBuilder::new()
217 .with_allowed_programs(vec!["11111111111111111111111111111111".to_string()])
218 .build();
219 validation.price = PriceConfig { model: PriceModel::Free };
220 let _m = ConfigMockBuilder::new()
221 .with_bundle_enabled(true)
222 .with_usage_limit_enabled(false)
223 .with_validation(validation)
224 .build_and_setup();
225 let signer_pubkey = setup_or_get_test_signer();
226 let _ = setup_or_get_test_usage_limiter().await;
227
228 let rpc_client = Arc::new(
229 RpcMockBuilder::new()
230 .with_fee_estimate(5000)
231 .with_blockhash()
232 .with_simulation()
233 .build(),
234 );
235
236 let transactions: Vec<String> = (0..5)
239 .map(|_| {
240 let ix = transfer(&Pubkey::new_unique(), &Pubkey::new_unique(), 1000000000);
241 let message = VersionedMessage::Legacy(Message::new(&[ix], Some(&signer_pubkey)));
242 let transaction = TransactionUtil::new_unsigned_versioned_transaction(message);
243 TransactionUtil::encode_versioned_transaction(&transaction).unwrap()
244 })
245 .collect();
246
247 let request = SignBundleRequest {
249 transactions,
250 signer_key: Some(signer_pubkey.to_string()),
251 sig_verify: true,
252 user_id: None,
253 sign_only_indices: None,
254 };
255
256 let result = sign_bundle(&rpc_client, request).await;
257
258 assert!(result.is_ok(), "Should succeed with valid transactions");
259 let response = result.unwrap();
260 assert_eq!(response.signed_transactions.len(), 5);
261 assert!(!response.signer_pubkey.is_empty());
262 }
263
264 #[tokio::test]
265 async fn test_sign_bundle_single_transaction() {
266 let mut validation = ValidationConfigBuilder::new()
267 .with_allowed_programs(vec!["11111111111111111111111111111111".to_string()])
268 .build();
269 validation.price = PriceConfig { model: PriceModel::Free };
270 let _m = ConfigMockBuilder::new()
271 .with_bundle_enabled(true)
272 .with_usage_limit_enabled(false)
273 .with_validation(validation)
274 .build_and_setup();
275 let signer_pubkey = setup_or_get_test_signer();
276 let _ = setup_or_get_test_usage_limiter().await;
277
278 let rpc_client = Arc::new(
279 RpcMockBuilder::new()
280 .with_fee_estimate(5000)
281 .with_blockhash()
282 .with_simulation()
283 .build(),
284 );
285
286 let ix = transfer(&Pubkey::new_unique(), &Pubkey::new_unique(), 1000000000);
288 let message = VersionedMessage::Legacy(Message::new(&[ix], Some(&signer_pubkey)));
289 let transaction = TransactionUtil::new_unsigned_versioned_transaction(message);
290 let encoded_tx = TransactionUtil::encode_versioned_transaction(&transaction).unwrap();
291
292 let request = SignBundleRequest {
294 transactions: vec![encoded_tx],
295 signer_key: Some(signer_pubkey.to_string()),
296 sig_verify: true,
297 user_id: None,
298 sign_only_indices: None,
299 };
300
301 let result = sign_bundle(&rpc_client, request).await;
302
303 assert!(result.is_ok(), "Should succeed with valid transaction");
304 let response = result.unwrap();
305 assert_eq!(response.signed_transactions.len(), 1);
306 assert!(!response.signer_pubkey.is_empty());
307 }
308
309 #[tokio::test]
310 async fn test_sign_bundle_sig_verify_default() {
311 let json = r#"{"transactions": ["tx1"]}"#;
313 let request: SignBundleRequest = serde_json::from_str(json).unwrap();
314
315 assert!(!request.sig_verify, "sig_verify should default to false");
316 assert!(request.signer_key.is_none());
317 }
318
319 #[tokio::test]
320 async fn test_sign_bundle_request_deserialization() {
321 let json = r#"{
322 "transactions": ["tx1", "tx2"],
323 "signer_key": "11111111111111111111111111111111",
324 "sig_verify": false,
325 "user_id": "test-user-456"
326 }"#;
327 let request: SignBundleRequest = serde_json::from_str(json).unwrap();
328
329 assert_eq!(request.transactions.len(), 2);
330 assert_eq!(request.signer_key, Some("11111111111111111111111111111111".to_string()));
331 assert!(!request.sig_verify);
332 assert_eq!(request.user_id, Some("test-user-456".to_string()));
333 assert!(request.sign_only_indices.is_none());
334 }
335
336 #[tokio::test]
337 async fn test_sign_bundle_request_deserialization_with_sign_only_indices() {
338 let json = r#"{
339 "transactions": ["tx1", "tx2", "tx3"],
340 "signer_key": "11111111111111111111111111111111",
341 "sig_verify": false,
342 "sign_only_indices": [0, 2]
343 }"#;
344 let request: SignBundleRequest = serde_json::from_str(json).unwrap();
345
346 assert_eq!(request.transactions.len(), 3);
347 assert_eq!(request.signer_key, Some("11111111111111111111111111111111".to_string()));
348 assert!(!request.sig_verify);
349 assert_eq!(request.sign_only_indices, Some(vec![0, 2]));
350 }
351}