Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 26 additions & 30 deletions crates/lib/src/rpc_server/method/transfer_transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,42 +11,52 @@ use utoipa::ToSchema;
use crate::{
constant::NATIVE_SOL,
state::{get_config, get_request_signer_with_signer_key},
transaction::{
TransactionUtil, VersionedMessageExt, VersionedTransactionOps, VersionedTransactionResolved,
},
transaction::{TransactionUtil, VersionedMessageExt},
validator::transaction_validator::TransactionValidator,
CacheUtil, KoraError,
};

/// **DEPRECATED**: Use `getPaymentInstruction` instead for fee payment flows.
/// This endpoint will be removed in a future version.
#[derive(Debug, Deserialize, ToSchema)]
pub struct TransferTransactionRequest {
pub amount: u64,
pub token: String,
/// The source wallet address
pub source: String,
/// The destination wallet address
pub destination: String,
/// Optional signer signer_key to ensure consistency across related RPC calls
/// Optional signer key to ensure consistency across related RPC calls
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signer_key: Option<String>,
}

/// **DEPRECATED**: Use `getPaymentInstruction` instead for fee payment flows.
#[derive(Debug, Serialize, ToSchema)]
pub struct TransferTransactionResponse {
/// Unsigned base64-encoded transaction
pub transaction: String,
/// Unsigned base64-encoded message
pub message: String,
pub blockhash: String,
/// Public key of the signer used (for client consistency)
pub signer_pubkey: String,
}

