kora_lib/rpc_server/method/
transfer_transaction.rs1use serde::{Deserialize, Serialize};
2use solana_client::nonblocking::rpc_client::RpcClient;
3use solana_keychain::SolanaSigner;
4use solana_message::Message;
5use solana_sdk::{message::VersionedMessage, pubkey::Pubkey};
6use solana_system_interface::instruction::transfer;
7use std::{str::FromStr, sync::Arc};
8use utoipa::ToSchema;
9
10use crate::{
11 constant::NATIVE_SOL,
12 state::get_request_signer_with_signer_key,
13 transaction::{TransactionUtil, VersionedMessageExt},
14 validator::transaction_validator::TransactionValidator,
15 CacheUtil, KoraError,
16};
17
18#[cfg(not(test))]
19use crate::state::get_config;
20
21#[cfg(test)]
22use crate::tests::config_mock::mock_state::get_config;
23
24#[derive(Debug, Deserialize, ToSchema)]
31pub struct TransferTransactionRequest {
32 pub amount: u64,
34 pub token: String,
36 pub source: String,
38 pub destination: String,
40 #[serde(default, skip_serializing_if = "Option::is_none")]
42 pub signer_key: Option<String>,
43}
44
45#[derive(Debug, Serialize, ToSchema)]
49pub struct TransferTransactionResponse {
50 pub transaction: String,
52 pub message: String,
54 pub blockhash: String,
56 pub signer_pubkey: String,
58}
59
60#[deprecated(since = "2.2.0", note = "Use getPaymentInstruction instead for fee payment flows")]
65pub async fn transfer_transaction(
66 rpc_client: &Arc<RpcClient>,
67 request: TransferTransactionRequest,
68) -> Result<TransferTransactionResponse, KoraError> {
69 let signer = get_request_signer_with_signer_key(request.signer_key.as_deref())?;
70 let config = &get_config()?;
71 let signer_pubkey = signer.pubkey();
72
73 let validator = TransactionValidator::new(config, signer_pubkey)?;
74
75 let source = Pubkey::from_str(&request.source)
76 .map_err(|e| KoraError::ValidationError(format!("Invalid source address: {e}")))?;
77 let destination = Pubkey::from_str(&request.destination)
78 .map_err(|e| KoraError::ValidationError(format!("Invalid destination address: {e}")))?;
79 let token_mint = Pubkey::from_str(&request.token)
80 .map_err(|e| KoraError::ValidationError(format!("Invalid token address: {e}")))?;
81
82 if validator.is_disallowed_account(&source) {
84 return Err(KoraError::InvalidTransaction(format!(
85 "Source account {source} is disallowed"
86 )));
87 }
88 if validator.is_disallowed_account(&destination) {
89 return Err(KoraError::InvalidTransaction(format!(
90 "Destination account {destination} is disallowed"
91 )));
92 }
93
94 let mut instructions = vec![];
95
96 if request.token == NATIVE_SOL {
98 instructions.push(transfer(&source, &destination, request.amount));
99 } else {
100 let token_mint =
102 validator.fetch_and_validate_token_mint(&token_mint, config, rpc_client).await?;
103 let token_program = token_mint.get_token_program();
104 let decimals = token_mint.decimals();
105
106 let source_ata = token_program.get_associated_token_address(&source, &token_mint.address());
107 let dest_ata =
108 token_program.get_associated_token_address(&destination, &token_mint.address());
109
110 CacheUtil::get_account(config, rpc_client, &source_ata, false).await.map_err(
111 |e| match e {
112 KoraError::AccountNotFound(_) => KoraError::AccountNotFound(source_ata.to_string()),
113 other => other,
114 },
115 )?;
116
117 match CacheUtil::get_account(config, rpc_client, &dest_ata, false).await {
118 Ok(_) => {} Err(KoraError::AccountNotFound(_)) => {
120 instructions.push(token_program.create_associated_token_account_instruction(
122 &signer_pubkey, &destination,
124 &token_mint.address(),
125 ));
126 }
127 Err(e) => return Err(e), }
129
130 instructions.push(
131 token_program
132 .create_transfer_checked_instruction(
133 &source_ata,
134 &token_mint.address(),
135 &dest_ata,
136 &source,
137 request.amount,
138 decimals,
139 )
140 .map_err(|e| {
141 KoraError::InvalidTransaction(format!(
142 "Failed to create transfer instruction: {e}"
143 ))
144 })?,
145 );
146 }
147
148 let blockhash = CacheUtil::get_or_fetch_latest_blockhash(config, rpc_client).await?;
149
150 let message = VersionedMessage::Legacy(Message::new_with_blockhash(
151 &instructions,
152 Some(&signer_pubkey), &blockhash,
154 ));
155 let transaction = TransactionUtil::new_unsigned_versioned_transaction(message);
156
157 let encoded = TransactionUtil::encode_versioned_transaction(&transaction)?;
158 let message_encoded = transaction.message.encode_b64_message()?;
159
160 Ok(TransferTransactionResponse {
161 transaction: encoded,
162 message: message_encoded,
163 blockhash: blockhash.to_string(),
164 signer_pubkey: signer_pubkey.to_string(),
165 })
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use crate::tests::{
172 common::{setup_or_get_test_signer, RpcMockBuilder},
173 config_mock::ConfigMockBuilder,
174 };
175
176 #[tokio::test]
177 #[allow(deprecated)]
178 async fn test_transfer_transaction_invalid_source() {
179 let _m = ConfigMockBuilder::new().build_and_setup();
180 let _ = setup_or_get_test_signer();
181
182 let rpc_client = Arc::new(RpcMockBuilder::new().with_mint_account(6).build());
183
184 let request = TransferTransactionRequest {
185 amount: 1000,
186 token: Pubkey::new_unique().to_string(),
187 source: "invalid".to_string(),
188 destination: Pubkey::new_unique().to_string(),
189 signer_key: None,
190 };
191
192 let result = transfer_transaction(&rpc_client, request).await;
193
194 assert!(result.is_err(), "Should fail with invalid source address");
195 let error = result.unwrap_err();
196 assert!(matches!(error, KoraError::ValidationError(_)), "Should return ValidationError");
197 match error {
198 KoraError::ValidationError(error_message) => {
199 assert!(error_message.contains("Invalid source address"));
200 }
201 _ => panic!("Should return ValidationError"),
202 }
203 }
204
205 #[tokio::test]
206 #[allow(deprecated)]
207 async fn test_transfer_transaction_invalid_destination() {
208 let _m = ConfigMockBuilder::new().build_and_setup();
209 let _ = setup_or_get_test_signer();
210
211 let rpc_client = Arc::new(RpcMockBuilder::new().with_mint_account(6).build());
212
213 let request = TransferTransactionRequest {
214 amount: 1000,
215 token: Pubkey::new_unique().to_string(),
216 source: Pubkey::new_unique().to_string(),
217 destination: "invalid".to_string(),
218 signer_key: None,
219 };
220
221 let result = transfer_transaction(&rpc_client, request).await;
222
223 assert!(result.is_err(), "Should fail with invalid destination address");
224 let error = result.unwrap_err();
225 match error {
226 KoraError::ValidationError(error_message) => {
227 assert!(error_message.contains("Invalid destination address"));
228 }
229 _ => panic!("Should return ValidationError"),
230 }
231 }
232
233 #[tokio::test]
234 #[allow(deprecated)]
235 async fn test_transfer_transaction_invalid_token() {
236 let _m = ConfigMockBuilder::new().build_and_setup();
237 let _ = setup_or_get_test_signer();
238
239 let rpc_client = Arc::new(RpcMockBuilder::new().with_mint_account(6).build());
240
241 let request = TransferTransactionRequest {
242 amount: 1000,
243 token: "invalid_token_address".to_string(),
244 source: Pubkey::new_unique().to_string(),
245 destination: Pubkey::new_unique().to_string(),
246 signer_key: None,
247 };
248
249 let result = transfer_transaction(&rpc_client, request).await;
250
251 assert!(result.is_err(), "Should fail with invalid token address");
252 let error = result.unwrap_err();
253 match error {
254 KoraError::ValidationError(error_message) => {
255 assert!(error_message.contains("Invalid token address"));
256 }
257 _ => panic!("Should return ValidationError"),
258 }
259 }
260
261 #[tokio::test]
262 #[allow(deprecated)]
263 async fn test_transfer_transaction_account_not_found_preserved() {
264 let pubkey = Pubkey::new_unique();
266
267 let error_from_cache = KoraError::AccountNotFound(pubkey.to_string());
269
270 let mapped_error = Err::<(), KoraError>(error_from_cache).map_err(|e| match e {
271 KoraError::AccountNotFound(_) => KoraError::AccountNotFound(pubkey.to_string()),
272 other => other,
273 });
274
275 assert!(matches!(mapped_error.unwrap_err(), KoraError::AccountNotFound(_)));
276 }
277
278 #[tokio::test]
279 #[allow(deprecated)]
280 async fn test_transfer_transaction_rpc_error_preserved() {
281 let pubkey = Pubkey::new_unique();
283
284 let error_from_cache = KoraError::RpcError("Service Unavailable".to_string());
286
287 let mapped_error = Err::<(), KoraError>(error_from_cache).map_err(|e| match e {
288 KoraError::AccountNotFound(_) => KoraError::AccountNotFound(pubkey.to_string()),
289 other => other,
290 });
291
292 let err = mapped_error.unwrap_err();
293 assert!(matches!(err, KoraError::RpcError(_)));
294 if let KoraError::RpcError(msg) = err {
295 assert_eq!(msg, "Service Unavailable");
296 }
297 }
298}