kora_lib/transaction/
transaction.rs1use solana_message::VersionedMessage;
2use solana_sdk::{
3 signature::Signature,
4 transaction::{Transaction, VersionedTransaction},
5};
6
7use crate::{error::KoraError, transaction::VersionedTransactionResolved};
8use base64::{engine::general_purpose::STANDARD, Engine as _};
9
10pub struct TransactionUtil {}
11
12impl TransactionUtil {
13 pub fn decode_b64_transaction(encoded: &str) -> Result<VersionedTransaction, KoraError> {
14 let decoded = STANDARD.decode(encoded).map_err(|e| {
15 KoraError::InvalidTransaction(format!("Failed to decode base64 transaction: {e}"))
16 })?;
17
18 if let Ok(versioned_tx) = bincode::deserialize::<VersionedTransaction>(&decoded) {
20 return Ok(versioned_tx);
21 }
22
23 let legacy_tx: Transaction = bincode::deserialize(&decoded).map_err(|e| {
25 KoraError::InvalidTransaction(format!("Failed to deserialize transaction: {e}"))
26 })?;
27
28 Ok(VersionedTransaction {
30 signatures: legacy_tx.signatures,
31 message: VersionedMessage::Legacy(legacy_tx.message),
32 })
33 }
34
35 pub fn new_unsigned_versioned_transaction(message: VersionedMessage) -> VersionedTransaction {
36 let num_required_signatures = message.header().num_required_signatures as usize;
37 VersionedTransaction {
38 signatures: vec![Signature::default(); num_required_signatures],
39 message,
40 }
41 }
42
43 pub fn new_unsigned_versioned_transaction_resolved(
44 message: VersionedMessage,
45 ) -> Result<VersionedTransactionResolved, KoraError> {
46 let transaction = TransactionUtil::new_unsigned_versioned_transaction(message);
47 VersionedTransactionResolved::from_kora_built_transaction(&transaction)
48 }
49
50 pub fn encode_versioned_transaction(
51 transaction: &VersionedTransaction,
52 ) -> Result<String, KoraError> {
53 let serialized = bincode::serialize(transaction).map_err(|_| {
54 KoraError::SerializationError("Failed to serialize transaction.".to_string())
55 })?;
56 Ok(STANDARD.encode(serialized))
57 }
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63 use crate::error::KoraError;
64 use solana_message::{compiled_instruction::CompiledInstruction, v0, Message};
65 use solana_sdk::{
66 hash::Hash,
67 instruction::{AccountMeta, Instruction},
68 pubkey::Pubkey,
69 signature::Keypair,
70 signer::Signer as _,
71 };
72
73 #[test]
74 fn test_decode_b64_transaction_invalid_input() {
75 let result = TransactionUtil::decode_b64_transaction("not-base64!");
76 assert!(matches!(result, Err(KoraError::InvalidTransaction(_))));
77
78 let result = TransactionUtil::decode_b64_transaction("AQID"); assert!(matches!(result, Err(KoraError::InvalidTransaction(_))));
80 }
81
82 #[test]
83 fn test_new_unsigned_versioned_transaction() {
84 let keypair = Keypair::new();
85 let instruction = Instruction::new_with_bytes(
86 Pubkey::new_unique(),
87 &[1, 2, 3],
88 vec![AccountMeta::new(keypair.pubkey(), true)],
89 );
90 let message =
91 VersionedMessage::Legacy(Message::new(&[instruction], Some(&keypair.pubkey())));
92
93 let transaction = TransactionUtil::new_unsigned_versioned_transaction(message.clone());
94
95 assert_eq!(transaction.signatures.len(), message.header().num_required_signatures as usize);
97 for sig in &transaction.signatures {
99 assert_eq!(*sig, Signature::default());
100 }
101 assert_eq!(transaction.message, message);
102 }
103
104 #[test]
105 fn test_new_unsigned_versioned_transaction_v0() {
106 let keypair = Keypair::new();
107 let instruction = Instruction::new_with_bytes(
108 Pubkey::new_unique(),
109 &[1, 2, 3],
110 vec![AccountMeta::new(keypair.pubkey(), true)],
111 );
112
113 let v0_message = v0::Message {
115 header: solana_message::MessageHeader {
116 num_required_signatures: 1,
117 num_readonly_signed_accounts: 0,
118 num_readonly_unsigned_accounts: 0,
119 },
120 account_keys: vec![keypair.pubkey(), instruction.program_id],
121 recent_blockhash: Hash::default(),
122 instructions: vec![CompiledInstruction {
123 program_id_index: 1,
124 accounts: vec![0],
125 data: instruction.data,
126 }],
127 address_table_lookups: vec![],
128 };
129 let message = VersionedMessage::V0(v0_message);
130
131 let transaction = TransactionUtil::new_unsigned_versioned_transaction(message.clone());
132
133 assert_eq!(transaction.signatures.len(), 1);
134 assert_eq!(transaction.signatures[0], Signature::default());
135 assert_eq!(transaction.message, message);
136 }
137
138 #[test]
139 fn test_decode_b64_transaction_legacy_fallback() {
140 let keypair = Keypair::new();
142 let instruction = Instruction::new_with_bytes(
143 Pubkey::new_unique(),
144 &[1, 2, 3],
145 vec![AccountMeta::new(keypair.pubkey(), true)],
146 );
147
148 let legacy_message = Message::new(&[instruction], Some(&keypair.pubkey()));
149 let legacy_tx = Transaction::new(&[&keypair], legacy_message, Hash::default());
150
151 let serialized = bincode::serialize(&legacy_tx).unwrap();
152 let encoded = base64::engine::general_purpose::STANDARD.encode(serialized);
153
154 let decoded = TransactionUtil::decode_b64_transaction(&encoded).unwrap();
155
156 match decoded.message {
157 VersionedMessage::Legacy(msg) => {
158 assert_eq!(msg.instructions.len(), 1);
159 assert_eq!(msg.account_keys.len(), 2); }
161 VersionedMessage::V0(_) => panic!("Expected legacy message after conversion"),
162 }
163 }
164}