kora_lib/rpc_server/method/
sign_and_send_bundle.rs1use crate::{
2 bundle::{BundleError, BundleProcessingMode, BundleProcessor, JitoBundleClient, 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 SignAndSendBundleRequest {
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 SignAndSendBundleResponse {
42 pub signed_transactions: Vec<String>,
44 pub signer_pubkey: String,
46 pub bundle_uuid: String,
48}
49
50pub async fn sign_and_send_bundle(
51 rpc_client: &Arc<RpcClient>,
52 request: SignAndSendBundleRequest,
53) -> Result<SignAndSendBundleResponse, KoraError> {
54 let config = &get_config()?;
55
56 if !config.kora.bundle.enabled {
57 return Err(BundleError::Jito(JitoError::NotEnabled).into());
58 }
59
60 BundleValidator::validate_jito_bundle_size(&request.transactions)?;
62
63 let (transactions_to_process, index_to_position) =
65 BundleProcessor::extract_transactions_to_process(
66 &request.transactions,
67 request.sign_only_indices,
68 )?;
69
70 let signer = get_request_signer_with_signer_key(request.signer_key.as_deref())?;
71 let fee_payer = signer.pubkey();
72 let payment_destination = config.kora.get_payment_address(&fee_payer)?;
73
74 let processor = BundleProcessor::process_bundle(
75 &transactions_to_process,
76 fee_payer,
77 &payment_destination,
78 config,
79 rpc_client,
80 request.sig_verify,
81 BundleProcessingMode::CheckUsage(request.user_id.as_deref()),
82 )
83 .await?;
84
85 let signed_resolved = processor.sign_all(&signer, &fee_payer, rpc_client).await?;
86
87 let jito_client = JitoBundleClient::new(&config.kora.bundle.jito);
89 let bundle_uuid = jito_client.send_bundle(&signed_resolved).await?;
90
91 let encoded_signed: Vec<String> = signed_resolved
93 .iter()
94 .map(|r| TransactionUtil::encode_versioned_transaction(&r.transaction))
95 .collect::<Result<Vec<_>, _>>()?;
96
97 let signed_transactions = BundleProcessor::merge_signed_transactions(
99 &request.transactions,
100 encoded_signed,
101 &index_to_position,
102 );
103
104 Ok(SignAndSendBundleResponse {
105 signed_transactions,
106 signer_pubkey: fee_payer.to_string(),
107 bundle_uuid,
108 })
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114 use crate::tests::{
115 common::{setup_or_get_test_signer, RpcMockBuilder},
116 config_mock::ConfigMockBuilder,
117 };
118
119 #[tokio::test]
120 async fn test_sign_and_send_bundle_empty_bundle() {
121 let _m = ConfigMockBuilder::new().with_bundle_enabled(true).build_and_setup();
122 let _ = setup_or_get_test_signer();
123
124 let rpc_client = Arc::new(RpcMockBuilder::new().build());
125
126 let request = SignAndSendBundleRequest {
127 transactions: vec![],
128 signer_key: None,
129 sig_verify: true,
130 user_id: None,
131 sign_only_indices: None,
132 };
133
134 let result = sign_and_send_bundle(&rpc_client, request).await;
135
136 assert!(result.is_err(), "Should fail with empty bundle");
137 let err = result.unwrap_err();
138 assert!(matches!(err, KoraError::InvalidTransaction(_)));
139 }
140
141 #[tokio::test]
142 async fn test_sign_and_send_bundle_disabled() {
143 let _m = ConfigMockBuilder::new().with_bundle_enabled(false).build_and_setup();
144 let _ = setup_or_get_test_signer();
145
146 let rpc_client = Arc::new(RpcMockBuilder::new().build());
147
148 let request = SignAndSendBundleRequest {
149 transactions: vec!["some_tx".to_string()],
150 signer_key: None,
151 sig_verify: true,
152 user_id: None,
153 sign_only_indices: None,
154 };
155
156 let result = sign_and_send_bundle(&rpc_client, request).await;
157
158 assert!(result.is_err(), "Should fail when bundles disabled");
159 let err = result.unwrap_err();
160 assert!(matches!(err, KoraError::JitoError(_)));
161 if let KoraError::JitoError(msg) = err {
162 assert!(msg.contains("not enabled"));
163 }
164 }
165
166 #[tokio::test]
167 async fn test_sign_and_send_bundle_too_large() {
168 let _m = ConfigMockBuilder::new().with_bundle_enabled(true).build_and_setup();
169 let _ = setup_or_get_test_signer();
170
171 let rpc_client = Arc::new(RpcMockBuilder::new().build());
172
173 let request = SignAndSendBundleRequest {
174 transactions: vec!["tx".to_string(); 6],
175 signer_key: None,
176 sig_verify: true,
177 user_id: None,
178 sign_only_indices: None,
179 };
180
181 let result = sign_and_send_bundle(&rpc_client, request).await;
182
183 assert!(result.is_err(), "Should fail with too many transactions");
184 let err = result.unwrap_err();
185 assert!(matches!(err, KoraError::JitoError(_)));
186 if let KoraError::JitoError(msg) = err {
187 assert!(msg.contains("maximum size"));
188 }
189 }
190
191 #[tokio::test]
192 async fn test_sign_and_send_bundle_invalid_signer_key() {
193 let _m = ConfigMockBuilder::new().with_bundle_enabled(true).build_and_setup();
194 let _ = setup_or_get_test_signer();
195
196 let rpc_client = Arc::new(RpcMockBuilder::new().build());
197
198 let request = SignAndSendBundleRequest {
199 transactions: vec!["some_tx".to_string()],
200 signer_key: Some("invalid_pubkey".to_string()),
201 sig_verify: true,
202 user_id: None,
203 sign_only_indices: None,
204 };
205
206 let result = sign_and_send_bundle(&rpc_client, request).await;
207
208 assert!(result.is_err(), "Should fail with invalid signer key");
209 let err = result.unwrap_err();
210 assert!(matches!(err, KoraError::ValidationError(_)));
211 }
212
213 #[tokio::test]
214 async fn test_sign_and_send_bundle_request_deserialization() {
215 let json = r#"{
216 "transactions": ["tx1", "tx2", "tx3"],
217 "signer_key": "11111111111111111111111111111111",
218 "sig_verify": false,
219 "user_id": "test-user-123"
220 }"#;
221 let request: SignAndSendBundleRequest = serde_json::from_str(json).unwrap();
222
223 assert_eq!(request.transactions.len(), 3);
224 assert_eq!(request.signer_key, Some("11111111111111111111111111111111".to_string()));
225 assert!(!request.sig_verify);
226 assert_eq!(request.user_id, Some("test-user-123".to_string()));
227 assert!(request.sign_only_indices.is_none());
228 }
229
230 #[tokio::test]
231 async fn test_sign_and_send_bundle_request_deserialization_with_sign_only_indices() {
232 let json = r#"{
233 "transactions": ["tx1", "tx2", "tx3"],
234 "signer_key": "11111111111111111111111111111111",
235 "sig_verify": false,
236 "sign_only_indices": [1, 2]
237 }"#;
238 let request: SignAndSendBundleRequest = serde_json::from_str(json).unwrap();
239
240 assert_eq!(request.transactions.len(), 3);
241 assert_eq!(request.signer_key, Some("11111111111111111111111111111111".to_string()));
242 assert!(!request.sig_verify);
243 assert_eq!(request.sign_only_indices, Some(vec![1, 2]));
244 }
245
246 #[tokio::test]
247 async fn test_sign_and_send_bundle_sig_verify_default() {
248 let json = r#"{"transactions": ["tx1"]}"#;
250 let request: SignAndSendBundleRequest = serde_json::from_str(json).unwrap();
251
252 assert!(!request.sig_verify, "sig_verify should default to false");
253 assert!(request.signer_key.is_none());
254 }
255
256 #[test]
257 fn test_sign_and_send_bundle_response_serialization() {
258 let response = SignAndSendBundleResponse {
259 signed_transactions: vec!["signed_tx1".to_string(), "signed_tx2".to_string()],
260 signer_pubkey: "11111111111111111111111111111111".to_string(),
261 bundle_uuid: "bundle-uuid-12345".to_string(),
262 };
263
264 let json = serde_json::to_string(&response).unwrap();
265
266 assert!(json.contains("signed_transactions"));
267 assert!(json.contains("signer_pubkey"));
268 assert!(json.contains("bundle_uuid"));
269 assert!(json.contains("bundle-uuid-12345"));
270 }
271}