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::tests::{
82 common::{setup_or_get_test_signer, RpcMockBuilder},
83 config_mock::ConfigMockBuilder,
84 };
85
86 #[tokio::test]
87 async fn test_sign_bundle_empty_bundle() {
88 let _m = ConfigMockBuilder::new().with_bundle_enabled(true).build_and_setup();
89 let _ = setup_or_get_test_signer();
90
91 let rpc_client = Arc::new(RpcMockBuilder::new().build());
92
93 let request =
94 SignBundleRequest { transactions: vec![], signer_key: None, sig_verify: true };
95
96 let result = sign_bundle(&rpc_client, request).await;
97
98 assert!(result.is_err(), "Should fail with empty bundle");
99 let err = result.unwrap_err();
100 assert!(matches!(err, KoraError::InvalidTransaction(_)));
101 }
102
103 #[tokio::test]
104 async fn test_sign_bundle_disabled() {
105 let _m = ConfigMockBuilder::new().with_bundle_enabled(false).build_and_setup();
106 let _ = setup_or_get_test_signer();
107
108 let rpc_client = Arc::new(RpcMockBuilder::new().build());
109
110 let request = SignBundleRequest {
111 transactions: vec!["some_tx".to_string()],
112 signer_key: None,
113 sig_verify: true,
114 };
115
116 let result = sign_bundle(&rpc_client, request).await;
117
118 assert!(result.is_err(), "Should fail when bundles disabled");
119 let err = result.unwrap_err();
120 assert!(matches!(err, KoraError::JitoError(_)));
121 if let KoraError::JitoError(msg) = err {
122 assert!(msg.contains("not enabled"));
123 }
124 }
125
126 #[tokio::test]
127 async fn test_sign_bundle_too_large() {
128 let _m = ConfigMockBuilder::new().with_bundle_enabled(true).build_and_setup();
129 let _ = setup_or_get_test_signer();
130
131 let rpc_client = Arc::new(RpcMockBuilder::new().build());
132
133 let request = SignBundleRequest {
134 transactions: vec!["tx".to_string(); 6],
135 signer_key: None,
136 sig_verify: true,
137 };
138
139 let result = sign_bundle(&rpc_client, request).await;
140
141 assert!(result.is_err(), "Should fail with too many transactions");
142 let err = result.unwrap_err();
143 assert!(matches!(err, KoraError::JitoError(_)));
144 if let KoraError::JitoError(msg) = err {
145 assert!(msg.contains("maximum size"));
146 }
147 }
148
149 #[tokio::test]
150 async fn test_sign_bundle_invalid_signer_key() {
151 let _m = ConfigMockBuilder::new().with_bundle_enabled(true).build_and_setup();
152 let _ = setup_or_get_test_signer();
153
154 let rpc_client = Arc::new(RpcMockBuilder::new().build());
155
156 let request = SignBundleRequest {
157 transactions: vec!["some_tx".to_string()],
158 signer_key: Some("invalid_pubkey".to_string()),
159 sig_verify: true,
160 };
161
162 let result = sign_bundle(&rpc_client, request).await;
163
164 assert!(result.is_err(), "Should fail with invalid signer key");
165 let err = result.unwrap_err();
166 assert!(matches!(err, KoraError::ValidationError(_)));
167 }
168
169 #[tokio::test]
170 async fn test_sign_bundle_exactly_max_size() {
171 let _m = ConfigMockBuilder::new().with_bundle_enabled(true).build_and_setup();
172 let _ = setup_or_get_test_signer();
173
174 let rpc_client = Arc::new(RpcMockBuilder::new().build());
175
176 let request = SignBundleRequest {
178 transactions: vec!["tx".to_string(); 5],
179 signer_key: None,
180 sig_verify: true,
181 };
182
183 let result = sign_bundle(&rpc_client, request).await;
184
185 assert!(result.is_err());
188 if let KoraError::JitoError(msg) = &result.unwrap_err() {
190 assert!(
191 !msg.contains("maximum size"),
192 "5 transactions should not fail size validation"
193 );
194 }
195 }
196
197 #[tokio::test]
198 async fn test_sign_bundle_single_transaction() {
199 let _m = ConfigMockBuilder::new().with_bundle_enabled(true).build_and_setup();
200 let _ = setup_or_get_test_signer();
201
202 let rpc_client = Arc::new(RpcMockBuilder::new().build());
203
204 let request = SignBundleRequest {
206 transactions: vec!["tx".to_string()],
207 signer_key: None,
208 sig_verify: true,
209 };
210
211 let result = sign_bundle(&rpc_client, request).await;
212
213 assert!(result.is_err());
215 let err = result.unwrap_err();
217 assert!(!matches!(err, KoraError::InvalidTransaction(ref msg) if msg.contains("empty")));
218 }
219
220 #[tokio::test]
221 async fn test_sign_bundle_sig_verify_default() {
222 let json = r#"{"transactions": ["tx1"]}"#;
224 let request: SignBundleRequest = serde_json::from_str(json).unwrap();
225
226 assert!(!request.sig_verify, "sig_verify should default to false");
227 assert!(request.signer_key.is_none());
228 }
229
230 #[tokio::test]
231 async fn test_sign_bundle_request_deserialization() {
232 let json = r#"{
233 "transactions": ["tx1", "tx2"],
234 "signer_key": "11111111111111111111111111111111",
235 "sig_verify": false
236 }"#;
237 let request: SignBundleRequest = serde_json::from_str(json).unwrap();
238
239 assert_eq!(request.transactions.len(), 2);
240 assert_eq!(request.signer_key, Some("11111111111111111111111111111111".to_string()));
241 assert!(!request.sig_verify);
242 }
243}