Skip to main content

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