kora_lib/rpc_server/method/
sign_bundle.rs1use crate::{
2 bundle::{BundleError, 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}
33
34#[derive(Debug, Serialize, ToSchema)]
35pub struct SignBundleResponse {
36 pub signed_transactions: Vec<String>,
38 pub signer_pubkey: String,
40}
41
42pub async fn sign_bundle(
43 rpc_client: &Arc<RpcClient>,
44 request: SignBundleRequest,
45) -> Result<SignBundleResponse, KoraError> {
46 let config = &get_config()?;
47
48 if !config.kora.bundle.enabled {
49 return Err(BundleError::Jito(JitoError::NotEnabled).into());
50 }
51
52 BundleValidator::validate_jito_bundle_size(&request.transactions)?;
53
54 let signer = get_request_signer_with_signer_key(request.signer_key.as_deref())?;
55 let fee_payer = signer.pubkey();
56 let payment_destination = config.kora.get_payment_address(&fee_payer)?;
57
58 let processor = BundleProcessor::process_bundle(
59 &request.transactions,
60 fee_payer,
61 &payment_destination,
62 config,
63 rpc_client,
64 request.sig_verify,
65 )
66 .await?;
67
68 let signed_resolved = processor.sign_all(&signer, &fee_payer, rpc_client).await?;
69
70 let signed_transactions = signed_resolved
71 .iter()
72 .map(|r| TransactionUtil::encode_versioned_transaction(&r.transaction))
73 .collect::<Result<Vec<_>, _>>()?;
74
75 Ok(SignBundleResponse { signed_transactions, signer_pubkey: fee_payer.to_string() })
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81 use crate::{
82 fee::price::{PriceConfig, PriceModel},
83 tests::{
84 common::{setup_or_get_test_signer, setup_or_get_test_usage_limiter, RpcMockBuilder},
85 config_mock::{ConfigMockBuilder, ValidationConfigBuilder},
86 },
87 transaction::TransactionUtil,
88 };
89 use solana_message::{Message, VersionedMessage};
90 use solana_sdk::pubkey::Pubkey;
91 use solana_system_interface::instruction::transfer;
92
93 #[tokio::test]
94 async fn test_sign_bundle_empty_bundle() {
95 let _m = ConfigMockBuilder::new().with_bundle_enabled(true).build_and_setup();
96 let _ = setup_or_get_test_signer();
97
98 let rpc_client = Arc::new(RpcMockBuilder::new().build());
99
100 let request =
101 SignBundleRequest { transactions: vec![], signer_key: None, sig_verify: true };
102
103 let result = sign_bundle(&rpc_client, request).await;
104
105 assert!(result.is_err(), "Should fail with empty bundle");
106 let err = result.unwrap_err();
107 assert!(matches!(err, KoraError::InvalidTransaction(_)));
108 }
109
110 #[tokio::test]
111 async fn test_sign_bundle_disabled() {
112 let _m = ConfigMockBuilder::new().with_bundle_enabled(false).build_and_setup();
113 let _ = setup_or_get_test_signer();
114
115 let rpc_client = Arc::new(RpcMockBuilder::new().build());
116
117 let request = SignBundleRequest {
118 transactions: vec!["some_tx".to_string()],
119 signer_key: None,
120 sig_verify: true,
121 };
122
123 let result = sign_bundle(&rpc_client, request).await;
124
125 assert!(result.is_err(), "Should fail when bundles disabled");
126 let err = result.unwrap_err();
127 assert!(matches!(err, KoraError::JitoError(_)));
128 if let KoraError::JitoError(msg) = err {
129 assert!(msg.contains("not enabled"));
130 }
131 }
132
133 #[tokio::test]
134 async fn test_sign_bundle_too_large() {
135 let _m = ConfigMockBuilder::new().with_bundle_enabled(true).build_and_setup();
136 let _ = setup_or_get_test_signer();
137
138 let rpc_client = Arc::new(RpcMockBuilder::new().build());
139
140 let request = SignBundleRequest {
141 transactions: vec!["tx".to_string(); 6],
142 signer_key: None,
143 sig_verify: true,
144 };
145
146 let result = sign_bundle(&rpc_client, request).await;
147
148 assert!(result.is_err(), "Should fail with too many transactions");
149 let err = result.unwrap_err();
150 assert!(matches!(err, KoraError::JitoError(_)));
151 if let KoraError::JitoError(msg) = err {
152 assert!(msg.contains("maximum size"));
153 }
154 }
155
156 #[tokio::test]
157 async fn test_sign_bundle_invalid_signer_key() {
158 let _m = ConfigMockBuilder::new().with_bundle_enabled(true).build_and_setup();
159 let _ = setup_or_get_test_signer();
160
161 let rpc_client = Arc::new(RpcMockBuilder::new().build());
162
163 let request = SignBundleRequest {
164 transactions: vec!["some_tx".to_string()],
165 signer_key: Some("invalid_pubkey".to_string()),
166 sig_verify: true,
167 };
168
169 let result = sign_bundle(&rpc_client, request).await;
170
171 assert!(result.is_err(), "Should fail with invalid signer key");
172 let err = result.unwrap_err();
173 assert!(matches!(err, KoraError::ValidationError(_)));
174 }
175
176 #[tokio::test]
177 async fn test_sign_bundle_exactly_max_size() {
178 let mut validation = ValidationConfigBuilder::new()
179 .with_allowed_programs(vec!["11111111111111111111111111111111".to_string()])
180 .build();
181 validation.price = PriceConfig { model: PriceModel::Free };
182 let _m = ConfigMockBuilder::new()
183 .with_bundle_enabled(true)
184 .with_usage_limit_enabled(false)
185 .with_validation(validation)
186 .build_and_setup();
187 let signer_pubkey = setup_or_get_test_signer();
188 let _ = setup_or_get_test_usage_limiter().await;
189
190 let rpc_client = Arc::new(
191 RpcMockBuilder::new()
192 .with_fee_estimate(5000)
193 .with_blockhash()
194 .with_simulation()
195 .build(),
196 );
197
198 let transactions: Vec<String> = (0..5)
201 .map(|_| {
202 let ix = transfer(&Pubkey::new_unique(), &Pubkey::new_unique(), 1000000000);
203 let message = VersionedMessage::Legacy(Message::new(&[ix], Some(&signer_pubkey)));
204 let transaction = TransactionUtil::new_unsigned_versioned_transaction(message);
205 TransactionUtil::encode_versioned_transaction(&transaction).unwrap()
206 })
207 .collect();
208
209 let request = SignBundleRequest {
211 transactions,
212 signer_key: Some(signer_pubkey.to_string()),
213 sig_verify: true,
214 };
215
216 let result = sign_bundle(&rpc_client, request).await;
217
218 assert!(result.is_ok(), "Should succeed with valid transactions");
219 let response = result.unwrap();
220 assert_eq!(response.signed_transactions.len(), 5);
221 assert!(!response.signer_pubkey.is_empty());
222 }
223
224 #[tokio::test]
225 async fn test_sign_bundle_single_transaction() {
226 let mut validation = ValidationConfigBuilder::new()
227 .with_allowed_programs(vec!["11111111111111111111111111111111".to_string()])
228 .build();
229 validation.price = PriceConfig { model: PriceModel::Free };
230 let _m = ConfigMockBuilder::new()
231 .with_bundle_enabled(true)
232 .with_usage_limit_enabled(false)
233 .with_validation(validation)
234 .build_and_setup();
235 let signer_pubkey = setup_or_get_test_signer();
236 let _ = setup_or_get_test_usage_limiter().await;
237
238 let rpc_client = Arc::new(
239 RpcMockBuilder::new()
240 .with_fee_estimate(5000)
241 .with_blockhash()
242 .with_simulation()
243 .build(),
244 );
245
246 let ix = transfer(&Pubkey::new_unique(), &Pubkey::new_unique(), 1000000000);
248 let message = VersionedMessage::Legacy(Message::new(&[ix], Some(&signer_pubkey)));
249 let transaction = TransactionUtil::new_unsigned_versioned_transaction(message);
250 let encoded_tx = TransactionUtil::encode_versioned_transaction(&transaction).unwrap();
251
252 let request = SignBundleRequest {
254 transactions: vec![encoded_tx],
255 signer_key: Some(signer_pubkey.to_string()),
256 sig_verify: true,
257 };
258
259 let result = sign_bundle(&rpc_client, request).await;
260
261 assert!(result.is_ok(), "Should succeed with valid transaction");
262 let response = result.unwrap();
263 assert_eq!(response.signed_transactions.len(), 1);
264 assert!(!response.signer_pubkey.is_empty());
265 }
266
267 #[tokio::test]
268 async fn test_sign_bundle_sig_verify_default() {
269 let json = r#"{"transactions": ["tx1"]}"#;
271 let request: SignBundleRequest = 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_sign_bundle_request_deserialization() {
279 let json = r#"{
280 "transactions": ["tx1", "tx2"],
281 "signer_key": "11111111111111111111111111111111",
282 "sig_verify": false
283 }"#;
284 let request: SignBundleRequest = serde_json::from_str(json).unwrap();
285
286 assert_eq!(request.transactions.len(), 2);
287 assert_eq!(request.signer_key, Some("11111111111111111111111111111111".to_string()));
288 assert!(!request.sig_verify);
289 }
290}