kora_lib/rpc_server/method/
sign_bundle.rs

1use 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    /// 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 true)
30    #[serde(default = "default_sig_verify")]
31    pub sig_verify: bool,
32}
33
34#[derive(Debug, Serialize, ToSchema)]
35pub struct SignBundleResponse {
36    /// Array of base64-encoded signed transactions
37    pub signed_transactions: Vec<String>,
38    /// Public key of the signer used (for client consistency)
39    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        // 5 transactions is the maximum allowed
177        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        // Will fail at decoding stage (not size validation), which is expected
186        // This test verifies that 5 transactions passes size validation
187        assert!(result.is_err());
188        // Should NOT be a JitoError about bundle size
189        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        // Single transaction bundle is valid
205        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        // Will fail at decoding stage, but should pass size validation
214        assert!(result.is_err());
215        // Should NOT be an empty bundle error
216        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        // Test that sig_verify defaults correctly via serde (defaults to false)
223        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}