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_config, get_request_signer_with_signer_key},
14 transaction::{TransactionUtil, VersionedMessageExt},
15 validator::transaction_validator::TransactionValidator,
16 CacheUtil, KoraError,
17};
18
19#[derive(Debug, Deserialize, ToSchema)]
22pub struct TransferTransactionRequest {
23 pub amount: u64,
24 pub token: String,
25 pub source: String,
27 pub destination: String,
29 #[serde(default, skip_serializing_if = "Option::is_none")]
31 pub signer_key: Option<String>,
32}
33
34#[derive(Debug, Serialize, ToSchema)]
36pub struct TransferTransactionResponse {
37 pub transaction: String,
39 pub message: String,
41 pub blockhash: String,
42 pub signer_pubkey: String,
44}
45
46#[deprecated(since = "2.2.0", note = "Use getPaymentInstruction instead for fee payment flows")]
51pub async fn transfer_transaction(
52 rpc_client: &Arc<RpcClient>,
53 request: TransferTransactionRequest,
54) -> Result<TransferTransactionResponse, KoraError> {
55 let signer = get_request_signer_with_signer_key(request.signer_key.as_deref())?;
56 let config = get_config()?;
57 let signer_pubkey = signer.pubkey();
58
59 let validator = TransactionValidator::new(config, signer_pubkey)?;
60
61 let source = Pubkey::from_str(&request.source)
62 .map_err(|e| KoraError::ValidationError(format!("Invalid source address: {e}")))?;
63 let destination = Pubkey::from_str(&request.destination)
64 .map_err(|e| KoraError::ValidationError(format!("Invalid destination address: {e}")))?;
65 let token_mint = Pubkey::from_str(&request.token)
66 .map_err(|e| KoraError::ValidationError(format!("Invalid token address: {e}")))?;
67
68 if validator.is_disallowed_account(&source) {
70 return Err(KoraError::InvalidTransaction(format!(
71 "Source account {source} is disallowed"
72 )));
73 }
74 if validator.is_disallowed_account(&destination) {
75 return Err(KoraError::InvalidTransaction(format!(
76 "Destination account {destination} is disallowed"
77 )));
78 }
79
80 let mut instructions = vec![];
81
82 if request.token == NATIVE_SOL {
84 instructions.push(transfer(&source, &destination, request.amount));
85 } else {
86 let token_mint =
88 validator.fetch_and_validate_token_mint(&token_mint, config, rpc_client).await?;
89 let token_program = token_mint.get_token_program();
90 let decimals = token_mint.decimals();
91
92 let source_ata = token_program.get_associated_token_address(&source, &token_mint.address());
93 let dest_ata =
94 token_program.get_associated_token_address(&destination, &token_mint.address());
95
96 CacheUtil::get_account(config, rpc_client, &source_ata, false)
97 .await
98 .map_err(|_| KoraError::AccountNotFound(source_ata.to_string()))?;
99
100 if CacheUtil::get_account(config, rpc_client, &dest_ata, false).await.is_err() {
102 instructions.push(token_program.create_associated_token_account_instruction(
103 &signer_pubkey, &destination,
105 &token_mint.address(),
106 ));
107 }
108
109 instructions.push(
110 token_program
111 .create_transfer_checked_instruction(
112 &source_ata,
113 &token_mint.address(),
114 &dest_ata,
115 &source,
116 request.amount,
117 decimals,
118 )
119 .map_err(|e| {
120 KoraError::InvalidTransaction(format!(
121 "Failed to create transfer instruction: {e}"
122 ))
123 })?,
124 );
125 }
126
127 let blockhash =
128 rpc_client.get_latest_blockhash_with_commitment(CommitmentConfig::confirmed()).await?;
129
130 let message = VersionedMessage::Legacy(Message::new_with_blockhash(
131 &instructions,
132 Some(&signer_pubkey), &blockhash.0,
134 ));
135 let transaction = TransactionUtil::new_unsigned_versioned_transaction(message);
136
137 let encoded = TransactionUtil::encode_versioned_transaction(&transaction)?;
138 let message_encoded = transaction.message.encode_b64_message()?;
139
140 Ok(TransferTransactionResponse {
141 transaction: encoded,
142 message: message_encoded,
143 blockhash: blockhash.0.to_string(),
144 signer_pubkey: signer_pubkey.to_string(),
145 })
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use crate::{
152 state::update_config,
153 tests::{
154 common::{setup_or_get_test_signer, RpcMockBuilder},
155 config_mock::ConfigMockBuilder,
156 },
157 };
158
159 #[tokio::test]
160 #[allow(deprecated)]
161 async fn test_transfer_transaction_invalid_source() {
162 let config = ConfigMockBuilder::new().build();
163 update_config(config).unwrap();
164 let _ = setup_or_get_test_signer();
165
166 let rpc_client = Arc::new(RpcMockBuilder::new().with_mint_account(6).build());
167
168 let request = TransferTransactionRequest {
169 amount: 1000,
170 token: Pubkey::new_unique().to_string(),
171 source: "invalid".to_string(),
172 destination: Pubkey::new_unique().to_string(),
173 signer_key: None,
174 };
175
176 let result = transfer_transaction(&rpc_client, request).await;
177
178 assert!(result.is_err(), "Should fail with invalid source address");
179 let error = result.unwrap_err();
180 assert!(matches!(error, KoraError::ValidationError(_)), "Should return ValidationError");
181 match error {
182 KoraError::ValidationError(error_message) => {
183 assert!(error_message.contains("Invalid source address"));
184 }
185 _ => panic!("Should return ValidationError"),
186 }
187 }
188
189 #[tokio::test]
190 #[allow(deprecated)]
191 async fn test_transfer_transaction_invalid_destination() {
192 let config = ConfigMockBuilder::new().build();
193 update_config(config).unwrap();
194 let _ = setup_or_get_test_signer();
195
196 let rpc_client = Arc::new(RpcMockBuilder::new().with_mint_account(6).build());
197
198 let request = TransferTransactionRequest {
199 amount: 1000,
200 token: Pubkey::new_unique().to_string(),
201 source: Pubkey::new_unique().to_string(),
202 destination: "invalid".to_string(),
203 signer_key: None,
204 };
205
206 let result = transfer_transaction(&rpc_client, request).await;
207
208 assert!(result.is_err(), "Should fail with invalid destination address");
209 let error = result.unwrap_err();
210 match error {
211 KoraError::ValidationError(error_message) => {
212 assert!(error_message.contains("Invalid destination address"));
213 }
214 _ => panic!("Should return ValidationError"),
215 }
216 }
217
218 #[tokio::test]
219 #[allow(deprecated)]
220 async fn test_transfer_transaction_invalid_token() {
221 let config = ConfigMockBuilder::new().build();
222 update_config(config).unwrap();
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}