kora_lib/transaction/
versioned_transaction.rs

1use async_trait::async_trait;
2use base64::{engine::general_purpose::STANDARD, Engine as _};
3use solana_client::{nonblocking::rpc_client::RpcClient, rpc_config::RpcSimulateTransactionConfig};
4use solana_commitment_config::CommitmentConfig;
5use solana_keychain::{Signer, SolanaSigner};
6use solana_message::{
7    compiled_instruction::CompiledInstruction, v0::MessageAddressTableLookup, VersionedMessage,
8};
9use solana_sdk::{instruction::Instruction, pubkey::Pubkey, transaction::VersionedTransaction};
10use std::{collections::HashMap, ops::Deref};
11
12use solana_transaction_status_client_types::{UiInstruction, UiTransactionEncoding};
13
14use crate::{
15    config::Config,
16    error::KoraError,
17    fee::fee::{FeeConfigUtil, TransactionFeeUtil},
18    transaction::{
19        instruction_util::IxUtils, ParsedSPLInstructionData, ParsedSPLInstructionType,
20        ParsedSystemInstructionData, ParsedSystemInstructionType,
21    },
22    validator::transaction_validator::TransactionValidator,
23    CacheUtil,
24};
25use solana_address_lookup_table_interface::state::AddressLookupTable;
26
27/// A fully resolved transaction with lookup tables and inner instructions resolved
28pub struct VersionedTransactionResolved {
29    pub transaction: VersionedTransaction,
30
31    // Includes lookup table addresses
32    pub all_account_keys: Vec<Pubkey>,
33
34    // Includes all instructions, including inner instructions
35    pub all_instructions: Vec<Instruction>,
36
37    // Parsed instructions by type (None if not parsed yet)
38    parsed_system_instructions:
39        Option<HashMap<ParsedSystemInstructionType, Vec<ParsedSystemInstructionData>>>,
40
41    // Parsed SPL instructions by type (None if not parsed yet)
42    parsed_spl_instructions:
43        Option<HashMap<ParsedSPLInstructionType, Vec<ParsedSPLInstructionData>>>,
44}
45
46impl Deref for VersionedTransactionResolved {
47    type Target = VersionedTransaction;
48
49    fn deref(&self) -> &Self::Target {
50        &self.transaction
51    }
52}
53
54#[async_trait]
55pub trait VersionedTransactionOps {
56    fn encode_b64_transaction(&self) -> Result<String, KoraError>;
57    fn find_signer_position(&self, signer_pubkey: &Pubkey) -> Result<usize, KoraError>;
58
59    async fn sign_transaction(
60        &mut self,
61        config: &Config,
62        signer: &std::sync::Arc<Signer>,
63        rpc_client: &RpcClient,
64    ) -> Result<(VersionedTransaction, String), KoraError>;
65    async fn sign_and_send_transaction(
66        &mut self,
67        config: &Config,
68        signer: &std::sync::Arc<Signer>,
69        rpc_client: &RpcClient,
70    ) -> Result<(String, String), KoraError>;
71}
72
73impl VersionedTransactionResolved {
74    pub async fn from_transaction(
75        transaction: &VersionedTransaction,
76        config: &Config,
77        rpc_client: &RpcClient,
78        sig_verify: bool,
79    ) -> Result<Self, KoraError> {
80        let mut resolved = Self {
81            transaction: transaction.clone(),
82            all_account_keys: vec![],
83            all_instructions: vec![],
84            parsed_system_instructions: None,
85            parsed_spl_instructions: None,
86        };
87
88        // 1. Resolve lookup table addresses based on transaction type
89        let resolved_addresses = match &transaction.message {
90            VersionedMessage::Legacy(_) => {
91                // Legacy transactions don't have lookup tables
92                vec![]
93            }
94            VersionedMessage::V0(v0_message) => {
95                // V0 transactions may have lookup tables
96                LookupTableUtil::resolve_lookup_table_addresses(
97                    config,
98                    rpc_client,
99                    &v0_message.address_table_lookups,
100                )
101                .await?
102            }
103        };
104
105        // Set all accout keys
106        let mut all_account_keys = transaction.message.static_account_keys().to_vec();
107        all_account_keys.extend(resolved_addresses.clone());
108        resolved.all_account_keys = all_account_keys.clone();
109
110        // 2. Fetch all instructions
111        let outer_instructions =
112            IxUtils::uncompile_instructions(transaction.message.instructions(), &all_account_keys)?;
113
114        let inner_instructions = resolved.fetch_inner_instructions(rpc_client, sig_verify).await?;
115
116        resolved.all_instructions.extend(outer_instructions);
117        resolved.all_instructions.extend(inner_instructions);
118
119        Ok(resolved)
120    }
121
122    /// Only use this is we built the transaction ourselves, because it won't do any checks for resolving LUT, etc.
123    pub fn from_kora_built_transaction(
124        transaction: &VersionedTransaction,
125    ) -> Result<Self, KoraError> {
126        Ok(Self {
127            transaction: transaction.clone(),
128            all_account_keys: transaction.message.static_account_keys().to_vec(),
129            all_instructions: IxUtils::uncompile_instructions(
130                transaction.message.instructions(),
131                transaction.message.static_account_keys(),
132            )?,
133            parsed_system_instructions: None,
134            parsed_spl_instructions: None,
135        })
136    }
137
138    /// Fetch inner instructions via simulation
139    async fn fetch_inner_instructions(
140        &mut self,
141        rpc_client: &RpcClient,
142        sig_verify: bool,
143    ) -> Result<Vec<Instruction>, KoraError> {
144        let simulation_result = rpc_client
145            .simulate_transaction_with_config(
146                &self.transaction,
147                RpcSimulateTransactionConfig {
148                    commitment: Some(rpc_client.commitment()),
149                    sig_verify,
150                    inner_instructions: true,
151                    replace_recent_blockhash: false,
152                    encoding: Some(UiTransactionEncoding::Base64),
153                    accounts: None,
154                    min_context_slot: None,
155                },
156            )
157            .await
158            .map_err(|e| KoraError::RpcError(format!("Failed to simulate transaction: {e}")))?;
159
160        if let Some(err) = simulation_result.value.err {
161            return Err(KoraError::InvalidTransaction(format!(
162                "Transaction simulation failed: {err}"
163            )));
164        }
165
166        if let Some(inner_instructions) = simulation_result.value.inner_instructions {
167            let mut compiled_inner_instructions: Vec<CompiledInstruction> = vec![];
168
169            inner_instructions.iter().for_each(|ix| {
170                ix.instructions.iter().for_each(|inner_ix| match inner_ix {
171                    UiInstruction::Compiled(ix) => {
172                        compiled_inner_instructions.push(CompiledInstruction {
173                            program_id_index: ix.program_id_index,
174                            accounts: ix.accounts.clone(),
175                            data: bs58::decode(&ix.data).into_vec().unwrap_or_default(),
176                        });
177                    }
178                    UiInstruction::Parsed(ui_parsed) => {
179                        if let Some(compiled) = IxUtils::reconstruct_instruction_from_ui(
180                            &UiInstruction::Parsed(ui_parsed.clone()),
181                            &self.all_account_keys,
182                        ) {
183                            compiled_inner_instructions.push(compiled);
184                        }
185                    }
186                });
187            });
188
189            return IxUtils::uncompile_instructions(
190                &compiled_inner_instructions,
191                &self.all_account_keys,
192            );
193        }
194
195        Ok(vec![])
196    }
197
198    pub fn get_or_parse_system_instructions(
199        &mut self,
200    ) -> Result<&HashMap<ParsedSystemInstructionType, Vec<ParsedSystemInstructionData>>, KoraError>
201    {
202        if self.parsed_system_instructions.is_none() {
203            self.parsed_system_instructions = Some(IxUtils::parse_system_instructions(self)?);
204        }
205
206        self.parsed_system_instructions.as_ref().ok_or_else(|| {
207            KoraError::SerializationError("Parsed system instructions not found".to_string())
208        })
209    }
210
211    pub fn get_or_parse_spl_instructions(
212        &mut self,
213    ) -> Result<&HashMap<ParsedSPLInstructionType, Vec<ParsedSPLInstructionData>>, KoraError> {
214        if self.parsed_spl_instructions.is_none() {
215            self.parsed_spl_instructions = Some(IxUtils::parse_token_instructions(self)?);
216        }
217
218        self.parsed_spl_instructions.as_ref().ok_or_else(|| {
219            KoraError::SerializationError("Parsed SPL instructions not found".to_string())
220        })
221    }
222}
223
224// Implementation of the consolidated trait for VersionedTransactionResolved
225#[async_trait]
226impl VersionedTransactionOps for VersionedTransactionResolved {
227    fn encode_b64_transaction(&self) -> Result<String, KoraError> {
228        let serialized = bincode::serialize(&self.transaction).map_err(|e| {
229            KoraError::SerializationError(format!("Base64 serialization failed: {e}"))
230        })?;
231        Ok(STANDARD.encode(serialized))
232    }
233
234    fn find_signer_position(&self, signer_pubkey: &Pubkey) -> Result<usize, KoraError> {
235        self.transaction
236            .message
237            .static_account_keys()
238            .iter()
239            .position(|key| key == signer_pubkey)
240            .ok_or_else(|| {
241                KoraError::InvalidTransaction(format!(
242                    "Signer {signer_pubkey} not found in transaction account keys"
243                ))
244            })
245    }
246
247    async fn sign_transaction(
248        &mut self,
249        config: &Config,
250        signer: &std::sync::Arc<Signer>,
251        rpc_client: &RpcClient,
252    ) -> Result<(VersionedTransaction, String), KoraError> {
253        let fee_payer = signer.pubkey();
254        let validator = TransactionValidator::new(config, fee_payer)?;
255
256        // Validate transaction and accounts (already resolved)
257        validator.validate_transaction(config, self, rpc_client).await?;
258
259        // Calculate fee and validate payment if price model requires it
260        let fee_calculation = FeeConfigUtil::estimate_kora_fee(
261            self,
262            &fee_payer,
263            config.validation.is_payment_required(),
264            rpc_client,
265            config,
266        )
267        .await?;
268
269        let required_lamports = fee_calculation.total_fee_lamports;
270
271        // Validate payment if price model is not Free
272        if required_lamports > 0 {
273            log::info!("Payment validation: required_lamports={}", required_lamports);
274            // Get the expected payment destination
275            let payment_destination = config.kora.get_payment_address(&fee_payer)?;
276
277            // Validate token payment using the resolved transaction
278            TransactionValidator::validate_token_payment(
279                config,
280                self,
281                required_lamports,
282                rpc_client,
283                &payment_destination,
284            )
285            .await?;
286
287            // Validate strict pricing if enabled
288            TransactionValidator::validate_strict_pricing_with_fee(config, &fee_calculation)?;
289        }
290
291        // Get latest blockhash and update transaction
292        let mut transaction = self.transaction.clone();
293
294        if transaction.signatures.is_empty() {
295            let blockhash = rpc_client
296                .get_latest_blockhash_with_commitment(CommitmentConfig::confirmed())
297                .await?;
298            transaction.message.set_recent_blockhash(blockhash.0);
299        }
300
301        // Validate transaction fee using resolved transaction
302        let estimated_fee = TransactionFeeUtil::get_estimate_fee_resolved(rpc_client, self).await?;
303        validator.validate_lamport_fee(estimated_fee)?;
304
305        // Sign transaction
306        let message_bytes = transaction.message.serialize();
307        let signature = signer
308            .sign_message(&message_bytes)
309            .await
310            .map_err(|e| KoraError::SigningError(e.to_string()))?;
311
312        // Find the fee payer position - don't assume it's at position 0
313        let fee_payer_position = self.find_signer_position(&fee_payer)?;
314        transaction.signatures[fee_payer_position] = signature;
315
316        // Serialize signed transaction
317        let serialized = bincode::serialize(&transaction)?;
318        let encoded = STANDARD.encode(serialized);
319
320        Ok((transaction, encoded))
321    }
322
323    async fn sign_and_send_transaction(
324        &mut self,
325        config: &Config,
326        signer: &std::sync::Arc<Signer>,
327        rpc_client: &RpcClient,
328    ) -> Result<(String, String), KoraError> {
329        // Payment validation is handled in sign_transaction
330        let (transaction, encoded) = self.sign_transaction(config, signer, rpc_client).await?;
331
332        // Send and confirm transaction
333        let signature = rpc_client
334            .send_and_confirm_transaction(&transaction)
335            .await
336            .map_err(|e| KoraError::RpcError(e.to_string()))?;
337
338        Ok((signature.to_string(), encoded))
339    }
340}
341
342pub struct LookupTableUtil {}
343
344impl LookupTableUtil {
345    /// Resolves addresses from lookup tables for V0 transactions
346    pub async fn resolve_lookup_table_addresses(
347        config: &Config,
348        rpc_client: &RpcClient,
349        lookup_table_lookups: &[MessageAddressTableLookup],
350    ) -> Result<Vec<Pubkey>, KoraError> {
351        let mut resolved_addresses = Vec::new();
352
353        // Maybe we can use caching here, there's a chance the lookup tables get updated though, so tbd
354        for lookup in lookup_table_lookups {
355            let lookup_table_account =
356                CacheUtil::get_account(config, rpc_client, &lookup.account_key, false)
357                    .await
358                    .map_err(|e| {
359                        KoraError::RpcError(format!("Failed to fetch lookup table: {e}"))
360                    })?;
361
362            // Parse the lookup table account data to get the actual addresses
363            let address_lookup_table = AddressLookupTable::deserialize(&lookup_table_account.data)
364                .map_err(|e| {
365                    KoraError::InvalidTransaction(format!(
366                        "Failed to deserialize lookup table: {e}"
367                    ))
368                })?;
369
370            // Resolve writable addresses
371            for &index in &lookup.writable_indexes {
372                if let Some(address) = address_lookup_table.addresses.get(index as usize) {
373                    resolved_addresses.push(*address);
374                } else {
375                    return Err(KoraError::InvalidTransaction(format!(
376                        "Lookup table index {index} out of bounds for writable addresses"
377                    )));
378                }
379            }
380
381            // Resolve readonly addresses
382            for &index in &lookup.readonly_indexes {
383                if let Some(address) = address_lookup_table.addresses.get(index as usize) {
384                    resolved_addresses.push(*address);
385                } else {
386                    return Err(KoraError::InvalidTransaction(format!(
387                        "Lookup table index {index} out of bounds for readonly addresses"
388                    )));
389                }
390            }
391        }
392
393        Ok(resolved_addresses)
394    }
395}
396
397#[cfg(test)]
398mod tests {
399    use crate::{
400        config::SplTokenConfig,
401        tests::{
402            common::RpcMockBuilder, config_mock::mock_state::setup_config_mock,
403            toml_mock::ConfigBuilder,
404        },
405        transaction::TransactionUtil,
406        Config,
407    };
408    use serde_json::json;
409    use solana_client::rpc_request::RpcRequest;
410    use std::collections::HashMap;
411
412    use super::*;
413    use solana_address_lookup_table_interface::state::LookupTableMeta;
414    use solana_message::{compiled_instruction::CompiledInstruction, v0, Message};
415    use solana_sdk::{
416        account::Account,
417        hash::Hash,
418        instruction::{AccountMeta, Instruction},
419        signature::Keypair,
420        signer::Signer,
421    };
422
423    fn setup_test_config() -> Config {
424        ConfigBuilder::new()
425            .with_programs(vec![])
426            .with_tokens(vec![])
427            .with_spl_paid_tokens(SplTokenConfig::Allowlist(vec![]))
428            .with_free_price()
429            .with_cache_config(None, false, 60, 30) // Disable cache for tests
430            .build_config()
431            .expect("Failed to build test config")
432    }
433
434    #[test]
435    fn test_encode_transaction_b64() {
436        let keypair = Keypair::new();
437        let instruction = Instruction::new_with_bytes(
438            Pubkey::new_unique(),
439            &[1, 2, 3],
440            vec![AccountMeta::new(keypair.pubkey(), true)],
441        );
442        let message =
443            VersionedMessage::Legacy(Message::new(&[instruction], Some(&keypair.pubkey())));
444        let tx = VersionedTransaction::try_new(message, &[&keypair]).unwrap();
445
446        let resolved = VersionedTransactionResolved::from_kora_built_transaction(&tx).unwrap();
447        let encoded = resolved.encode_b64_transaction().unwrap();
448        assert!(!encoded.is_empty());
449        assert!(encoded
450            .chars()
451            .all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '/' || c == '='));
452    }
453
454    #[test]
455    fn test_encode_decode_b64_transaction() {
456        let keypair = Keypair::new();
457        let instruction = Instruction::new_with_bytes(
458            Pubkey::new_unique(),
459            &[1, 2, 3],
460            vec![AccountMeta::new(keypair.pubkey(), true)],
461        );
462        let message =
463            VersionedMessage::Legacy(Message::new(&[instruction], Some(&keypair.pubkey())));
464        let tx = VersionedTransaction::try_new(message, &[&keypair]).unwrap();
465
466        let resolved = VersionedTransactionResolved::from_kora_built_transaction(&tx).unwrap();
467        let encoded = resolved.encode_b64_transaction().unwrap();
468        let decoded = TransactionUtil::decode_b64_transaction(&encoded).unwrap();
469
470        assert_eq!(tx, decoded);
471    }
472
473    #[test]
474    fn test_find_signer_position_success() {
475        let keypair = Keypair::new();
476        let program_id = Pubkey::new_unique();
477        let instruction = Instruction::new_with_bytes(
478            program_id,
479            &[1, 2, 3],
480            vec![AccountMeta::new(keypair.pubkey(), true)],
481        );
482        let message =
483            VersionedMessage::Legacy(Message::new(&[instruction], Some(&keypair.pubkey())));
484        let transaction =
485            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
486
487        let position = transaction.find_signer_position(&keypair.pubkey()).unwrap();
488        assert_eq!(position, 0); // Fee payer is typically at position 0
489    }
490
491    #[test]
492    fn test_find_signer_position_success_v0() {
493        let keypair = Keypair::new();
494        let program_id = Pubkey::new_unique();
495        let other_account = Pubkey::new_unique();
496
497        let v0_message = v0::Message {
498            header: solana_message::MessageHeader {
499                num_required_signatures: 1,
500                num_readonly_signed_accounts: 0,
501                num_readonly_unsigned_accounts: 2,
502            },
503            account_keys: vec![keypair.pubkey(), other_account, program_id],
504            recent_blockhash: Hash::default(),
505            instructions: vec![CompiledInstruction {
506                program_id_index: 2,
507                accounts: vec![0, 1],
508                data: vec![1, 2, 3],
509            }],
510            address_table_lookups: vec![],
511        };
512        let message = VersionedMessage::V0(v0_message);
513        let transaction =
514            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
515
516        let position = transaction.find_signer_position(&keypair.pubkey()).unwrap();
517        assert_eq!(position, 0);
518
519        let other_position = transaction.find_signer_position(&other_account).unwrap();
520        assert_eq!(other_position, 1);
521    }
522
523    #[test]
524    fn test_find_signer_position_middle_of_accounts() {
525        let keypair1 = Keypair::new();
526        let keypair2 = Keypair::new();
527        let keypair3 = Keypair::new();
528        let program_id = Pubkey::new_unique();
529
530        let v0_message = v0::Message {
531            header: solana_message::MessageHeader {
532                num_required_signatures: 3,
533                num_readonly_signed_accounts: 0,
534                num_readonly_unsigned_accounts: 1,
535            },
536            account_keys: vec![keypair1.pubkey(), keypair2.pubkey(), keypair3.pubkey(), program_id],
537            recent_blockhash: Hash::default(),
538            instructions: vec![CompiledInstruction {
539                program_id_index: 3,
540                accounts: vec![0, 1, 2],
541                data: vec![1, 2, 3],
542            }],
543            address_table_lookups: vec![],
544        };
545        let message = VersionedMessage::V0(v0_message);
546        let transaction =
547            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
548
549        assert_eq!(transaction.find_signer_position(&keypair1.pubkey()).unwrap(), 0);
550        assert_eq!(transaction.find_signer_position(&keypair2.pubkey()).unwrap(), 1);
551        assert_eq!(transaction.find_signer_position(&keypair3.pubkey()).unwrap(), 2);
552    }
553
554    #[test]
555    fn test_find_signer_position_not_found() {
556        let keypair = Keypair::new();
557        let missing_keypair = Keypair::new();
558        let instruction = Instruction::new_with_bytes(
559            Pubkey::new_unique(),
560            &[1, 2, 3],
561            vec![AccountMeta::new(keypair.pubkey(), true)],
562        );
563        let message =
564            VersionedMessage::Legacy(Message::new(&[instruction], Some(&keypair.pubkey())));
565        let transaction =
566            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
567
568        let result = transaction.find_signer_position(&missing_keypair.pubkey());
569        assert!(matches!(result, Err(KoraError::InvalidTransaction(_))));
570
571        if let Err(KoraError::InvalidTransaction(msg)) = result {
572            assert!(msg.contains(&missing_keypair.pubkey().to_string()));
573            assert!(msg.contains("not found in transaction account keys"));
574        }
575    }
576
577    #[test]
578    fn test_find_signer_position_empty_account_keys() {
579        // Create a transaction with minimal account keys
580        let v0_message = v0::Message {
581            header: solana_message::MessageHeader {
582                num_required_signatures: 0,
583                num_readonly_signed_accounts: 0,
584                num_readonly_unsigned_accounts: 0,
585            },
586            account_keys: vec![], // Empty account keys
587            recent_blockhash: Hash::default(),
588            instructions: vec![],
589            address_table_lookups: vec![],
590        };
591        let message = VersionedMessage::V0(v0_message);
592        let transaction =
593            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
594        let search_key = Pubkey::new_unique();
595
596        let result = transaction.find_signer_position(&search_key);
597        assert!(matches!(result, Err(KoraError::InvalidTransaction(_))));
598    }
599
600    #[test]
601    fn test_from_kora_built_transaction() {
602        let keypair = Keypair::new();
603        let program_id = Pubkey::new_unique();
604        let instruction = Instruction::new_with_bytes(
605            program_id,
606            &[1, 2, 3, 4],
607            vec![
608                AccountMeta::new(keypair.pubkey(), true),
609                AccountMeta::new_readonly(Pubkey::new_unique(), false),
610            ],
611        );
612        let message = VersionedMessage::Legacy(Message::new(
613            std::slice::from_ref(&instruction),
614            Some(&keypair.pubkey()),
615        ));
616        let transaction = VersionedTransaction::try_new(message.clone(), &[&keypair]).unwrap();
617
618        let resolved =
619            VersionedTransactionResolved::from_kora_built_transaction(&transaction).unwrap();
620
621        assert_eq!(resolved.transaction, transaction);
622        assert_eq!(resolved.all_account_keys, transaction.message.static_account_keys());
623        assert_eq!(resolved.all_instructions.len(), 1);
624
625        // Check instruction properties rather than direct equality since IxUtils::uncompile_instructions
626        // properly sets signer status based on the transaction message
627        let resolved_instruction = &resolved.all_instructions[0];
628        assert_eq!(resolved_instruction.program_id, instruction.program_id);
629        assert_eq!(resolved_instruction.data, instruction.data);
630        assert_eq!(resolved_instruction.accounts.len(), instruction.accounts.len());
631
632        assert!(resolved.parsed_system_instructions.is_none());
633        assert!(resolved.parsed_spl_instructions.is_none());
634    }
635
636    #[test]
637    fn test_from_kora_built_transaction_v0() {
638        let keypair = Keypair::new();
639        let program_id = Pubkey::new_unique();
640        let other_account = Pubkey::new_unique();
641
642        let v0_message = v0::Message {
643            header: solana_message::MessageHeader {
644                num_required_signatures: 1,
645                num_readonly_signed_accounts: 0,
646                num_readonly_unsigned_accounts: 2,
647            },
648            account_keys: vec![keypair.pubkey(), other_account, program_id],
649            recent_blockhash: Hash::new_unique(),
650            instructions: vec![CompiledInstruction {
651                program_id_index: 2,
652                accounts: vec![0, 1],
653                data: vec![1, 2, 3],
654            }],
655            address_table_lookups: vec![],
656        };
657        let message = VersionedMessage::V0(v0_message);
658        let transaction = VersionedTransaction::try_new(message.clone(), &[&keypair]).unwrap();
659
660        let resolved =
661            VersionedTransactionResolved::from_kora_built_transaction(&transaction).unwrap();
662
663        assert_eq!(resolved.transaction, transaction);
664        assert_eq!(resolved.all_account_keys, vec![keypair.pubkey(), other_account, program_id]);
665        assert_eq!(resolved.all_instructions.len(), 1);
666        assert_eq!(resolved.all_instructions[0].program_id, program_id);
667        assert_eq!(resolved.all_instructions[0].accounts.len(), 2);
668        assert_eq!(resolved.all_instructions[0].data, vec![1, 2, 3]);
669    }
670
671    #[tokio::test]
672    async fn test_from_transaction_legacy() {
673        let config = setup_test_config();
674        let _m = setup_config_mock(config.clone());
675
676        let keypair = Keypair::new();
677        let instruction = Instruction::new_with_bytes(
678            Pubkey::new_unique(),
679            &[1, 2, 3],
680            vec![AccountMeta::new(keypair.pubkey(), true)],
681        );
682        let message = VersionedMessage::Legacy(Message::new(
683            std::slice::from_ref(&instruction),
684            Some(&keypair.pubkey()),
685        ));
686        let transaction = VersionedTransaction::try_new(message, &[&keypair]).unwrap();
687
688        // Mock RPC client that will be used for inner instructions
689        let mut mocks = HashMap::new();
690        mocks.insert(
691            RpcRequest::SimulateTransaction,
692            json!({
693                "context": { "slot": 1 },
694                "value": {
695                    "err": null,
696                    "logs": [],
697                    "accounts": null,
698                    "unitsConsumed": 1000,
699                    "innerInstructions": []
700                }
701            }),
702        );
703        let rpc_client = RpcMockBuilder::new().with_custom_mocks(mocks).build();
704
705        let resolved = VersionedTransactionResolved::from_transaction(
706            &transaction,
707            &config,
708            &rpc_client,
709            true,
710        )
711        .await
712        .unwrap();
713
714        assert_eq!(resolved.transaction, transaction);
715        assert_eq!(resolved.all_account_keys, transaction.message.static_account_keys());
716        assert_eq!(resolved.all_instructions.len(), 1); // Only outer instruction since no inner instructions in mock
717
718        // Check instruction properties rather than direct equality since IxUtils::uncompile_instructions
719        // properly sets signer status based on the transaction message
720        let resolved_instruction = &resolved.all_instructions[0];
721        assert_eq!(resolved_instruction.program_id, instruction.program_id);
722        assert_eq!(resolved_instruction.data, instruction.data);
723        assert_eq!(resolved_instruction.accounts.len(), instruction.accounts.len());
724        assert_eq!(resolved_instruction.accounts[0].pubkey, instruction.accounts[0].pubkey);
725        assert_eq!(
726            resolved_instruction.accounts[0].is_writable,
727            instruction.accounts[0].is_writable
728        );
729    }
730
731    #[tokio::test]
732    async fn test_from_transaction_v0_with_lookup_tables() {
733        let config = setup_test_config();
734        let _m = setup_config_mock(config.clone());
735
736        let keypair = Keypair::new();
737        let program_id = Pubkey::new_unique();
738        let lookup_table_account = Pubkey::new_unique();
739        let resolved_address = Pubkey::new_unique();
740
741        // Create lookup table
742        let lookup_table = AddressLookupTable {
743            meta: LookupTableMeta {
744                deactivation_slot: u64::MAX,
745                last_extended_slot: 0,
746                last_extended_slot_start_index: 0,
747                authority: Some(Pubkey::new_unique()),
748                _padding: 0,
749            },
750            addresses: vec![resolved_address].into(),
751        };
752
753        let v0_message = v0::Message {
754            header: solana_message::MessageHeader {
755                num_required_signatures: 1,
756                num_readonly_signed_accounts: 0,
757                num_readonly_unsigned_accounts: 1,
758            },
759            account_keys: vec![keypair.pubkey(), program_id],
760            recent_blockhash: Hash::new_unique(),
761            instructions: vec![CompiledInstruction {
762                program_id_index: 1,
763                accounts: vec![0, 2], // Index 2 comes from lookup table
764                data: vec![42],
765            }],
766            address_table_lookups: vec![solana_message::v0::MessageAddressTableLookup {
767                account_key: lookup_table_account,
768                writable_indexes: vec![0],
769                readonly_indexes: vec![],
770            }],
771        };
772
773        let message = VersionedMessage::V0(v0_message);
774        let transaction = VersionedTransaction::try_new(message, &[&keypair]).unwrap();
775
776        // Create mock RPC client with lookup table account and simulation
777        let mut mocks = HashMap::new();
778        let serialized_data = lookup_table.serialize_for_tests().unwrap();
779        let encoded_data = base64::engine::general_purpose::STANDARD.encode(&serialized_data);
780
781        mocks.insert(
782            RpcRequest::GetAccountInfo,
783            json!({
784                "context": { "slot": 1 },
785                "value": {
786                    "data": [encoded_data, "base64"],
787                    "executable": false,
788                    "lamports": 0,
789                    "owner": "AddressLookupTab1e1111111111111111111111111".to_string(),
790                    "rentEpoch": 0
791                }
792            }),
793        );
794
795        mocks.insert(
796            RpcRequest::SimulateTransaction,
797            json!({
798                "context": { "slot": 1 },
799                "value": {
800                    "err": null,
801                    "logs": [],
802                    "accounts": null,
803                    "unitsConsumed": 1000,
804                    "innerInstructions": []
805                }
806            }),
807        );
808
809        let rpc_client = RpcMockBuilder::new().with_custom_mocks(mocks).build();
810
811        let resolved = VersionedTransactionResolved::from_transaction(
812            &transaction,
813            &config,
814            &rpc_client,
815            true,
816        )
817        .await
818        .unwrap();
819
820        assert_eq!(resolved.transaction, transaction);
821
822        // Should include both static accounts and resolved addresses
823        assert_eq!(resolved.all_account_keys.len(), 3); // keypair, program_id, resolved_address
824        assert_eq!(resolved.all_account_keys[0], keypair.pubkey());
825        assert_eq!(resolved.all_account_keys[1], program_id);
826        assert_eq!(resolved.all_account_keys[2], resolved_address);
827    }
828
829    #[tokio::test]
830    async fn test_from_transaction_simulation_failure() {
831        let config = setup_test_config();
832        let _m = setup_config_mock(config.clone());
833
834        let keypair = Keypair::new();
835        let instruction = Instruction::new_with_bytes(
836            Pubkey::new_unique(),
837            &[1, 2, 3],
838            vec![AccountMeta::new(keypair.pubkey(), true)],
839        );
840        let message =
841            VersionedMessage::Legacy(Message::new(&[instruction], Some(&keypair.pubkey())));
842        let transaction = VersionedTransaction::try_new(message, &[&keypair]).unwrap();
843
844        // Mock RPC client with simulation error
845        let mut mocks = HashMap::new();
846        mocks.insert(
847            RpcRequest::SimulateTransaction,
848            json!({
849                "context": { "slot": 1 },
850                "value": {
851                    "err": "InstructionError",
852                    "logs": ["Some error log"],
853                    "accounts": null,
854                    "unitsConsumed": 0
855                }
856            }),
857        );
858        let rpc_client = RpcMockBuilder::new().with_custom_mocks(mocks).build();
859
860        let result = VersionedTransactionResolved::from_transaction(
861            &transaction,
862            &config,
863            &rpc_client,
864            true,
865        )
866        .await;
867
868        // The simulation should fail, but the exact error type depends on mock implementation
869        // We expect either an RpcError (from mock deserialization) or InvalidTransaction (from simulation logic)
870        assert!(result.is_err());
871
872        match result {
873            Err(KoraError::RpcError(msg)) => {
874                assert!(msg.contains("Failed to simulate transaction"));
875            }
876            Err(KoraError::InvalidTransaction(msg)) => {
877                assert!(msg.contains("inner instructions fetching failed"));
878            }
879            _ => panic!("Expected RpcError or InvalidTransaction"),
880        }
881    }
882
883    #[tokio::test]
884    async fn test_fetch_inner_instructions_with_inner_instructions() {
885        let config = setup_test_config();
886        let _m = setup_config_mock(config);
887
888        let keypair = Keypair::new();
889        let instruction = Instruction::new_with_bytes(
890            Pubkey::new_unique(),
891            &[1, 2, 3],
892            vec![AccountMeta::new(keypair.pubkey(), true)],
893        );
894        let message =
895            VersionedMessage::Legacy(Message::new(&[instruction], Some(&keypair.pubkey())));
896        let transaction = VersionedTransaction::try_new(message, &[&keypair]).unwrap();
897
898        // Mock RPC client with inner instructions
899        let inner_instruction_data = bs58::encode(&[10, 20, 30]).into_string();
900        let mut mocks = HashMap::new();
901        mocks.insert(
902            RpcRequest::SimulateTransaction,
903            json!({
904                "context": { "slot": 1 },
905                "value": {
906                    "err": null,
907                    "logs": [],
908                    "accounts": null,
909                    "unitsConsumed": 1000,
910                    "innerInstructions": [
911                        {
912                            "index": 0,
913                            "instructions": [
914                                {
915                                    "programIdIndex": 1,
916                                    "accounts": [0],
917                                    "data": inner_instruction_data
918                                }
919                            ]
920                        }
921                    ]
922                }
923            }),
924        );
925        let rpc_client = RpcMockBuilder::new().with_custom_mocks(mocks).build();
926
927        let mut resolved =
928            VersionedTransactionResolved::from_kora_built_transaction(&transaction).unwrap();
929        let inner_instructions =
930            resolved.fetch_inner_instructions(&rpc_client, true).await.unwrap();
931
932        assert_eq!(inner_instructions.len(), 1);
933        assert_eq!(inner_instructions[0].data, vec![10, 20, 30]);
934    }
935
936    #[tokio::test]
937    async fn test_fetch_inner_instructions_with_sig_verify_false() {
938        let config = setup_test_config();
939        let _m = setup_config_mock(config);
940
941        let keypair = Keypair::new();
942        let instruction = Instruction::new_with_bytes(
943            Pubkey::new_unique(),
944            &[1, 2, 3],
945            vec![AccountMeta::new(keypair.pubkey(), true)],
946        );
947        let message =
948            VersionedMessage::Legacy(Message::new(&[instruction], Some(&keypair.pubkey())));
949        let transaction = VersionedTransaction::try_new(message, &[&keypair]).unwrap();
950
951        // Mock RPC client with inner instructions
952        let inner_instruction_data = bs58::encode(&[10, 20, 30]).into_string();
953        let mut mocks = HashMap::new();
954        mocks.insert(
955            RpcRequest::SimulateTransaction,
956            json!({
957                "context": { "slot": 1 },
958                "value": {
959                    "err": null,
960                    "logs": [],
961                    "accounts": null,
962                    "unitsConsumed": 1000,
963                    "innerInstructions": [
964                        {
965                            "index": 0,
966                            "instructions": [
967                                {
968                                    "programIdIndex": 1,
969                                    "accounts": [0],
970                                    "data": inner_instruction_data
971                                }
972                            ]
973                        }
974                    ]
975                }
976            }),
977        );
978        let rpc_client = RpcMockBuilder::new().with_custom_mocks(mocks).build();
979
980        let mut resolved =
981            VersionedTransactionResolved::from_kora_built_transaction(&transaction).unwrap();
982        let inner_instructions =
983            resolved.fetch_inner_instructions(&rpc_client, false).await.unwrap();
984
985        assert_eq!(inner_instructions.len(), 1);
986        assert_eq!(inner_instructions[0].data, vec![10, 20, 30]);
987    }
988
989    #[tokio::test]
990    async fn test_get_or_parse_system_instructions() {
991        let config = setup_test_config();
992        let _m = setup_config_mock(config);
993
994        let keypair = Keypair::new();
995        let recipient = Pubkey::new_unique();
996
997        // Create a system transfer instruction
998        let instruction =
999            solana_system_interface::instruction::transfer(&keypair.pubkey(), &recipient, 1000000);
1000        let message =
1001            VersionedMessage::Legacy(Message::new(&[instruction], Some(&keypair.pubkey())));
1002        let transaction = VersionedTransaction::try_new(message, &[&keypair]).unwrap();
1003
1004        let mut resolved =
1005            VersionedTransactionResolved::from_kora_built_transaction(&transaction).unwrap();
1006
1007        // First call should parse and cache
1008        let parsed1_len = {
1009            let parsed1 = resolved.get_or_parse_system_instructions().unwrap();
1010            assert!(!parsed1.is_empty());
1011            parsed1.len()
1012        };
1013
1014        // Second call should return cached result
1015        let parsed2 = resolved.get_or_parse_system_instructions().unwrap();
1016        assert_eq!(parsed1_len, parsed2.len());
1017
1018        // Should contain transfer instruction
1019        assert!(
1020            parsed2.contains_key(&crate::transaction::ParsedSystemInstructionType::SystemTransfer)
1021        );
1022    }
1023
1024    #[tokio::test]
1025    async fn test_resolve_lookup_table_addresses() {
1026        let config = setup_test_config();
1027        let _m = setup_config_mock(config.clone());
1028
1029        let lookup_account_key = Pubkey::new_unique();
1030        let address1 = Pubkey::new_unique();
1031        let address2 = Pubkey::new_unique();
1032        let address3 = Pubkey::new_unique();
1033
1034        let lookup_table = AddressLookupTable {
1035            meta: LookupTableMeta {
1036                deactivation_slot: u64::MAX,
1037                last_extended_slot: 0,
1038                last_extended_slot_start_index: 0,
1039                authority: Some(Pubkey::new_unique()),
1040                _padding: 0,
1041            },
1042            addresses: vec![address1, address2, address3].into(),
1043        };
1044
1045        let serialized_data = lookup_table.serialize_for_tests().unwrap();
1046
1047        let rpc_client = RpcMockBuilder::new()
1048            .with_account_info(&Account {
1049                data: serialized_data,
1050                executable: false,
1051                lamports: 0,
1052                owner: Pubkey::new_unique(),
1053                rent_epoch: 0,
1054            })
1055            .build();
1056
1057        let lookups = vec![solana_message::v0::MessageAddressTableLookup {
1058            account_key: lookup_account_key,
1059            writable_indexes: vec![0, 2], // address1, address3
1060            readonly_indexes: vec![1],    // address2
1061        }];
1062
1063        let resolved_addresses =
1064            LookupTableUtil::resolve_lookup_table_addresses(&config, &rpc_client, &lookups)
1065                .await
1066                .unwrap();
1067
1068        assert_eq!(resolved_addresses.len(), 3);
1069        assert_eq!(resolved_addresses[0], address1);
1070        assert_eq!(resolved_addresses[1], address3);
1071        assert_eq!(resolved_addresses[2], address2);
1072    }
1073
1074    #[tokio::test]
1075    async fn test_resolve_lookup_table_addresses_empty() {
1076        let config = setup_test_config();
1077        let _m = setup_config_mock(config.clone());
1078
1079        let rpc_client = RpcMockBuilder::new().with_account_not_found().build();
1080        let lookups = vec![];
1081
1082        let resolved_addresses =
1083            LookupTableUtil::resolve_lookup_table_addresses(&config, &rpc_client, &lookups)
1084                .await
1085                .unwrap();
1086
1087        assert_eq!(resolved_addresses.len(), 0);
1088    }
1089
1090    #[tokio::test]
1091    async fn test_resolve_lookup_table_addresses_account_not_found() {
1092        let config = setup_test_config();
1093        let _m = setup_config_mock(config.clone());
1094
1095        let rpc_client = RpcMockBuilder::new().with_account_not_found().build();
1096        let lookups = vec![solana_message::v0::MessageAddressTableLookup {
1097            account_key: Pubkey::new_unique(),
1098            writable_indexes: vec![0],
1099            readonly_indexes: vec![],
1100        }];
1101
1102        let result =
1103            LookupTableUtil::resolve_lookup_table_addresses(&config, &rpc_client, &lookups).await;
1104        assert!(matches!(result, Err(KoraError::RpcError(_))));
1105
1106        if let Err(KoraError::RpcError(msg)) = result {
1107            assert!(msg.contains("Failed to fetch lookup table"));
1108        }
1109    }
1110
1111    #[tokio::test]
1112    async fn test_resolve_lookup_table_addresses_invalid_index() {
1113        let config = setup_test_config();
1114        let _m = setup_config_mock(config.clone());
1115
1116        let lookup_account_key = Pubkey::new_unique();
1117        let address1 = Pubkey::new_unique();
1118
1119        let lookup_table = AddressLookupTable {
1120            meta: LookupTableMeta {
1121                deactivation_slot: u64::MAX,
1122                last_extended_slot: 0,
1123                last_extended_slot_start_index: 0,
1124                authority: Some(Pubkey::new_unique()),
1125                _padding: 0,
1126            },
1127            addresses: vec![address1].into(), // Only 1 address, index 0
1128        };
1129
1130        let serialized_data = lookup_table.serialize_for_tests().unwrap();
1131        let rpc_client = RpcMockBuilder::new()
1132            .with_account_info(&Account {
1133                data: serialized_data,
1134                executable: false,
1135                lamports: 0,
1136                owner: Pubkey::new_unique(),
1137                rent_epoch: 0,
1138            })
1139            .build();
1140
1141        // Try to access index 1 which doesn't exist
1142        let lookups = vec![solana_message::v0::MessageAddressTableLookup {
1143            account_key: lookup_account_key,
1144            writable_indexes: vec![1], // Invalid index
1145            readonly_indexes: vec![],
1146        }];
1147
1148        let result =
1149            LookupTableUtil::resolve_lookup_table_addresses(&config, &rpc_client, &lookups).await;
1150        assert!(matches!(result, Err(KoraError::InvalidTransaction(_))));
1151
1152        if let Err(KoraError::InvalidTransaction(msg)) = result {
1153            assert!(msg.contains("index 1 out of bounds"));
1154            assert!(msg.contains("writable addresses"));
1155        }
1156    }
1157
1158    #[tokio::test]
1159    async fn test_resolve_lookup_table_addresses_invalid_readonly_index() {
1160        let config = setup_test_config();
1161        let _m = setup_config_mock(config.clone());
1162
1163        let lookup_account_key = Pubkey::new_unique();
1164        let address1 = Pubkey::new_unique();
1165
1166        let lookup_table = AddressLookupTable {
1167            meta: LookupTableMeta {
1168                deactivation_slot: u64::MAX,
1169                last_extended_slot: 0,
1170                last_extended_slot_start_index: 0,
1171                authority: Some(Pubkey::new_unique()),
1172                _padding: 0,
1173            },
1174            addresses: vec![address1].into(),
1175        };
1176
1177        let serialized_data = lookup_table.serialize_for_tests().unwrap();
1178        let rpc_client = RpcMockBuilder::new()
1179            .with_account_info(&Account {
1180                data: serialized_data,
1181                executable: false,
1182                lamports: 0,
1183                owner: Pubkey::new_unique(),
1184                rent_epoch: 0,
1185            })
1186            .build();
1187
1188        let lookups = vec![solana_message::v0::MessageAddressTableLookup {
1189            account_key: lookup_account_key,
1190            writable_indexes: vec![],
1191            readonly_indexes: vec![5], // Invalid index
1192        }];
1193
1194        let result =
1195            LookupTableUtil::resolve_lookup_table_addresses(&config, &rpc_client, &lookups).await;
1196        assert!(matches!(result, Err(KoraError::InvalidTransaction(_))));
1197
1198        if let Err(KoraError::InvalidTransaction(msg)) = result {
1199            assert!(msg.contains("index 5 out of bounds"));
1200            assert!(msg.contains("readonly addresses"));
1201        }
1202    }
1203}