kora_lib/rpc_server/method/
sign_and_send_bundle.rs

1use 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    /// Array of base64-encoded transactions
25    pub transactions: Vec<String>,
26    /// Optional signer key to ensure consistency across related RPC calls
27    #[serde(default, skip_serializing_if = "Option::is_none")]
28    pub signer_key: Option<String>,
29    /// Whether to verify signatures during simulation (defaults to false)
30    #[serde(default = "default_sig_verify")]
31    pub sig_verify: bool,
32    /// Optional user ID for usage tracking (required when pricing is free and usage tracking is enabled)
33    #[serde(default, skip_serializing_if = "Option::is_none")]
34    pub user_id: Option<String>,
35    /// Optional indices of transactions to sign (defaults to all if not specified)
36    #[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    /// Array of base64-encoded signed transactions
43    pub signed_transactions: Vec<String>,
44    /// Public key of the signer used (for client consistency)
45    pub signer_pubkey: String,
46    /// Jito bundle UUID
47    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    // Validate bundle size on ALL transactions first
61    BundleValidator::validate_jito_bundle_size(&request.transactions)?;
62
63    // Extract only the transactions we need to process
64    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    // Send to Jito
88    let jito_client = JitoBundleClient::new(&config.kora.bundle.jito);
89    let bundle_uuid = jito_client.send_bundle(&signed_resolved).await?;
90
91    // Encode signed transactions
92    let encoded_signed: Vec<String> = signed_resolved
93        .iter()
94        .map(|r| TransactionUtil::encode_versioned_transaction(&r.transaction))
95        .collect::<Result<Vec<_>, _>>()?;
96
97    // Merge signed transactions back into original positions
98    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        // sig_verify defaults to false
249        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}