Skip to main content

kora_rpc/method/
transfer_transaction.rs

1use 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    // manually check disallowed account because we're creating the message
52    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    // Handle native SOL transfers
69    if request.token == NATIVE_SOL {
70        instructions.push(system_instruction::transfer(&source, &destination, request.amount));
71    } else {
72        // Handle wrapped SOL and other SPL tokens
73        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    // validate transaction before signing
127    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}