Skip to main content

kora_lib/rpc_server/method/
sign_transaction.rs

1use crate::{
2    rpc_server::middleware_utils::default_sig_verify,
3    state::get_request_signer_with_signer_key,
4    transaction::{TransactionUtil, VersionedTransactionOps, VersionedTransactionResolved},
5    usage_limit::UsageTracker,
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#[derive(Debug, Deserialize, ToSchema)]
15pub struct SignTransactionRequest {
16    pub transaction: String,
17    /// Optional signer signer_key to ensure consistency across related RPC calls
18    #[serde(default, skip_serializing_if = "Option::is_none")]
19    pub signer_key: Option<String>,
20    /// Whether to verify signatures during simulation (defaults to true)
21    #[serde(default = "default_sig_verify")]
22    pub sig_verify: bool,
23}
24
25#[derive(Debug, Serialize, ToSchema)]
26pub struct SignTransactionResponse {
27    pub signed_transaction: String,
28    /// Public key of the signer used (for client consistency)
29    pub signer_pubkey: String,
30}
31
32pub async fn sign_transaction(
33    rpc_client: &Arc<RpcClient>,
34    request: SignTransactionRequest,
35) -> Result<SignTransactionResponse, KoraError> {
36    let transaction = TransactionUtil::decode_b64_transaction(&request.transaction)?;
37
38    // Check usage limit for transaction sender
39    UsageTracker::check_transaction_usage_limit(&transaction).await?;
40
41    let signer = get_request_signer_with_signer_key(request.signer_key.as_deref())?;
42
43    let mut resolved_transaction = VersionedTransactionResolved::from_transaction(
44        &transaction,
45        rpc_client,
46        request.sig_verify,
47    )
48    .await?;
49
50    let (signed_transaction, _) =
51        resolved_transaction.sign_transaction(&signer, rpc_client).await?;
52
53    let encoded = TransactionUtil::encode_versioned_transaction(&signed_transaction)?;
54
55    Ok(SignTransactionResponse {
56        signed_transaction: encoded,
57        signer_pubkey: signer.pubkey().to_string(),
58    })
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64    use crate::tests::{
65        common::{setup_or_get_test_signer, setup_or_get_test_usage_limiter, RpcMockBuilder},
66        config_mock::ConfigMockBuilder,
67        transaction_mock::create_mock_encoded_transaction,
68    };
69
70    #[tokio::test]
71    async fn test_sign_transaction_decode_error() {
72        let _m = ConfigMockBuilder::new().build_and_setup();
73        let _ = setup_or_get_test_signer();
74
75        let _ = setup_or_get_test_usage_limiter().await;
76
77        let rpc_client = Arc::new(RpcMockBuilder::new().build());
78
79        let request = SignTransactionRequest {
80            transaction: "invalid_base64!@#$".to_string(),
81            signer_key: None,
82            sig_verify: true,
83        };
84
85        let result = sign_transaction(&rpc_client, request).await;
86
87        assert!(result.is_err(), "Should fail with decode error");
88    }
89
90    #[tokio::test]
91    async fn test_sign_transaction_invalid_signer_key() {
92        let _m = ConfigMockBuilder::new().build_and_setup();
93        let _ = setup_or_get_test_signer();
94
95        let _ = setup_or_get_test_usage_limiter().await;
96
97        let rpc_client = Arc::new(RpcMockBuilder::new().build());
98
99        let request = SignTransactionRequest {
100            transaction: create_mock_encoded_transaction(),
101            signer_key: Some("invalid_pubkey".to_string()),
102            sig_verify: true,
103        };
104
105        let result = sign_transaction(&rpc_client, request).await;
106
107        assert!(result.is_err(), "Should fail with invalid signer key");
108        let error = result.unwrap_err();
109        assert!(matches!(error, KoraError::ValidationError(_)), "Should return ValidationError");
110    }
111}