/// **DEPRECATED**: Use `getPaymentInstruction` instead for fee payment flows.
///
/// Creates an unsigned transfer transaction from source to destination.
/// Kora is the fee payer but does NOT sign - user must sign before submitting.
#[deprecated(since = "2.2.0", note = "Use getPaymentInstruction instead for fee payment flows")]
pub async fn transfer_transaction(
rpc_client: &Arc<RpcClient>,
request: TransferTransactionRequest,
) -> Result<TransferTransactionResponse, KoraError> {
let signer = get_request_signer_with_signer_key(request.signer_key.as_deref())?;
let config = get_config()?;
let fee_payer = signer.pubkey();
let signer_pubkey = signer.pubkey();

let validator = TransactionValidator::new(config, fee_payer)?;
let validator = TransactionValidator::new(config, signer_pubkey)?;

let source = Pubkey::from_str(&request.source)
.map_err(|e| KoraError::ValidationError(format!("Invalid source address: {e}")))?;
Expand All @@ -55,13 +65,12 @@ pub async fn transfer_transaction(
let token_mint = Pubkey::from_str(&request.token)
.map_err(|e| KoraError::ValidationError(format!("Invalid token address: {e}")))?;

// manually check disallowed account because we're creating the message
// Check source and destination are not disallowed
if validator.is_disallowed_account(&source) {
return Err(KoraError::InvalidTransaction(format!(
"Source account {source} is disallowed"
)));
}

if validator.is_disallowed_account(&destination) {
return Err(KoraError::InvalidTransaction(format!(
"Destination account {destination} is disallowed"
Expand All @@ -88,9 +97,10 @@ pub async fn transfer_transaction(
.await
.map_err(|_| KoraError::AccountNotFound(source_ata.to_string()))?;

// Create ATA for destination if it doesn't exist (Kora pays for ATA creation)
if CacheUtil::get_account(config, rpc_client, &dest_ata, false).await.is_err() {
instructions.push(token_program.create_associated_token_account_instruction(
&fee_payer,
&signer_pubkey, // Kora pays for ATA creation
&destination,
&token_mint.address(),
));
Expand Down Expand Up @@ -119,36 +129,19 @@ pub async fn transfer_transaction(

let message = VersionedMessage::Legacy(Message::new_with_blockhash(
&instructions,
Some(&fee_payer),
Some(&signer_pubkey), // Kora as fee payer
&blockhash.0,
));
let transaction = TransactionUtil::new_unsigned_versioned_transaction(message);

let mut resolved_transaction =
VersionedTransactionResolved::from_kora_built_transaction(&transaction)?;

// validate transaction before signing
validator.validate_transaction(config, &mut resolved_transaction, rpc_client).await?;

// Find the fee payer position in the account keys
let fee_payer_position = resolved_transaction.find_signer_position(&fee_payer)?;

let message_bytes = resolved_transaction.transaction.message.serialize();
let signature = signer
.sign_message(&message_bytes)
.await
.map_err(|e| KoraError::SigningError(e.to_string()))?;

resolved_transaction.transaction.signatures[fee_payer_position] = signature;

let encoded = resolved_transaction.encode_b64_transaction()?;
let encoded = TransactionUtil::encode_versioned_transaction(&transaction)?;
let message_encoded = transaction.message.encode_b64_message()?;

Ok(TransferTransactionResponse {
transaction: encoded,
message: message_encoded,
blockhash: blockhash.0.to_string(),
signer_pubkey: fee_payer.to_string(),
signer_pubkey: signer_pubkey.to_string(),
})
}

Expand All @@ -164,6 +157,7 @@ mod tests {
};

#[tokio::test]
#[allow(deprecated)]
async fn test_transfer_transaction_invalid_source() {
let config = ConfigMockBuilder::new().build();
update_config(config).unwrap();
Expand Down Expand Up @@ -193,6 +187,7 @@ mod tests {
}

#[tokio::test]
#[allow(deprecated)]
async fn test_transfer_transaction_invalid_destination() {
let config = ConfigMockBuilder::new().build();
update_config(config).unwrap();
Expand All @@ -204,7 +199,7 @@ mod tests {
amount: 1000,
token: Pubkey::new_unique().to_string(),
source: Pubkey::new_unique().to_string(),
destination: "invalid_pubkey".to_string(),
destination: "invalid".to_string(),
signer_key: None,
};

Expand All @@ -221,6 +216,7 @@ mod tests {
}

#[tokio::test]
#[allow(deprecated)]
async fn test_transfer_transaction_invalid_token() {
let config = ConfigMockBuilder::new().build();
update_config(config).unwrap();
Expand Down
3 changes: 3 additions & 0 deletions crates/lib/src/rpc_server/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use utoipa::{
ToSchema,
};

#[allow(deprecated)]
use crate::rpc_server::method::{
estimate_transaction_fee::{
estimate_transaction_fee, EstimateTransactionFeeRequest, EstimateTransactionFeeResponse,
Expand Down Expand Up @@ -98,11 +99,13 @@ impl KoraRpc {
result
}

#[deprecated(since = "2.2.0", note = "Use getPaymentInstruction instead for fee payment flows")]
pub async fn transfer_transaction(
&self,
request: TransferTransactionRequest,
) -> Result<TransferTransactionResponse, KoraError> {
info!("Transfer transaction request: {request:?}");
#[allow(deprecated)]
let result = transfer_transaction(&self.rpc_client, request).await;
info!("Transfer transaction response: {result:?}");
result
Expand Down
2 changes: 2 additions & 0 deletions crates/lib/src/rpc_server/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,12 @@ macro_rules! register_method_if_enabled {
// For methods with parameters
($module:expr, $enabled_methods:expr, $field:ident, $method_name:expr, $rpc_method:ident, with_params) => {
if $enabled_methods.$field {
#[allow(deprecated)]
let _ =
$module.register_async_method($method_name, |rpc_params, rpc_context| async move {
let rpc = rpc_context.as_ref();
let params = rpc_params.parse()?;
#[allow(deprecated)]
rpc.$rpc_method(params).await.map_err(Into::into)
});
}
Expand Down
16 changes: 9 additions & 7 deletions sdks/ts/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import {
GetPaymentInstructionResponse,
} from './types/index.js';
import crypto from 'crypto';
import { getInstructionsFromBase64Message } from './utils/transaction.js';
import { findAssociatedTokenPda, TOKEN_PROGRAM_ADDRESS, getTransferInstruction } from '@solana-program/token';
import { getInstructionsFromBase64Message } from './utils/transaction.js';

/**
* Kora RPC client for interacting with the Kora paymaster service.
Expand Down Expand Up @@ -248,13 +248,17 @@ export class KoraClient {
}

/**
* Creates a token transfer transaction with Kora as the fee payer.
* Creates an unsigned transfer transaction.
*
* @deprecated Use `getPaymentInstruction` instead for fee payment flows.
*
* @param request - Transfer request parameters
* @param request.amount - Amount to transfer (in token's smallest unit)
* @param request.token - Mint address of the token to transfer
* @param request.source - Source wallet public key
* @param request.destination - Destination wallet public key
* @returns Base64-encoded signed transaction, base64-encoded message, blockhash, and parsed instructions
* @param request.signer_key - Optional signer key to select specific Kora signer
* @returns Unsigned transaction, message, blockhash, and signer info
* @throws {Error} When the RPC call fails or token is not supported
*
* @example
Expand All @@ -263,11 +267,9 @@ export class KoraClient {
* amount: 1000000, // 1 USDC (6 decimals)
* token: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
* source: 'sourceWalletPublicKey',
* destination: 'destinationWalletPublicKey'
* destination: 'destinationWalletPublicKey',
* });
* console.log('Transaction:', transfer.transaction);
* console.log('Message:', transfer.message);
* console.log('Instructions:', transfer.instructions);
* console.log('Signer:', transfer.signer_pubkey);
* ```
*/
async transferTransaction(request: TransferTransactionRequest): Promise<TransferTransactionResponse> {
Expand Down
13 changes: 8 additions & 5 deletions sdks/ts/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { Instruction } from '@solana/kit';
*/

/**
* Parameters for creating a token transfer transaction.
* Parameters for creating a transfer transaction.
* @deprecated Use `getPaymentInstruction` instead for fee payment flows.
*/
export interface TransferTransactionRequest {
/** Amount to transfer in the token's smallest unit (e.g., lamports for SOL) */
Expand All @@ -16,7 +17,7 @@ export interface TransferTransactionRequest {
source: string;
/** Public key of the destination wallet (not token account) */
destination: string;
/** Optional signer address for the transaction */
/** Optional signer key to select a specific Kora signer */
signer_key?: string;
}

Expand Down Expand Up @@ -82,15 +83,17 @@ export interface GetPaymentInstructionRequest {

/**
* Response from creating a transfer transaction.
* The transaction is unsigned.
* @deprecated Use `getPaymentInstruction` instead for fee payment flows.
*/
export interface TransferTransactionResponse {
/** Base64-encoded signed transaction */
/** Base64-encoded unsigned transaction */
transaction: string;
/** Base64-encoded message */
/** Base64-encoded unsigned message */
message: string;
/** Recent blockhash used in the transaction */
blockhash: string;
/** Public key of the signer used to send the transaction */
/** Public key of the Kora signer (fee payer) */
signer_pubkey: string;
/** Parsed instructions from the transaction message */
instructions: Instruction[];
Expand Down
Loading