kora_lib/rpc_server/method/
sign_and_send_bundle.rs1use crate::{
2 bundle::{BundleError, 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}
33
34#[derive(Debug, Serialize, ToSchema)]
35pub struct SignAndSendBundleResponse {
36 pub signed_transactions: Vec<String>,
38 pub signer_pubkey: String,
40 pub bundle_uuid: String,
42}
43
44pub async fn sign_and_send_bundle(
45 rpc_client: &Arc<RpcClient>,
46 request: SignAndSendBundleRequest,
47) -> Result<SignAndSendBundleResponse, KoraError> {
48 let config = &get_config()?;
49
50 if !config.kora.bundle.enabled {
51 return Err(BundleError::Jito(JitoError::NotEnabled).into());
52 }
53
54 BundleValidator::validate_jito_bundle_size(&request.transactions)?;
55
56 let signer = get_request_signer_with_signer_key(request.signer_key.as_deref())?;
57 let fee_payer = signer.pubkey();
58 let payment_destination = config.kora.get_payment_address(&fee_payer)?;
59
60 let processor = BundleProcessor::process_bundle(
61 &request.transactions,
62 fee_payer,
63 &payment_destination,
64 config,
65 rpc_client,
66 request.sig_verify,
67 )
68 .await?;
69
70 let signed_resolved = processor.sign_all(&signer, &fee_payer, rpc_client).await?;
71
72 let jito_client = JitoBundleClient::new(&config.kora.bundle.jito);
74 let bundle_uuid = jito_client.send_bundle(&signed_resolved).await?;
75
76 let signed_transactions = signed_resolved
78 .iter()
79 .map(|r| TransactionUtil::encode_versioned_transaction(&r.transaction))
80 .collect::<Result<Vec<_>, _>>()?;
81
82 Ok(SignAndSendBundleResponse {
83 signed_transactions,
84 signer_pubkey: fee_payer.to_string(),
85 bundle_uuid,
86 })
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92 use crate::tests::{
93 common::{setup_or_get_test_signer, RpcMockBuilder},
94 config_mock::ConfigMockBuilder,
95 };
96
97 #[tokio::test]
98 async fn test_sign_and_send_bundle_empty_bundle() {
99 let _m = ConfigMockBuilder::new().with_bundle_enabled(true).build_and_setup();
100 let _ = setup_or_get_test_signer();
101
102 let rpc_client = Arc::new(RpcMockBuilder::new().build());
103
104 let request =
105 SignAndSendBundleRequest { transactions: vec![], signer_key: None, sig_verify: true };
106
107 let result = sign_and_send_bundle(&rpc_client, request).await;
108
109 assert!(result.is_err(), "Should fail with empty bundle");
110 let err = result.unwrap_err();
111 assert!(matches!(err, KoraError::InvalidTransaction(_)));
112 }
113
114 #[tokio::test]
115 async fn test_sign_and_send_bundle_disabled() {
116 let _m = ConfigMockBuilder::new().with_bundle_enabled(false).build_and_setup();
117 let _ = setup_or_get_test_signer();
118
119 let rpc_client = Arc::new(RpcMockBuilder::new().build());
120
121 let request = SignAndSendBundleRequest {
122 transactions: vec!["some_tx".to_string()],
123 signer_key: None,
124 sig_verify: true,
125 };
126
127 let result = sign_and_send_bundle(&rpc_client, request).await;
128
129 assert!(result.is_err(), "Should fail when bundles disabled");
130 let err = result.unwrap_err();
131 assert!(matches!(err, KoraError::JitoError(_)));
132 if let KoraError::JitoError(msg) = err {
133 assert!(msg.contains("not enabled"));
134 }
135 }
136
137 #[tokio::test]
138 async fn test_sign_and_send_bundle_too_large() {
139 let _m = ConfigMockBuilder::new().with_bundle_enabled(true).build_and_setup();
140 let _ = setup_or_get_test_signer();
141
142 let rpc_client = Arc::new(RpcMockBuilder::new().build());
143
144 let request = SignAndSendBundleRequest {
145 transactions: vec!["tx".to_string(); 6],
146 signer_key: None,
147 sig_verify: true,
148 };
149
150 let result = sign_and_send_bundle(&rpc_client, request).await;
151
152 assert!(result.is_err(), "Should fail with too many transactions");
153 let err = result.unwrap_err();
154 assert!(matches!(err, KoraError::JitoError(_)));
155 if let KoraError::JitoError(msg) = err {
156 assert!(msg.contains("maximum size"));
157 }
158 }
159
160 #[tokio::test]
161 async fn test_sign_and_send_bundle_invalid_signer_key() {
162 let _m = ConfigMockBuilder::new().with_bundle_enabled(true).build_and_setup();
163 let _ = setup_or_get_test_signer();
164
165 let rpc_client = Arc::new(RpcMockBuilder::new().build());
166
167 let request = SignAndSendBundleRequest {
168 transactions: vec!["some_tx".to_string()],
169 signer_key: Some("invalid_pubkey".to_string()),
170 sig_verify: true,
171 };
172
173 let result = sign_and_send_bundle(&rpc_client, request).await;
174
175 assert!(result.is_err(), "Should fail with invalid signer key");
176 let err = result.unwrap_err();
177 assert!(matches!(err, KoraError::ValidationError(_)));
178 }
179
180 #[tokio::test]
181 async fn test_sign_and_send_bundle_request_deserialization() {
182 let json = r#"{
183 "transactions": ["tx1", "tx2", "tx3"],
184 "signer_key": "11111111111111111111111111111111",
185 "sig_verify": false
186 }"#;
187 let request: SignAndSendBundleRequest = serde_json::from_str(json).unwrap();
188
189 assert_eq!(request.transactions.len(), 3);
190 assert_eq!(request.signer_key, Some("11111111111111111111111111111111".to_string()));
191 assert!(!request.sig_verify);
192 }
193
194 #[tokio::test]
195 async fn test_sign_and_send_bundle_sig_verify_default() {
196 let json = r#"{"transactions": ["tx1"]}"#;
198 let request: SignAndSendBundleRequest = serde_json::from_str(json).unwrap();
199
200 assert!(!request.sig_verify, "sig_verify should default to false");
201 assert!(request.signer_key.is_none());
202 }
203
204 #[test]
205 fn test_sign_and_send_bundle_response_serialization() {
206 let response = SignAndSendBundleResponse {
207 signed_transactions: vec!["signed_tx1".to_string(), "signed_tx2".to_string()],
208 signer_pubkey: "11111111111111111111111111111111".to_string(),
209 bundle_uuid: "bundle-uuid-12345".to_string(),
210 };
211
212 let json = serde_json::to_string(&response).unwrap();
213
214 assert!(json.contains("signed_transactions"));
215 assert!(json.contains("signer_pubkey"));
216 assert!(json.contains("bundle_uuid"));
217 assert!(json.contains("bundle-uuid-12345"));
218 }
219}