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::{
82        fee::price::{PriceConfig, PriceModel},
83        tests::{
84            common::{setup_or_get_test_signer, setup_or_get_test_usage_limiter, RpcMockBuilder},
85            config_mock::{ConfigMockBuilder, ValidationConfigBuilder},
86        },
87        transaction::TransactionUtil,
88    };
89    use solana_message::{Message, VersionedMessage};
90    use solana_sdk::pubkey::Pubkey;
91    use solana_system_interface::instruction::transfer;
92
93    #[tokio::test]
94    async fn test_sign_bundle_empty_bundle() {
95        let _m = ConfigMockBuilder::new().with_bundle_enabled(true).build_and_setup();
96        let _ = setup_or_get_test_signer();
97
98        let rpc_client = Arc::new(RpcMockBuilder::new().build());
99
100        let request =
101            SignBundleRequest { transactions: vec![], signer_key: None, sig_verify: true };
102
103        let result = sign_bundle(&rpc_client, request).await;
104
105        assert!(result.is_err(), "Should fail with empty bundle");
106        let err = result.unwrap_err();
107        assert!(matches!(err, KoraError::InvalidTransaction(_)));
108    }
109
110    #[tokio::test]
111    async fn test_sign_bundle_disabled() {
112        let _m = ConfigMockBuilder::new().with_bundle_enabled(false).build_and_setup();
113        let _ = setup_or_get_test_signer();
114
115        let rpc_client = Arc::new(RpcMockBuilder::new().build());
116
117        let request = SignBundleRequest {
118            transactions: vec!["some_tx".to_string()],
119            signer_key: None,
120            sig_verify: true,
121        };
122
123        let result = sign_bundle(&rpc_client, request).await;
124
125        assert!(result.is_err(), "Should fail when bundles disabled");
126        let err = result.unwrap_err();
127        assert!(matches!(err, KoraError::JitoError(_)));
128        if let KoraError::JitoError(msg) = err {
129            assert!(msg.contains("not enabled"));
130        }
131    }
132
133    #[tokio::test]
134    async fn test_sign_bundle_too_large() {
135        let _m = ConfigMockBuilder::new().with_bundle_enabled(true).build_and_setup();
136        let _ = setup_or_get_test_signer();
137
138        let rpc_client = Arc::new(RpcMockBuilder::new().build());
139
140        let request = SignBundleRequest {
141            transactions: vec!["tx".to_string(); 6],
142            signer_key: None,
143            sig_verify: true,
144        };
145
146        let result = sign_bundle(&rpc_client, request).await;
147
148        assert!(result.is_err(), "Should fail with too many transactions");
149        let err = result.unwrap_err();
150        assert!(matches!(err, KoraError::JitoError(_)));
151        if let KoraError::JitoError(msg) = err {
152            assert!(msg.contains("maximum size"));
153        }
154    }
155
156    #[tokio::test]
157    async fn test_sign_bundle_invalid_signer_key() {
158        let _m = ConfigMockBuilder::new().with_bundle_enabled(true).build_and_setup();
159        let _ = setup_or_get_test_signer();
160
161        let rpc_client = Arc::new(RpcMockBuilder::new().build());
162
163        let request = SignBundleRequest {
164            transactions: vec!["some_tx".to_string()],
165            signer_key: Some("invalid_pubkey".to_string()),
166            sig_verify: true,
167        };
168
169        let result = sign_bundle(&rpc_client, request).await;
170
171        assert!(result.is_err(), "Should fail with invalid signer key");
172        let err = result.unwrap_err();
173        assert!(matches!(err, KoraError::ValidationError(_)));
174    }
175
176    #[tokio::test]
177    async fn test_sign_bundle_exactly_max_size() {
178        let mut validation = ValidationConfigBuilder::new()
179            .with_allowed_programs(vec!["11111111111111111111111111111111".to_string()])
180            .build();
181        validation.price = PriceConfig { model: PriceModel::Free };
182        let _m = ConfigMockBuilder::new()
183            .with_bundle_enabled(true)
184            .with_usage_limit_enabled(false)
185            .with_validation(validation)
186            .build_and_setup();
187        let signer_pubkey = setup_or_get_test_signer();
188        let _ = setup_or_get_test_usage_limiter().await;
189
190        let rpc_client = Arc::new(
191            RpcMockBuilder::new()
192                .with_fee_estimate(5000)
193                .with_blockhash()
194                .with_simulation()
195                .build(),
196        );
197
198        // Create transactions with signer as fee payer
199
200        let transactions: Vec<String> = (0..5)
201            .map(|_| {
202                let ix = transfer(&Pubkey::new_unique(), &Pubkey::new_unique(), 1000000000);
203                let message = VersionedMessage::Legacy(Message::new(&[ix], Some(&signer_pubkey)));
204                let transaction = TransactionUtil::new_unsigned_versioned_transaction(message);
205                TransactionUtil::encode_versioned_transaction(&transaction).unwrap()
206            })
207            .collect();
208
209        // Use signer_key to ensure consistency - prevents race conditions with parallel tests
210        let request = SignBundleRequest {
211            transactions,
212            signer_key: Some(signer_pubkey.to_string()),
213            sig_verify: true,
214        };
215
216        let result = sign_bundle(&rpc_client, request).await;
217
218        assert!(result.is_ok(), "Should succeed with valid transactions");
219        let response = result.unwrap();
220        assert_eq!(response.signed_transactions.len(), 5);
221        assert!(!response.signer_pubkey.is_empty());
222    }
223
224    #[tokio::test]
225    async fn test_sign_bundle_single_transaction() {
226        let mut validation = ValidationConfigBuilder::new()
227            .with_allowed_programs(vec!["11111111111111111111111111111111".to_string()])
228            .build();
229        validation.price = PriceConfig { model: PriceModel::Free };
230        let _m = ConfigMockBuilder::new()
231            .with_bundle_enabled(true)
232            .with_usage_limit_enabled(false)
233            .with_validation(validation)
234            .build_and_setup();
235        let signer_pubkey = setup_or_get_test_signer();
236        let _ = setup_or_get_test_usage_limiter().await;
237
238        let rpc_client = Arc::new(
239            RpcMockBuilder::new()
240                .with_fee_estimate(5000)
241                .with_blockhash()
242                .with_simulation()
243                .build(),
244        );
245
246        // Create transaction with signer as fee payer
247        let ix = transfer(&Pubkey::new_unique(), &Pubkey::new_unique(), 1000000000);
248        let message = VersionedMessage::Legacy(Message::new(&[ix], Some(&signer_pubkey)));
249        let transaction = TransactionUtil::new_unsigned_versioned_transaction(message);
250        let encoded_tx = TransactionUtil::encode_versioned_transaction(&transaction).unwrap();
251
252        // Single transaction bundle is valid
253        let request = SignBundleRequest {
254            transactions: vec![encoded_tx],
255            signer_key: Some(signer_pubkey.to_string()),
256            sig_verify: true,
257        };
258
259        let result = sign_bundle(&rpc_client, request).await;
260
261        assert!(result.is_ok(), "Should succeed with valid transaction");
262        let response = result.unwrap();
263        assert_eq!(response.signed_transactions.len(), 1);
264        assert!(!response.signer_pubkey.is_empty());
265    }
266
267    #[tokio::test]
268    async fn test_sign_bundle_sig_verify_default() {
269        // Test that sig_verify defaults correctly via serde (defaults to false)
270        let json = r#"{"transactions": ["tx1"]}"#;
271        let request: SignBundleRequest = serde_json::from_str(json).unwrap();
272
273        assert!(!request.sig_verify, "sig_verify should default to false");
274        assert!(request.signer_key.is_none());
275    }
276
277    #[tokio::test]
278    async fn test_sign_bundle_request_deserialization() {
279        let json = r#"{
280            "transactions": ["tx1", "tx2"],
281            "signer_key": "11111111111111111111111111111111",
282            "sig_verify": false
283        }"#;
284        let request: SignBundleRequest = serde_json::from_str(json).unwrap();
285
286        assert_eq!(request.transactions.len(), 2);
287        assert_eq!(request.signer_key, Some("11111111111111111111111111111111".to_string()));
288        assert!(!request.sig_verify);
289    }
290}