kora_rpc/method/
transfer_transaction.rs1use serde::{Deserialize, Serialize};
2use solana_client::nonblocking::rpc_client::RpcClient;
3use solana_sdk::{
4 commitment_config::CommitmentConfig, message::Message, pubkey::Pubkey, system_instruction,
5 transaction::Transaction,
6};
7use std::{str::FromStr, sync::Arc};
8use utoipa::ToSchema;
9
10use kora_lib::{
11 config::ValidationConfig,
12 constant::NATIVE_SOL,
13 get_signer,
14 token::{TokenInterface, TokenProgram, TokenType},
15 transaction::{encode_b64_message, encode_b64_transaction, validator::TransactionValidator},
16 KoraError, Signer as _,
17};
18
19#[derive(Debug, Deserialize, ToSchema)]
20pub struct TransferTransactionRequest {
21 pub amount: u64,
22 pub token: String,
23 pub source: String,
24 pub destination: String,
25}
26
27#[derive(Debug, Serialize, ToSchema)]
28pub struct TransferTransactionResponse {
29 pub transaction: String,
30 pub message: String,
31 pub blockhash: String,
32}
33
34pub async fn transfer_transaction(
35 rpc_client: &Arc<RpcClient>,
36 validation: &ValidationConfig,
37 request: TransferTransactionRequest,
38) -> Result<TransferTransactionResponse, KoraError> {
39 let signer = get_signer()?;
40 let fee_payer = signer.solana_pubkey();
41
42 let validator = TransactionValidator::new(fee_payer, validation)?;
43
44 let source = Pubkey::from_str(&request.source)
45 .map_err(|e| KoraError::ValidationError(format!("Invalid source address: {}", e)))?;
46 let destination = Pubkey::from_str(&request.destination)
47 .map_err(|e| KoraError::ValidationError(format!("Invalid destination address: {}", e)))?;
48 let token_mint = Pubkey::from_str(&request.token)
49 .map_err(|e| KoraError::ValidationError(format!("Invalid token address: {}", e)))?;
50
51 if validator.is_disallowed_account(&source) {
53 return Err(KoraError::InvalidTransaction(format!(
54 "Source account {} is disallowed",
55 source
56 )));
57 }
58
59 if validator.is_disallowed_account(&destination) {
60 return Err(KoraError::InvalidTransaction(format!(
61 "Destination account {} is disallowed",
62 destination
63 )));
64 }
65
66 let mut instructions = vec![];
67
68 if request.token == NATIVE_SOL {
70 instructions.push(system_instruction::transfer(&source, &destination, request.amount));
71 } else {
72 validator.validate_token_mint(&token_mint, rpc_client).await?;
74
75 let mint_account = rpc_client
76 .get_account(&token_mint)
77 .await
78 .map_err(|e| KoraError::RpcError(e.to_string()))?;
79
80 let token_program = TokenProgram::new(TokenType::Spl);
81 let decimals = token_program
82 .get_mint_decimals(&mint_account.data)
83 .map_err(|_| KoraError::ValidationError("Invalid mint account data".to_string()))?;
84
85 let source_ata = token_program.get_associated_token_address(&source, &token_mint);
86 let dest_ata = token_program.get_associated_token_address(&destination, &token_mint);
87
88 let _ = rpc_client
89 .get_account(&source_ata)
90 .await
91 .map_err(|_| KoraError::AccountNotFound(source_ata.to_string()))?;
92
93 if rpc_client.get_account(&dest_ata).await.is_err() {
94 instructions.push(token_program.create_associated_token_account_instruction(
95 &fee_payer,
96 &destination,
97 &token_mint,
98 ));
99 }
100
101 instructions.push(
102 token_program
103 .create_transfer_checked_instruction(
104 &source_ata,
105 &token_mint,
106 &dest_ata,
107 &source,
108 request.amount,
109 decimals,
110 )
111 .map_err(|e| {
112 KoraError::InvalidTransaction(format!(
113 "Failed to create transfer instruction: {}",
114 e
115 ))
116 })?,
117 );
118 }
119
120 let blockhash =
121 rpc_client.get_latest_blockhash_with_commitment(CommitmentConfig::finalized()).await?;
122
123 let message = Message::new_with_blockhash(&instructions, Some(&fee_payer), &blockhash.0);
124 let mut transaction = Transaction::new_unsigned(message);
125
126 validator.validate_transaction(&transaction)?;
128
129 let signature = signer.sign_solana(&transaction.message_data()).await?;
130 transaction.signatures[0] = signature;
131
132 let encoded = encode_b64_transaction(&transaction)?;
133 let message_encoded = encode_b64_message(&transaction.message)?;
134
135 Ok(TransferTransactionResponse {
136 transaction: encoded,
137 message: message_encoded,
138 blockhash: blockhash.0.to_string(),
139 })
140}