kora_lib/transaction/
transaction.rs

1use solana_client::nonblocking::rpc_client::RpcClient;
2use solana_sdk::{
3    commitment_config::CommitmentConfig,
4    instruction::{AccountMeta, CompiledInstruction, Instruction},
5    message::Message,
6    pubkey::Pubkey,
7    transaction::Transaction,
8};
9
10use crate::{
11    config::ValidationConfig, error::KoraError, get_signer,
12    transaction::validator::TransactionValidator, Signer as _,
13};
14
15use base64::{engine::general_purpose::STANDARD, Engine as _};
16
17pub fn uncompile_instructions(
18    instructions: &[CompiledInstruction],
19    account_keys: &[Pubkey],
20) -> Vec<Instruction> {
21    instructions
22        .iter()
23        .map(|ix| {
24            let program_id = account_keys[ix.program_id_index as usize];
25            let accounts = ix
26                .accounts
27                .iter()
28                .map(|idx| AccountMeta {
29                    pubkey: account_keys[*idx as usize],
30                    is_signer: false,
31                    is_writable: true,
32                })
33                .collect();
34
35            Instruction { program_id, accounts, data: ix.data.clone() }
36        })
37        .collect()
38}
39
40pub async fn sign_transaction(
41    rpc_client: &RpcClient,
42    validation: &ValidationConfig,
43    transaction: Transaction,
44) -> Result<(Transaction, String), KoraError> {
45    let signer = get_signer()?;
46    let validator = TransactionValidator::new(signer.solana_pubkey(), validation)?;
47
48    // Validate transaction and accounts
49    validator.validate_transaction(&transaction)?;
50    validator.validate_disallowed_accounts(&transaction.message)?;
51
52    // Get latest blockhash and update transaction
53    let mut transaction = transaction;
54    if transaction.signatures.is_empty() {
55        let blockhash =
56            rpc_client.get_latest_blockhash_with_commitment(CommitmentConfig::finalized()).await?;
57        transaction.message.recent_blockhash = blockhash.0;
58    }
59
60    // Validate transaction fee
61    let estimated_fee = rpc_client.get_fee_for_message(&transaction.message).await?;
62    validator.validate_lamport_fee(estimated_fee)?;
63
64    // Sign transaction
65    let signature = signer.sign_solana(&transaction.message_data()).await?;
66    transaction.signatures[0] = signature;
67
68    // Serialize signed transaction
69    let serialized = bincode::serialize(&transaction)?;
70    let encoded = STANDARD.encode(serialized);
71
72    Ok((transaction, encoded))
73}
74
75pub async fn sign_and_send_transaction(
76    rpc_client: &RpcClient,
77    validation: &ValidationConfig,
78    transaction: Transaction,
79) -> Result<(String, String), KoraError> {
80    let (transaction, encoded) = sign_transaction(rpc_client, validation, transaction).await?;
81
82    // Send and confirm transaction
83    let signature = rpc_client
84        .send_and_confirm_transaction(&transaction)
85        .await
86        .map_err(|e| KoraError::RpcError(e.to_string()))?;
87
88    Ok((signature.to_string(), encoded))
89}
90
91pub fn encode_b64_transaction(transaction: &Transaction) -> Result<String, KoraError> {
92    let serialized = bincode::serialize(transaction).map_err(|e| {
93        KoraError::SerializationError(format!("Base64 serialization failed: {}", e))
94    })?;
95    Ok(STANDARD.encode(serialized))
96}
97
98pub fn encode_b64_message(message: &Message) -> Result<String, KoraError> {
99    let serialized = bincode::serialize(message).map_err(|e| {
100        KoraError::SerializationError(format!("Base64 serialization failed: {}", e))
101    })?;
102    Ok(STANDARD.encode(serialized))
103}
104
105pub fn decode_b64_transaction(encoded: &str) -> Result<Transaction, KoraError> {
106    let decoded = STANDARD.decode(encoded).map_err(|e| {
107        KoraError::InvalidTransaction(format!("Failed to decode base64 transaction: {}", e))
108    })?;
109
110    bincode::deserialize(&decoded).map_err(|e| {
111        KoraError::InvalidTransaction(format!("Failed to deserialize transaction: {}", e))
112    })
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    use solana_sdk::{hash::Hash, message::Message, signature::Keypair, signer::Signer as _};
119
120    #[test]
121    fn test_encode_decode_b64_transaction() {
122        let keypair = Keypair::new();
123        let instruction = Instruction::new_with_bytes(
124            Pubkey::new_unique(),
125            &[1, 2, 3],
126            vec![AccountMeta::new(keypair.pubkey(), true)],
127        );
128        let message = Message::new(&[instruction], Some(&keypair.pubkey()));
129        let tx = Transaction::new(&[&keypair], message, Hash::default());
130
131        let encoded = encode_b64_transaction(&tx).unwrap();
132        let decoded = decode_b64_transaction(&encoded).unwrap();
133
134        assert_eq!(tx, decoded);
135    }
136
137    #[test]
138    fn test_decode_b64_transaction_invalid_input() {
139        let result = decode_b64_transaction("not-base64!");
140        assert!(matches!(result, Err(KoraError::InvalidTransaction(_))));
141
142        let result = decode_b64_transaction("AQID"); // base64 of [1,2,3]
143        assert!(matches!(result, Err(KoraError::InvalidTransaction(_))));
144    }
145
146    #[test]
147    fn test_encode_transaction_b64() {
148        let keypair = Keypair::new();
149        let instruction = Instruction::new_with_bytes(
150            Pubkey::new_unique(),
151            &[1, 2, 3],
152            vec![AccountMeta::new(keypair.pubkey(), true)],
153        );
154        let message = Message::new(&[instruction], Some(&keypair.pubkey()));
155        let tx = Transaction::new(&[&keypair], message, Hash::default());
156
157        let encoded = encode_b64_transaction(&tx).unwrap();
158        assert!(!encoded.is_empty());
159        assert!(encoded
160            .chars()
161            .all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '/' || c == '='));
162    }
163
164    #[test]
165    fn test_uncompile_instructions() {
166        let program_id = Pubkey::new_unique();
167        let account1 = Pubkey::new_unique();
168        let account2 = Pubkey::new_unique();
169
170        let account_keys = vec![program_id, account1, account2];
171        let compiled_ix = CompiledInstruction {
172            program_id_index: 0,
173            accounts: vec![1, 2], // indices into account_keys
174            data: vec![1, 2, 3],
175        };
176
177        let instructions = uncompile_instructions(&[compiled_ix], &account_keys);
178
179        assert_eq!(instructions.len(), 1);
180        let uncompiled = &instructions[0];
181        assert_eq!(uncompiled.program_id, program_id);
182        assert_eq!(uncompiled.accounts.len(), 2);
183        assert_eq!(uncompiled.accounts[0].pubkey, account1);
184        assert_eq!(uncompiled.accounts[1].pubkey, account2);
185        assert_eq!(uncompiled.data, vec![1, 2, 3]);
186    }
187}