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_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/// Request payload for signing a transaction.
15///
16/// This endpoint accepts a base64-encoded Solana transaction, validates it against
17/// the configured fee payer policies, and if successful, signs it using Kora's
18/// configured signer policy
19/// but not broadcasted to the network.
20#[derive(Debug, Deserialize, ToSchema)]
21pub struct SignTransactionRequest {
22    /// Base64-encoded Solana transaction
23    pub transaction: String,
24    /// Optional public key of the signer to ensure consistency
25    #[serde(default, skip_serializing_if = "Option::is_none")]
26    pub signer_key: Option<String>,
27    /// Whether to verify signatures during simulation (defaults to false)
28    #[serde(default = "default_sig_verify")]
29    pub sig_verify: bool,
30    /// Optional user ID for usage tracking (required when pricing is Free and usage tracking is enabled)
31    #[serde(default, skip_serializing_if = "Option::is_none")]
32    pub user_id: Option<String>,
33}
34
35/// Response payload containing the signed transaction.
36#[derive(Debug, Serialize, ToSchema)]
37pub struct SignTransactionResponse {
38    /// Base64-encoded transaction signed by the Kora fee payer
39    pub signed_transaction: String,
40    /// Public key of the signer used (for client consistency)
41    pub signer_pubkey: String,
42}
43
44pub async fn sign_transaction(
45    rpc_client: &Arc<RpcClient>,
46    request: SignTransactionRequest,
47) -> Result<SignTransactionResponse, KoraError> {
48    let transaction = TransactionUtil::decode_b64_transaction(&request.transaction)?;
49
50    let config = get_config()?;
51
52    let signer = get_request_signer_with_signer_key(request.signer_key.as_deref())?;
53    let fee_payer = signer.pubkey();
54
55    let sig_verify = request.sig_verify || config.kora.force_sig_verify;
56    let mut resolved_transaction = VersionedTransactionResolved::from_transaction(
57        &transaction,
58        config,
59        rpc_client,
60        sig_verify,
61    )
62    .await?;
63
64    // Check usage limit for transaction sender
65    UsageTracker::check_transaction_usage_limit(
66        config,
67        &mut resolved_transaction,
68        request.user_id.as_deref(),
69        &fee_payer,
70        rpc_client,
71    )
72    .await?;
73
74    let (signed_transaction, _) =
75        resolved_transaction.sign_transaction(config, &signer, rpc_client, false).await?;
76
77    let encoded = TransactionUtil::encode_versioned_transaction(&signed_transaction)?;
78
79    Ok(SignTransactionResponse {
80        signed_transaction: encoded,
81        signer_pubkey: signer.pubkey().to_string(),
82    })
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88    use crate::tests::{
89        common::{setup_or_get_test_signer, setup_or_get_test_usage_limiter, RpcMockBuilder},
90        config_mock::ConfigMockBuilder,
91        transaction_mock::create_mock_encoded_transaction,
92    };
93
94    #[tokio::test]
95    async fn test_sign_transaction_decode_error() {
96        let _m = ConfigMockBuilder::new().build_and_setup();
97        let _ = setup_or_get_test_signer();
98
99        let _ = setup_or_get_test_usage_limiter().await;
100
101        let rpc_client = Arc::new(RpcMockBuilder::new().build());
102
103        let request = SignTransactionRequest {
104            transaction: "invalid_base64!@#$".to_string(),
105            signer_key: None,
106            sig_verify: true,
107            user_id: None,
108        };
109
110        let result = sign_transaction(&rpc_client, request).await;
111
112        assert!(result.is_err(), "Should fail with decode error");
113    }
114
115    #[tokio::test]
116    async fn test_sign_transaction_invalid_signer_key() {
117        let _m = ConfigMockBuilder::new().build_and_setup();
118        let _ = setup_or_get_test_signer();
119
120        let _ = setup_or_get_test_usage_limiter().await;
121
122        let rpc_client = Arc::new(RpcMockBuilder::new().build());
123
124        let request = SignTransactionRequest {
125            transaction: create_mock_encoded_transaction(),
126            signer_key: Some("invalid_pubkey".to_string()),
127            sig_verify: true,
128            user_id: None,
129        };
130
131        let result = sign_transaction(&rpc_client, request).await;
132
133        assert!(result.is_err(), "Should fail with invalid signer key");
134        let error = result.unwrap_err();
135        assert!(matches!(error, KoraError::ValidationError(_)), "Should return ValidationError");
136    }
137}