kora_lib/transaction/
transaction.rs1use 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 validator.validate_transaction(&transaction)?;
50 validator.validate_disallowed_accounts(&transaction.message)?;
51
52 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 let estimated_fee = rpc_client.get_fee_for_message(&transaction.message).await?;
62 validator.validate_lamport_fee(estimated_fee)?;
63
64 let signature = signer.sign_solana(&transaction.message_data()).await?;
66 transaction.signatures[0] = signature;
67
68 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 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"); 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], 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}