kora_lib/rpc_server/method/
sign_and_send_bundle.rs

1use 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    /// 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}
33
34#[derive(Debug, Serialize, ToSchema)]
35pub struct SignAndSendBundleResponse {
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    /// Jito bundle UUID
41    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    // Send to Jito
73    let jito_client = JitoBundleClient::new(&config.kora.bundle.jito);
74    let bundle_uuid = jito_client.send_bundle(&signed_resolved).await?;
75
76    // Encode for response
77    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        // sig_verify defaults to false
197        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}