kora_lib/rpc_server/method/
sign_transaction.rs

1use crate::{
2    rpc_server::middleware_utils::default_sig_verify,
3    state::{get_config, 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    /// Optional user ID for usage tracking (required when pricing is free and usage tracking is enabled)
24    #[serde(default, skip_serializing_if = "Option::is_none")]
25    pub user_id: Option<String>,
26}
27
28#[derive(Debug, Serialize, ToSchema)]
29pub struct SignTransactionResponse {
30    pub signed_transaction: String,
31    /// Public key of the signer used (for client consistency)
32    pub signer_pubkey: String,
33}
34
35pub async fn sign_transaction(
36    rpc_client: &Arc<RpcClient>,
37    request: SignTransactionRequest,
38) -> Result<SignTransactionResponse, KoraError> {
39    let transaction = TransactionUtil::decode_b64_transaction(&request.transaction)?;
40
41    let config = get_config()?;
42
43    let signer = get_request_signer_with_signer_key(request.signer_key.as_deref())?;
44    let fee_payer = signer.pubkey();
45
46    let mut resolved_transaction = VersionedTransactionResolved::from_transaction(
47        &transaction,
48        config,
49        rpc_client,
50        request.sig_verify,
51    )
52    .await?;
53
54    // Check usage limit for transaction sender
55    UsageTracker::check_transaction_usage_limit(
56        config,
57        &mut resolved_transaction,
58        request.user_id.as_deref(),
59        &fee_payer,
60        rpc_client,
61    )
62    .await?;
63
64    let (signed_transaction, _) =
65        resolved_transaction.sign_transaction(config, &signer, rpc_client).await?;
66
67    let encoded = TransactionUtil::encode_versioned_transaction(&signed_transaction)?;
68
69    Ok(SignTransactionResponse {
70        signed_transaction: encoded,
71        signer_pubkey: signer.pubkey().to_string(),
72    })
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use crate::tests::{
79        common::{setup_or_get_test_signer, setup_or_get_test_usage_limiter, RpcMockBuilder},
80        config_mock::ConfigMockBuilder,
81        transaction_mock::create_mock_encoded_transaction,
82    };
83
84    #[tokio::test]
85    async fn test_sign_transaction_decode_error() {
86        let _m = ConfigMockBuilder::new().build_and_setup();
87        let _ = setup_or_get_test_signer();
88
89        let _ = setup_or_get_test_usage_limiter().await;
90
91        let rpc_client = Arc::new(RpcMockBuilder::new().build());
92
93        let request = SignTransactionRequest {
94            transaction: "invalid_base64!@#$".to_string(),
95            signer_key: None,
96            sig_verify: true,
97            user_id: None,
98        };
99
100        let result = sign_transaction(&rpc_client, request).await;
101
102        assert!(result.is_err(), "Should fail with decode error");
103    }
104
105    #[tokio::test]
106    async fn test_sign_transaction_invalid_signer_key() {
107        let _m = ConfigMockBuilder::new().build_and_setup();
108        let _ = setup_or_get_test_signer();
109
110        let _ = setup_or_get_test_usage_limiter().await;
111
112        let rpc_client = Arc::new(RpcMockBuilder::new().build());
113
114        let request = SignTransactionRequest {
115            transaction: create_mock_encoded_transaction(),
116            signer_key: Some("invalid_pubkey".to_string()),
117            sig_verify: true,
118            user_id: None,
119        };
120
121        let result = sign_transaction(&rpc_client, request).await;
122
123        assert!(result.is_err(), "Should fail with invalid signer key");
124        let error = result.unwrap_err();
125        assert!(matches!(error, KoraError::ValidationError(_)), "Should return ValidationError");
126    }
127}