kora_lib/rpc_server/method/
transfer_transaction.rs1use serde::{Deserialize, Serialize};
2use solana_client::nonblocking::rpc_client::RpcClient;
3use solana_commitment_config::CommitmentConfig;
4use solana_keychain::SolanaSigner;
5use solana_message::Message;
6use solana_sdk::{message::VersionedMessage, pubkey::Pubkey};
7use solana_system_interface::instruction::transfer;
8use std::{str::FromStr, sync::Arc};
9use utoipa::ToSchema;
10
11use crate::{
12 constant::NATIVE_SOL,
13 state::get_request_signer_with_signer_key,
14 transaction::{TransactionUtil, VersionedMessageExt},
15 validator::transaction_validator::TransactionValidator,
16 CacheUtil, KoraError,
17};
18
19#[cfg(not(test))]
20use crate::state::get_config;
21
22#[cfg(test)]
23use crate::tests::config_mock::mock_state::get_config;
24
25#[derive(Debug, Deserialize, ToSchema)]
28pub struct TransferTransactionRequest {
29 pub amount: u64,
30 pub token: String,
31 pub source: String,
33 pub destination: String,
35 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub signer_key: Option<String>,
38}
39
40#[derive(Debug, Serialize, ToSchema)]
42pub struct TransferTransactionResponse {
43 pub transaction: String,
45 pub message: String,
47 pub blockhash: String,
48 pub signer_pubkey: String,
50}
51
52#[deprecated(since = "2.2.0", note = "Use getPaymentInstruction instead for fee payment flows")]
57pub async fn transfer_transaction(
58 rpc_client: &Arc<RpcClient>,
59 request: TransferTransactionRequest,
60) -> Result<TransferTransactionResponse, KoraError> {
61 let signer = get_request_signer_with_signer_key(request.signer_key.as_deref())?;
62 let config = &get_config()?;
63 let signer_pubkey = signer.pubkey();
64
65 let validator = TransactionValidator::new(config, signer_pubkey)?;
66
67 let source = Pubkey::from_str(&request.source)
68 .map_err(|e| KoraError::ValidationError(format!("Invalid source address: {e}")))?;
69 let destination = Pubkey::from_str(&request.destination)
70 .map_err(|e| KoraError::ValidationError(format!("Invalid destination address: {e}")))?;
71 let token_mint = Pubkey::from_str(&request.token)
72 .map_err(|e| KoraError::ValidationError(format!("Invalid token address: {e}")))?;
73
74 if validator.is_disallowed_account(&source) {
76 return Err(KoraError::InvalidTransaction(format!(
77 "Source account {source} is disallowed"
78 )));
79 }
80 if validator.is_disallowed_account(&destination) {
81 return Err(KoraError::InvalidTransaction(format!(
82 "Destination account {destination} is disallowed"
83 )));
84 }
85
86 let mut instructions = vec![];
87
88 if request.token == NATIVE_SOL {
90 instructions.push(transfer(&source, &destination, request.amount));
91 } else {
92 let token_mint =
94 validator.fetch_and_validate_token_mint(&token_mint, config, rpc_client).await?;
95 let token_program = token_mint.get_token_program();
96 let decimals = token_mint.decimals();
97
98 let source_ata = token_program.get_associated_token_address(&source, &token_mint.address());
99 let dest_ata =
100 token_program.get_associated_token_address(&destination, &token_mint.address());
101
102 CacheUtil::get_account(config, rpc_client, &source_ata, false)
103 .await
104 .map_err(|_| KoraError::AccountNotFound(source_ata.to_string()))?;
105
106 if CacheUtil::get_account(config, rpc_client, &dest_ata, false).await.is_err() {
108 instructions.push(token_program.create_associated_token_account_instruction(
109 &signer_pubkey, &destination,
111 &token_mint.address(),
112 ));
113 }
114
115 instructions.push(
116 token_program
117 .create_transfer_checked_instruction(
118 &source_ata,
119 &token_mint.address(),
120 &dest_ata,
121 &source,
122 request.amount,
123 decimals,
124 )
125 .map_err(|e| {
126 KoraError::InvalidTransaction(format!(
127 "Failed to create transfer instruction: {e}"
128 ))
129 })?,
130 );
131 }
132
133 let blockhash =
134 rpc_client.get_latest_blockhash_with_commitment(CommitmentConfig::confirmed()).await?;
135
136 let message = VersionedMessage::Legacy(Message::new_with_blockhash(
137 &instructions,
138 Some(&signer_pubkey), &blockhash.0,
140 ));
141 let transaction = TransactionUtil::new_unsigned_versioned_transaction(message);
142
143 let encoded = TransactionUtil::encode_versioned_transaction(&transaction)?;
144 let message_encoded = transaction.message.encode_b64_message()?;
145
146 Ok(TransferTransactionResponse {
147 transaction: encoded,
148 message: message_encoded,
149 blockhash: blockhash.0.to_string(),
150 signer_pubkey: signer_pubkey.to_string(),
151 })
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157 use crate::tests::{
158 common::{setup_or_get_test_signer, RpcMockBuilder},
159 config_mock::ConfigMockBuilder,
160 };
161
162 #[tokio::test]
163 #[allow(deprecated)]
164 async fn test_transfer_transaction_invalid_source() {
165 let _m = ConfigMockBuilder::new().build_and_setup();
166 let _ = setup_or_get_test_signer();
167
168 let rpc_client = Arc::new(RpcMockBuilder::new().with_mint_account(6).build());
169
170 let request = TransferTransactionRequest {
171 amount: 1000,
172 token: Pubkey::new_unique().to_string(),
173 source: "invalid".to_string(),
174 destination: Pubkey::new_unique().to_string(),
175 signer_key: None,
176 };
177
178 let result = transfer_transaction(&rpc_client, request).await;
179
180 assert!(result.is_err(), "Should fail with invalid source address");
181 let error = result.unwrap_err();
182 assert!(matches!(error, KoraError::ValidationError(_)), "Should return ValidationError");
183 match error {
184 KoraError::ValidationError(error_message) => {
185 assert!(error_message.contains("Invalid source address"));
186 }
187 _ => panic!("Should return ValidationError"),
188 }
189 }
190
191 #[tokio::test]
192 #[allow(deprecated)]
193 async fn test_transfer_transaction_invalid_destination() {
194 let _m = ConfigMockBuilder::new().build_and_setup();
195 let _ = setup_or_get_test_signer();
196
197 let rpc_client = Arc::new(RpcMockBuilder::new().with_mint_account(6).build());
198
199 let request = TransferTransactionRequest {
200 amount: 1000,
201 token: Pubkey::new_unique().to_string(),
202 source: Pubkey::new_unique().to_string(),
203 destination: "invalid".to_string(),
204 signer_key: None,
205 };
206
207 let result = transfer_transaction(&rpc_client, request).await;
208
209 assert!(result.is_err(), "Should fail with invalid destination address");
210 let error = result.unwrap_err();
211 match error {
212 KoraError::ValidationError(error_message) => {
213 assert!(error_message.contains("Invalid destination address"));
214 }
215 _ => panic!("Should return ValidationError"),
216 }
217 }
218
219 #[tokio::test]
220 #[allow(deprecated)]
221 async fn test_transfer_transaction_invalid_token() {
222 let _m = ConfigMockBuilder::new().build_and_setup();
223 let _ = setup_or_get_test_signer();
224
225 let rpc_client = Arc::new(RpcMockBuilder::new().with_mint_account(6).build());
226
227 let request = TransferTransactionRequest {
228 amount: 1000,
229 token: "invalid_token_address".to_string(),
230 source: Pubkey::new_unique().to_string(),
231 destination: Pubkey::new_unique().to_string(),
232 signer_key: None,
233 };
234
235 let result = transfer_transaction(&rpc_client, request).await;
236
237 assert!(result.is_err(), "Should fail with invalid token address");
238 let error = result.unwrap_err();
239 match error {
240 KoraError::ValidationError(error_message) => {
241 assert!(error_message.contains("Invalid token address"));
242 }
243 _ => panic!("Should return ValidationError"),
244 }
245 }
246}