kora_lib/transaction/
transaction.rs

1use 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        // First try to deserialize as VersionedTransaction
19        if let Ok(versioned_tx) = bincode::deserialize::<VersionedTransaction>(&decoded) {
20            return Ok(versioned_tx);
21        }
22
23        // Fall back to legacy Transaction and convert to VersionedTransaction
24        let legacy_tx: Transaction = bincode::deserialize(&decoded).map_err(|e| {
25            KoraError::InvalidTransaction(format!("Failed to deserialize transaction: {e}"))
26        })?;
27
28        // Convert legacy Transaction to VersionedTransaction
29        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"); // base64 of [1,2,3]
79        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        // Should have correct number of signatures (all default/empty)
96        assert_eq!(transaction.signatures.len(), message.header().num_required_signatures as usize);
97        // All signatures should be default (empty)
98        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        // Create V0 message
114        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        // Test that we can decode legacy transactions and convert them to versioned
141        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); // keypair + program_id
160            }
161            VersionedMessage::V0(_) => panic!("Expected legacy message after conversion"),
162        }
163    }
164}