kora_lib/rpc_server/method/
sign_transaction.rs1use 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)]
21pub struct SignTransactionRequest {
22 pub transaction: String,
24 #[serde(default, skip_serializing_if = "Option::is_none")]
26 pub signer_key: Option<String>,
27 #[serde(default = "default_sig_verify")]
29 pub sig_verify: bool,
30 #[serde(default, skip_serializing_if = "Option::is_none")]
32 pub user_id: Option<String>,
33}
34
35#[derive(Debug, Serialize, ToSchema)]
37pub struct SignTransactionResponse {
38 pub signed_transaction: String,
40 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 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}