Skip to main content

kora_lib/transaction/
instruction_util.rs

1use std::collections::HashMap;
2
3use solana_message::compiled_instruction::CompiledInstruction;
4use solana_sdk::{
5    instruction::{AccountMeta, Instruction},
6    pubkey::Pubkey,
7};
8use solana_system_interface::{instruction::SystemInstruction, program::ID as SYSTEM_PROGRAM_ID};
9use solana_transaction_status_client_types::{
10    UiInstruction, UiParsedInstruction, UiPartiallyDecodedInstruction,
11};
12
13use crate::{
14    constant::instruction_indexes, error::KoraError, sanitize_error,
15    transaction::VersionedTransactionResolved,
16};
17
18// Instruction type that we support to parse from the transaction
19#[derive(Debug, Clone, PartialEq, Eq, Hash)]
20pub enum ParsedSystemInstructionType {
21    SystemTransfer,
22    SystemCreateAccount,
23    SystemWithdrawNonceAccount,
24    SystemAssign,
25    SystemAllocate,
26    SystemInitializeNonceAccount,
27    SystemAdvanceNonceAccount,
28    SystemAuthorizeNonceAccount,
29    // Note: SystemUpgradeNonceAccount not included - no authority parameter
30}
31
32// Instruction type that we support to parse from the transaction
33#[derive(Debug, Clone, PartialEq, Eq, Hash)]
34pub enum ParsedSystemInstructionData {
35    // Includes transfer and transfer with seed
36    SystemTransfer { lamports: u64, sender: Pubkey, receiver: Pubkey },
37    // Includes create account and create account with seed
38    SystemCreateAccount { lamports: u64, payer: Pubkey },
39    // Includes withdraw nonce account
40    SystemWithdrawNonceAccount { lamports: u64, nonce_authority: Pubkey, recipient: Pubkey },
41    // Includes assign and assign with seed
42    SystemAssign { authority: Pubkey },
43    // Includes allocate and allocate with seed
44    SystemAllocate { account: Pubkey },
45    // Initialize nonce account
46    SystemInitializeNonceAccount { nonce_account: Pubkey, nonce_authority: Pubkey },
47    // Advance nonce account
48    SystemAdvanceNonceAccount { nonce_account: Pubkey, nonce_authority: Pubkey },
49    // Authorize nonce account
50    SystemAuthorizeNonceAccount { nonce_account: Pubkey, nonce_authority: Pubkey },
51    // Note: SystemUpgradeNonceAccount not included - no authority parameter
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, Hash)]
55pub enum ParsedSPLInstructionType {
56    SplTokenTransfer,
57    SplTokenBurn,
58    SplTokenCloseAccount,
59    SplTokenApprove,
60    SplTokenRevoke,
61    SplTokenSetAuthority,
62    SplTokenMintTo,
63    SplTokenInitializeMint,
64    SplTokenInitializeAccount,
65    SplTokenInitializeMultisig,
66    SplTokenFreezeAccount,
67    SplTokenThawAccount,
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, Hash)]
71pub enum ParsedSPLInstructionData {
72    // Includes transfer and transfer with seed (both spl and spl 2022)
73    SplTokenTransfer {
74        amount: u64,
75        owner: Pubkey,
76        mint: Option<Pubkey>,
77        source_address: Pubkey,
78        destination_address: Pubkey,
79        is_2022: bool,
80    },
81    // Includes burn and burn with seed
82    SplTokenBurn {
83        owner: Pubkey,
84        is_2022: bool,
85    },
86    // Includes close account
87    SplTokenCloseAccount {
88        owner: Pubkey,
89        is_2022: bool,
90    },
91    // Includes approve and approve with seed
92    SplTokenApprove {
93        owner: Pubkey,
94        is_2022: bool,
95    },
96    // Revoke
97    SplTokenRevoke {
98        owner: Pubkey,
99        is_2022: bool,
100    },
101    // SetAuthority
102    SplTokenSetAuthority {
103        authority: Pubkey,
104        is_2022: bool,
105    },
106    // MintTo and MintToChecked
107    SplTokenMintTo {
108        mint_authority: Pubkey,
109        is_2022: bool,
110    },
111    // InitializeMint and InitializeMint2
112    SplTokenInitializeMint {
113        mint_authority: Pubkey,
114        is_2022: bool,
115    },
116    // InitializeAccount, InitializeAccount2, InitializeAccount3
117    SplTokenInitializeAccount {
118        owner: Pubkey,
119        is_2022: bool,
120    },
121    // InitializeMultisig and InitializeMultisig2
122    SplTokenInitializeMultisig {
123        signers: Vec<Pubkey>,
124        is_2022: bool,
125    },
126    // FreezeAccount
127    SplTokenFreezeAccount {
128        freeze_authority: Pubkey,
129        is_2022: bool,
130    },
131    // ThawAccount
132    SplTokenThawAccount {
133        freeze_authority: Pubkey,
134        is_2022: bool,
135    },
136}
137
138/// Macro to validate that an instruction has the required number of accounts
139/// Usage: validate_accounts!(instruction, min_count)
140macro_rules! validate_number_accounts {
141    ($instruction:expr, $min_count:expr) => {
142        if $instruction.accounts.len() < $min_count {
143            log::error!("Instruction {:?} has less than {} accounts", $instruction, $min_count);
144            return Err(KoraError::InvalidTransaction(format!(
145                "Instruction doesn't have the required number of accounts",
146            )));
147        }
148    };
149}
150
151/// Macro to parse system instructions with validation and account extraction
152/// Usage: parse_system_instruction!(parsed_instructions, instruction, validate_module, EnumVariant, DataVariant { fields })
153macro_rules! parse_system_instruction {
154    // Simple version: separate constant module path and enum variant names
155    ($parsed:ident, $ix:ident, $const_mod:ident, $enum_variant:ident, $data_variant:ident { $($field:ident: $account_path:expr),* $(,)? }) => {
156        validate_number_accounts!($ix, instruction_indexes::$const_mod::REQUIRED_NUMBER_OF_ACCOUNTS);
157        $parsed
158            .entry(ParsedSystemInstructionType::$enum_variant)
159            .or_default()
160            .push(ParsedSystemInstructionData::$data_variant {
161                $($field: $ix.accounts[$account_path].pubkey,)*
162            });
163    };
164    // Version with extra fields (like lamports) that come from instruction data
165    ($parsed:ident, $ix:ident, $const_mod:ident, $enum_variant:ident, $data_variant:ident { $($data_field:ident: $data_val:expr),* ; $($field:ident: $account_path:expr),* $(,)? }) => {
166        validate_number_accounts!($ix, instruction_indexes::$const_mod::REQUIRED_NUMBER_OF_ACCOUNTS);
167        $parsed
168            .entry(ParsedSystemInstructionType::$enum_variant)
169            .or_default()
170            .push(ParsedSystemInstructionData::$data_variant {
171                $($data_field: $data_val,)*
172                $($field: $ix.accounts[$account_path].pubkey,)*
173            });
174    };
175}
176
177pub struct IxUtils;
178
179pub const PARSED_DATA_FIELD_TYPE: &str = "type";
180pub const PARSED_DATA_FIELD_INFO: &str = "info";
181
182pub const PARSED_DATA_FIELD_SOURCE: &str = "source";
183pub const PARSED_DATA_FIELD_DESTINATION: &str = "destination";
184pub const PARSED_DATA_FIELD_OWNER: &str = "owner";
185
186pub const PARSED_DATA_FIELD_TRANSFER: &str = "transfer";
187pub const PARSED_DATA_FIELD_CREATE_ACCOUNT: &str = "createAccount";
188pub const PARSED_DATA_FIELD_ASSIGN: &str = "assign";
189pub const PARSED_DATA_FIELD_TRANSFER_WITH_SEED: &str = "transferWithSeed";
190pub const PARSED_DATA_FIELD_CREATE_ACCOUNT_WITH_SEED: &str = "createAccountWithSeed";
191pub const PARSED_DATA_FIELD_ASSIGN_WITH_SEED: &str = "assignWithSeed";
192pub const PARSED_DATA_FIELD_WITHDRAW_NONCE_ACCOUNT: &str = "withdrawFromNonce";
193pub const PARSED_DATA_FIELD_ALLOCATE: &str = "allocate";
194pub const PARSED_DATA_FIELD_ALLOCATE_WITH_SEED: &str = "allocateWithSeed";
195pub const PARSED_DATA_FIELD_INITIALIZE_NONCE_ACCOUNT: &str = "initializeNonce";
196pub const PARSED_DATA_FIELD_ADVANCE_NONCE_ACCOUNT: &str = "advanceNonce";
197pub const PARSED_DATA_FIELD_AUTHORIZE_NONCE_ACCOUNT: &str = "authorizeNonce";
198pub const PARSED_DATA_FIELD_BURN: &str = "burn";
199pub const PARSED_DATA_FIELD_BURN_CHECKED: &str = "burnChecked";
200pub const PARSED_DATA_FIELD_CLOSE_ACCOUNT: &str = "closeAccount";
201pub const PARSED_DATA_FIELD_TRANSFER_CHECKED: &str = "transferChecked";
202pub const PARSED_DATA_FIELD_APPROVE: &str = "approve";
203pub const PARSED_DATA_FIELD_APPROVE_CHECKED: &str = "approveChecked";
204
205pub const PARSED_DATA_FIELD_AMOUNT: &str = "amount";
206pub const PARSED_DATA_FIELD_LAMPORTS: &str = "lamports";
207pub const PARSED_DATA_FIELD_DECIMALS: &str = "decimals";
208pub const PARSED_DATA_FIELD_UI_AMOUNT: &str = "uiAmount";
209pub const PARSED_DATA_FIELD_UI_AMOUNT_STRING: &str = "uiAmountString";
210pub const PARSED_DATA_FIELD_TOKEN_AMOUNT: &str = "tokenAmount";
211pub const PARSED_DATA_FIELD_ACCOUNT: &str = "account";
212pub const PARSED_DATA_FIELD_NEW_ACCOUNT: &str = "newAccount";
213pub const PARSED_DATA_FIELD_AUTHORITY: &str = "authority";
214pub const PARSED_DATA_FIELD_MINT: &str = "mint";
215pub const PARSED_DATA_FIELD_SPACE: &str = "space";
216pub const PARSED_DATA_FIELD_DELEGATE: &str = "delegate";
217pub const PARSED_DATA_FIELD_BASE: &str = "base";
218pub const PARSED_DATA_FIELD_SEED: &str = "seed";
219pub const PARSED_DATA_FIELD_SOURCE_BASE: &str = "sourceBase";
220pub const PARSED_DATA_FIELD_SOURCE_SEED: &str = "sourceSeed";
221pub const PARSED_DATA_FIELD_SOURCE_OWNER: &str = "sourceOwner";
222pub const PARSED_DATA_FIELD_NONCE_ACCOUNT: &str = "nonceAccount";
223pub const PARSED_DATA_FIELD_RECIPIENT: &str = "recipient";
224pub const PARSED_DATA_FIELD_NONCE_AUTHORITY: &str = "nonceAuthority";
225pub const PARSED_DATA_FIELD_NEW_AUTHORITY: &str = "newAuthority";
226
227// SPL Token instruction type constants
228pub const PARSED_DATA_FIELD_REVOKE: &str = "revoke";
229pub const PARSED_DATA_FIELD_SET_AUTHORITY: &str = "setAuthority";
230pub const PARSED_DATA_FIELD_MINT_TO: &str = "mintTo";
231pub const PARSED_DATA_FIELD_MINT_TO_CHECKED: &str = "mintToChecked";
232pub const PARSED_DATA_FIELD_INITIALIZE_MINT: &str = "initializeMint";
233pub const PARSED_DATA_FIELD_INITIALIZE_MINT2: &str = "initializeMint2";
234pub const PARSED_DATA_FIELD_INITIALIZE_ACCOUNT: &str = "initializeAccount";
235pub const PARSED_DATA_FIELD_INITIALIZE_ACCOUNT2: &str = "initializeAccount2";
236pub const PARSED_DATA_FIELD_INITIALIZE_ACCOUNT3: &str = "initializeAccount3";
237pub const PARSED_DATA_FIELD_INITIALIZE_MULTISIG: &str = "initializeMultisig";
238pub const PARSED_DATA_FIELD_INITIALIZE_MULTISIG2: &str = "initializeMultisig2";
239pub const PARSED_DATA_FIELD_FREEZE_ACCOUNT: &str = "freezeAccount";
240pub const PARSED_DATA_FIELD_THAW_ACCOUNT: &str = "thawAccount";
241pub const PARSED_DATA_FIELD_GET_ACCOUNT_DATA_SIZE: &str = "getAccountDataSize";
242pub const PARSED_DATA_FIELD_INITIALIZE_IMMUTABLE_OWNER: &str = "initializeImmutableOwner";
243
244// Additional field names for new instructions
245pub const PARSED_DATA_FIELD_MINT_AUTHORITY: &str = "mintAuthority";
246pub const PARSED_DATA_FIELD_FREEZE_AUTHORITY: &str = "freezeAuthority";
247pub const PARSED_DATA_FIELD_AUTHORITY_TYPE: &str = "authorityType";
248pub const PARSED_DATA_FIELD_MULTISIG_ACCOUNT: &str = "multisig";
249pub const PARSED_DATA_FIELD_SIGNERS: &str = "signers";
250
251impl IxUtils {
252    /// Helper method to extract a field as a string from JSON with proper error handling
253    fn get_field_as_str<'a>(
254        info: &'a serde_json::Value,
255        field_name: &str,
256    ) -> Result<&'a str, KoraError> {
257        info.get(field_name)
258            .ok_or_else(|| {
259                KoraError::SerializationError(format!("Missing field '{}'", field_name))
260            })?
261            .as_str()
262            .ok_or_else(|| {
263                KoraError::SerializationError(format!("Field '{}' is not a string", field_name))
264            })
265    }
266
267    /// Helper method to extract a field as a Pubkey from JSON with proper error handling
268    fn get_field_as_pubkey(
269        info: &serde_json::Value,
270        field_name: &str,
271    ) -> Result<Pubkey, KoraError> {
272        let pubkey_str = Self::get_field_as_str(info, field_name)?;
273        pubkey_str.parse::<Pubkey>().map_err(|e| {
274            KoraError::SerializationError(format!(
275                "Field '{}' is not a valid pubkey: {}",
276                field_name, e
277            ))
278        })
279    }
280
281    /// Helper method to extract a field as u64 from JSON string with proper error handling
282    fn get_field_as_u64(info: &serde_json::Value, field_name: &str) -> Result<u64, KoraError> {
283        let value = info.get(field_name).ok_or_else(|| {
284            KoraError::SerializationError(format!("Missing field '{}'", field_name))
285        })?;
286
287        // Try as native JSON number first
288        if let Some(num) = value.as_u64() {
289            return Ok(num);
290        }
291
292        // Fall back to string parsing
293        if let Some(str_val) = value.as_str() {
294            return str_val.parse::<u64>().map_err(|e| {
295                KoraError::SerializationError(format!(
296                    "Field '{}' is not a valid u64: {}",
297                    field_name, e
298                ))
299            });
300        }
301
302        Err(KoraError::SerializationError(format!(
303            "Field '{}' is neither a number nor a string",
304            field_name
305        )))
306    }
307
308    /// Helper method to get account index from hashmap with proper error handling
309    fn get_account_index(
310        account_keys_hashmap: &HashMap<Pubkey, u8>,
311        pubkey: &Pubkey,
312    ) -> Result<u8, KoraError> {
313        account_keys_hashmap.get(pubkey).copied().ok_or_else(|| {
314            KoraError::SerializationError(format!("{} not found in account keys", pubkey))
315        })
316    }
317
318    pub fn build_account_keys_hashmap(account_keys: &[Pubkey]) -> HashMap<Pubkey, u8> {
319        account_keys.iter().enumerate().map(|(idx, key)| (*key, idx as u8)).collect()
320    }
321
322    fn decode_base58_instruction_data(
323        encoded_data: &str,
324        error_context: &str,
325    ) -> Result<Vec<u8>, KoraError> {
326        bs58::decode(encoded_data).into_vec().map_err(|e| {
327            KoraError::SerializationError(format!(
328                "Failed to decode {error_context} from base58: {}",
329                sanitize_error!(e)
330            ))
331        })
332    }
333
334    fn get_or_insert_account_index(
335        account_keys_hashmap: &mut HashMap<Pubkey, u8>,
336        all_account_keys: &mut Vec<Pubkey>,
337        pubkey: Pubkey,
338        overflow_context: &str,
339        inserted_keys: &mut Vec<Pubkey>,
340    ) -> Result<u8, KoraError> {
341        if let Some(&idx) = account_keys_hashmap.get(&pubkey) {
342            return Ok(idx);
343        }
344
345        let idx = u8::try_from(all_account_keys.len()).map_err(|_| {
346            KoraError::SerializationError(format!(
347                "{overflow_context} exceeds u8 index limit (len={})",
348                all_account_keys.len()
349            ))
350        })?;
351
352        all_account_keys.push(pubkey);
353        account_keys_hashmap.insert(pubkey, idx);
354        inserted_keys.push(pubkey);
355
356        Ok(idx)
357    }
358
359    fn rollback_account_key_extensions(
360        account_keys_hashmap: &mut HashMap<Pubkey, u8>,
361        all_account_keys: &mut Vec<Pubkey>,
362        snapshot_len: usize,
363        inserted_keys: &[Pubkey],
364    ) {
365        all_account_keys.truncate(snapshot_len);
366        for key in inserted_keys {
367            account_keys_hashmap.remove(key);
368        }
369    }
370
371    pub fn get_account_key_if_present(ix: &Instruction, index: usize) -> Option<Pubkey> {
372        if ix.accounts.is_empty() {
373            return None;
374        }
375
376        if index >= ix.accounts.len() {
377            return None;
378        }
379
380        Some(ix.accounts[index].pubkey)
381    }
382
383    pub fn get_account_key_required(
384        account_keys: &[Pubkey],
385        index: usize,
386    ) -> Result<Pubkey, KoraError> {
387        account_keys.get(index).copied().ok_or_else(|| {
388            KoraError::SerializationError(format!("Account key at index {} not found", index))
389        })
390    }
391
392    fn parse_burn_owner_with_mint_fallback(instruction: &Instruction) -> Result<Pubkey, KoraError> {
393        // Standard Burn has 3 accounts: [source, mint, authority]
394        // Reconstructed from parsed RPC data may have only 2: [source, authority]
395        // (Solana RPC doesn't include mint in parsed "burn" non-checked JSON)
396        if instruction.accounts.len()
397            >= instruction_indexes::spl_token_burn::REQUIRED_NUMBER_OF_ACCOUNTS
398        {
399            return Ok(
400                instruction.accounts[instruction_indexes::spl_token_burn::OWNER_INDEX].pubkey
401            );
402        }
403
404        if instruction.accounts.len() == 2 {
405            log::debug!(
406                "Burn instruction has 2 accounts (reconstructed without mint), using index 1 as authority"
407            );
408            return Ok(instruction.accounts[1].pubkey);
409        }
410
411        log::error!("Burn instruction has less than 2 accounts: {:?}", instruction);
412        Err(KoraError::InvalidTransaction(
413            "Burn instruction doesn't have the required number of accounts".to_string(),
414        ))
415    }
416
417    fn extract_multisig_signers(instruction: &Instruction, skip: usize) -> Vec<Pubkey> {
418        instruction.accounts.iter().skip(skip).map(|a| a.pubkey).collect()
419    }
420
421    fn push_parsed_spl_instruction(
422        parsed_instructions: &mut HashMap<ParsedSPLInstructionType, Vec<ParsedSPLInstructionData>>,
423        instruction_type: ParsedSPLInstructionType,
424        instruction_data: ParsedSPLInstructionData,
425    ) {
426        parsed_instructions.entry(instruction_type).or_default().push(instruction_data);
427    }
428
429    pub fn build_default_compiled_instruction(program_id_index: u8) -> CompiledInstruction {
430        CompiledInstruction { program_id_index, accounts: vec![], data: vec![] }
431    }
432
433    pub fn uncompile_instructions(
434        instructions: &[CompiledInstruction],
435        account_keys: &[Pubkey],
436    ) -> Result<Vec<Instruction>, KoraError> {
437        instructions
438            .iter()
439            .map(|ix| {
440                let program_id =
441                    Self::get_account_key_required(account_keys, ix.program_id_index as usize)?;
442                let accounts: Result<Vec<AccountMeta>, KoraError> = ix
443                    .accounts
444                    .iter()
445                    .map(|idx| {
446                        Ok(AccountMeta {
447                            pubkey: Self::get_account_key_required(account_keys, *idx as usize)?,
448                            is_signer: false,
449                            is_writable: true,
450                        })
451                    })
452                    .collect();
453
454                Ok(Instruction { program_id, accounts: accounts?, data: ix.data.clone() })
455            })
456            .collect()
457    }
458
459    /// Reconstruct a CompiledInstruction from various UiInstruction formats
460    ///
461    /// This is required because when you simulate a transaction with inner instructions flag,
462    /// the RPC pre-parses some of the instructions (like for SPL program and System Program),
463    /// however this is an issue for Kora, as we expected "Compiled" instructions rather than "Parsed" instructions,
464    /// because we have our own parsing logic on our Kora's side.
465    ///
466    /// So we need to reconstruct the "Compiled" instructions from the "Parsed" instructions, by "unparsing" the "Parsed" instructions.
467    ///
468    /// There's no known way to force the RPC to not parsed the instructions, so we need this "hack" to reverse the process.
469    ///
470    /// Example: https://github.com/anza-xyz/agave/blob/68032b576dc4c14b31c15974c6734ae1513980a3/transaction-status/src/parse_system.rs#L11
471    pub fn reconstruct_instruction_from_ui(
472        ui_instruction: &UiInstruction,
473        all_account_keys: &mut Vec<Pubkey>,
474    ) -> Result<CompiledInstruction, KoraError> {
475        let mut account_keys_hashmap = Self::build_account_keys_hashmap(all_account_keys);
476        Self::reconstruct_instruction_from_ui_with_account_key_cache(
477            ui_instruction,
478            all_account_keys,
479            &mut account_keys_hashmap,
480        )
481    }
482
483    pub fn reconstruct_instruction_from_ui_with_account_key_cache(
484        ui_instruction: &UiInstruction,
485        all_account_keys: &mut Vec<Pubkey>,
486        account_keys_hashmap: &mut HashMap<Pubkey, u8>,
487    ) -> Result<CompiledInstruction, KoraError> {
488        match ui_instruction {
489            UiInstruction::Compiled(compiled) => {
490                let data = Self::decode_base58_instruction_data(
491                    &compiled.data,
492                    "compiled instruction data",
493                )?;
494
495                Ok(CompiledInstruction {
496                    program_id_index: compiled.program_id_index,
497                    accounts: compiled.accounts.clone(),
498                    data,
499                })
500            }
501            UiInstruction::Parsed(ui_parsed) => Self::reconstruct_parsed_instruction(
502                ui_parsed,
503                all_account_keys,
504                account_keys_hashmap,
505            ),
506        }
507    }
508
509    fn reconstruct_parsed_instruction(
510        ui_parsed: &UiParsedInstruction,
511        all_account_keys: &mut Vec<Pubkey>,
512        account_keys_hashmap: &mut HashMap<Pubkey, u8>,
513    ) -> Result<CompiledInstruction, KoraError> {
514        match ui_parsed {
515            UiParsedInstruction::Parsed(parsed) => {
516                // Reconstruct based on program type
517                if parsed.program_id == SYSTEM_PROGRAM_ID.to_string() {
518                    Self::reconstruct_system_instruction(parsed, account_keys_hashmap)
519                } else if parsed.program_id == spl_token_interface::ID.to_string()
520                    || parsed.program_id == spl_token_2022_interface::ID.to_string()
521                {
522                    Self::reconstruct_spl_token_instruction(parsed, account_keys_hashmap)
523                } else {
524                    // For unsupported programs, create a stub instruction with just the program ID
525                    // This ensures the program ID is preserved for security validation
526                    let program_id = parsed.program_id.parse::<Pubkey>().map_err(|e| {
527                        KoraError::SerializationError(format!(
528                            "Invalid parsed instruction program_id '{}': {}",
529                            parsed.program_id,
530                            sanitize_error!(e)
531                        ))
532                    })?;
533                    let program_id_index = *account_keys_hashmap.get(&program_id).ok_or_else(|| {
534                        KoraError::SerializationError(format!(
535                            "Unsupported parsed instruction program_id {} not found in account keys",
536                            program_id
537                        ))
538                    })?;
539
540                    Ok(Self::build_default_compiled_instruction(program_id_index))
541                }
542            }
543            UiParsedInstruction::PartiallyDecoded(partial) => {
544                Self::reconstruct_partially_decoded_instruction(
545                    partial,
546                    all_account_keys,
547                    account_keys_hashmap,
548                )
549            }
550        }
551    }
552
553    fn reconstruct_partially_decoded_instruction(
554        partial: &UiPartiallyDecodedInstruction,
555        all_account_keys: &mut Vec<Pubkey>,
556        account_keys_hashmap: &mut HashMap<Pubkey, u8>,
557    ) -> Result<CompiledInstruction, KoraError> {
558        let snapshot_len = all_account_keys.len();
559        let mut inserted_keys = Vec::new();
560        let result = (|| -> Result<CompiledInstruction, KoraError> {
561            let program_id = partial.program_id.parse::<Pubkey>().map_err(|e| {
562                KoraError::SerializationError(format!(
563                    "Invalid partially decoded instruction program_id '{}': {}",
564                    partial.program_id,
565                    sanitize_error!(e)
566                ))
567            })?;
568
569            // Program ID itself may be a CPI-only account not in the
570            // outer transaction's keys (e.g. an invoked program via CPI).
571            let program_idx = Self::get_or_insert_account_index(
572                account_keys_hashmap,
573                all_account_keys,
574                program_id,
575                "CPI program key",
576                &mut inserted_keys,
577            )?;
578
579            // Convert account addresses to indices, extending the keys vec
580            // for any CPI PDA accounts not in the original keys.
581            let mut account_indices: Vec<u8> = Vec::with_capacity(partial.accounts.len());
582            for addr_str in &partial.accounts {
583                let pubkey = addr_str.parse::<Pubkey>().map_err(|e| {
584                    KoraError::SerializationError(format!(
585                        "Invalid partially decoded instruction account '{}': {}",
586                        addr_str,
587                        sanitize_error!(e)
588                    ))
589                })?;
590
591                // CPI PDA signers (e.g. Kamino lending authority) are not in
592                // the outer transaction's account keys.
593                let idx = Self::get_or_insert_account_index(
594                    account_keys_hashmap,
595                    all_account_keys,
596                    pubkey,
597                    "CPI account key",
598                    &mut inserted_keys,
599                )?;
600                account_indices.push(idx);
601            }
602
603            let data = Self::decode_base58_instruction_data(
604                &partial.data,
605                "partially decoded instruction data",
606            )?;
607
608            Ok(CompiledInstruction {
609                program_id_index: program_idx,
610                accounts: account_indices,
611                data,
612            })
613        })();
614
615        if result.is_err() {
616            Self::rollback_account_key_extensions(
617                account_keys_hashmap,
618                all_account_keys,
619                snapshot_len,
620                &inserted_keys,
621            );
622        }
623
624        result
625    }
626
627    /// Reconstruct system program instructions from parsed format
628    fn reconstruct_system_instruction(
629        parsed: &solana_transaction_status_client_types::ParsedInstruction,
630        account_keys_hashmap: &HashMap<Pubkey, u8>,
631    ) -> Result<CompiledInstruction, KoraError> {
632        let program_id_index = Self::get_account_index(account_keys_hashmap, &SYSTEM_PROGRAM_ID)?;
633
634        let parsed_data = &parsed.parsed;
635        let instruction_type = Self::get_field_as_str(parsed_data, PARSED_DATA_FIELD_TYPE)?;
636        let info = parsed_data
637            .get(PARSED_DATA_FIELD_INFO)
638            .ok_or_else(|| KoraError::SerializationError("Missing 'info' field".to_string()))?;
639
640        match instruction_type {
641            PARSED_DATA_FIELD_TRANSFER => {
642                let source = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_SOURCE)?;
643                let destination = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_DESTINATION)?;
644                let lamports = Self::get_field_as_u64(info, PARSED_DATA_FIELD_LAMPORTS)?;
645
646                let source_idx = Self::get_account_index(account_keys_hashmap, &source)?;
647                let destination_idx = Self::get_account_index(account_keys_hashmap, &destination)?;
648
649                let transfer_ix = SystemInstruction::Transfer { lamports };
650                let data = bincode::serialize(&transfer_ix).map_err(|e| {
651                    KoraError::SerializationError(format!(
652                        "Failed to serialize Transfer instruction: {}",
653                        e
654                    ))
655                })?;
656
657                Ok(CompiledInstruction {
658                    program_id_index,
659                    accounts: vec![source_idx, destination_idx],
660                    data,
661                })
662            }
663            PARSED_DATA_FIELD_CREATE_ACCOUNT => {
664                let source = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_SOURCE)?;
665                let new_account = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_NEW_ACCOUNT)?;
666                let owner = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_OWNER)?;
667                let lamports = Self::get_field_as_u64(info, PARSED_DATA_FIELD_LAMPORTS)?;
668                let space = Self::get_field_as_u64(info, PARSED_DATA_FIELD_SPACE)?;
669
670                let source_idx = Self::get_account_index(account_keys_hashmap, &source)?;
671                let new_account_idx = Self::get_account_index(account_keys_hashmap, &new_account)?;
672
673                let create_ix = SystemInstruction::CreateAccount { lamports, space, owner };
674                let data = bincode::serialize(&create_ix).map_err(|e| {
675                    KoraError::SerializationError(format!(
676                        "Failed to serialize CreateAccount instruction: {}",
677                        e
678                    ))
679                })?;
680
681                Ok(CompiledInstruction {
682                    program_id_index,
683                    accounts: vec![source_idx, new_account_idx],
684                    data,
685                })
686            }
687            PARSED_DATA_FIELD_ASSIGN => {
688                let authority = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_ACCOUNT)?;
689                let owner = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_OWNER)?;
690
691                let authority_idx = Self::get_account_index(account_keys_hashmap, &authority)?;
692
693                let assign_ix = SystemInstruction::Assign { owner };
694                let data = bincode::serialize(&assign_ix).map_err(|e| {
695                    KoraError::SerializationError(format!(
696                        "Failed to serialize Assign instruction: {}",
697                        e
698                    ))
699                })?;
700
701                Ok(CompiledInstruction { program_id_index, accounts: vec![authority_idx], data })
702            }
703            PARSED_DATA_FIELD_TRANSFER_WITH_SEED => {
704                let source = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_SOURCE)?;
705                let destination = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_DESTINATION)?;
706                let lamports = Self::get_field_as_u64(info, PARSED_DATA_FIELD_LAMPORTS)?;
707                let source_base = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_SOURCE_BASE)?;
708                let source_seed =
709                    Self::get_field_as_str(info, PARSED_DATA_FIELD_SOURCE_SEED)?.to_string();
710                let source_owner = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_SOURCE_OWNER)?;
711
712                let source_idx = Self::get_account_index(account_keys_hashmap, &source)?;
713                let destination_idx = Self::get_account_index(account_keys_hashmap, &destination)?;
714                let source_base_idx = Self::get_account_index(account_keys_hashmap, &source_base)?;
715
716                let transfer_ix = SystemInstruction::TransferWithSeed {
717                    lamports,
718                    from_seed: source_seed,
719                    from_owner: source_owner,
720                };
721                let data = bincode::serialize(&transfer_ix).map_err(|e| {
722                    KoraError::SerializationError(format!(
723                        "Failed to serialize TransferWithSeed instruction: {}",
724                        e
725                    ))
726                })?;
727
728                Ok(CompiledInstruction {
729                    program_id_index,
730                    accounts: vec![source_idx, source_base_idx, destination_idx],
731                    data,
732                })
733            }
734            PARSED_DATA_FIELD_CREATE_ACCOUNT_WITH_SEED => {
735                let source = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_SOURCE)?;
736                let new_account = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_NEW_ACCOUNT)?;
737                let base = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_BASE)?;
738                let seed = Self::get_field_as_str(info, PARSED_DATA_FIELD_SEED)?.to_string();
739                let owner = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_OWNER)?;
740                let lamports = Self::get_field_as_u64(info, PARSED_DATA_FIELD_LAMPORTS)?;
741                let space = Self::get_field_as_u64(info, PARSED_DATA_FIELD_SPACE)?;
742
743                let source_idx = Self::get_account_index(account_keys_hashmap, &source)?;
744                let new_account_idx = Self::get_account_index(account_keys_hashmap, &new_account)?;
745                let base_idx = Self::get_account_index(account_keys_hashmap, &base)?;
746
747                let create_ix =
748                    SystemInstruction::CreateAccountWithSeed { base, seed, lamports, space, owner };
749                let data = bincode::serialize(&create_ix).map_err(|e| {
750                    KoraError::SerializationError(format!(
751                        "Failed to serialize CreateAccountWithSeed instruction: {}",
752                        e
753                    ))
754                })?;
755
756                Ok(CompiledInstruction {
757                    program_id_index,
758                    accounts: vec![source_idx, new_account_idx, base_idx],
759                    data,
760                })
761            }
762            PARSED_DATA_FIELD_ASSIGN_WITH_SEED => {
763                let account = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_ACCOUNT)?;
764                let base = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_BASE)?;
765                let seed = Self::get_field_as_str(info, PARSED_DATA_FIELD_SEED)?.to_string();
766                let owner = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_OWNER)?;
767
768                let account_idx = Self::get_account_index(account_keys_hashmap, &account)?;
769                let base_idx = Self::get_account_index(account_keys_hashmap, &base)?;
770
771                let assign_ix = SystemInstruction::AssignWithSeed { base, seed, owner };
772                let data = bincode::serialize(&assign_ix).map_err(|e| {
773                    KoraError::SerializationError(format!(
774                        "Failed to serialize AssignWithSeed instruction: {}",
775                        e
776                    ))
777                })?;
778
779                Ok(CompiledInstruction {
780                    program_id_index,
781                    accounts: vec![account_idx, base_idx],
782                    data,
783                })
784            }
785            PARSED_DATA_FIELD_WITHDRAW_NONCE_ACCOUNT => {
786                let nonce_account =
787                    Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_NONCE_ACCOUNT)?;
788                let recipient = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_DESTINATION)?;
789                let nonce_authority =
790                    Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_NONCE_AUTHORITY)?;
791                let lamports = Self::get_field_as_u64(info, PARSED_DATA_FIELD_LAMPORTS)?;
792
793                let nonce_account_idx =
794                    Self::get_account_index(account_keys_hashmap, &nonce_account)?;
795                let recipient_idx = Self::get_account_index(account_keys_hashmap, &recipient)?;
796                let nonce_authority_idx =
797                    Self::get_account_index(account_keys_hashmap, &nonce_authority)?;
798
799                let withdraw_ix = SystemInstruction::WithdrawNonceAccount(lamports);
800                let data = bincode::serialize(&withdraw_ix).map_err(|e| {
801                    KoraError::SerializationError(format!(
802                        "Failed to serialize WithdrawNonceAccount instruction: {}",
803                        e
804                    ))
805                })?;
806
807                Ok(CompiledInstruction {
808                    program_id_index,
809                    accounts: vec![nonce_account_idx, recipient_idx, nonce_authority_idx],
810                    data,
811                })
812            }
813            PARSED_DATA_FIELD_ALLOCATE => {
814                let account = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_ACCOUNT)?;
815                let space = Self::get_field_as_u64(info, PARSED_DATA_FIELD_SPACE)?;
816
817                let account_idx = Self::get_account_index(account_keys_hashmap, &account)?;
818
819                let allocate_ix = SystemInstruction::Allocate { space };
820                let data = bincode::serialize(&allocate_ix).map_err(|e| {
821                    KoraError::SerializationError(format!(
822                        "Failed to serialize Allocate instruction: {}",
823                        e
824                    ))
825                })?;
826
827                Ok(CompiledInstruction { program_id_index, accounts: vec![account_idx], data })
828            }
829            PARSED_DATA_FIELD_ALLOCATE_WITH_SEED => {
830                let account = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_ACCOUNT)?;
831                let base = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_BASE)?;
832                let seed = Self::get_field_as_str(info, PARSED_DATA_FIELD_SEED)?.to_string();
833                let space = Self::get_field_as_u64(info, PARSED_DATA_FIELD_SPACE)?;
834                let owner = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_OWNER)?;
835
836                let account_idx = Self::get_account_index(account_keys_hashmap, &account)?;
837                let base_idx = Self::get_account_index(account_keys_hashmap, &base)?;
838
839                let allocate_ix = SystemInstruction::AllocateWithSeed { base, seed, space, owner };
840                let data = bincode::serialize(&allocate_ix).map_err(|e| {
841                    KoraError::SerializationError(format!(
842                        "Failed to serialize AllocateWithSeed instruction: {}",
843                        e
844                    ))
845                })?;
846
847                Ok(CompiledInstruction {
848                    program_id_index,
849                    accounts: vec![account_idx, base_idx],
850                    data,
851                })
852            }
853            PARSED_DATA_FIELD_INITIALIZE_NONCE_ACCOUNT => {
854                let nonce_account =
855                    Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_NONCE_ACCOUNT)?;
856                let nonce_authority =
857                    Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_NONCE_AUTHORITY)?;
858
859                let nonce_account_idx =
860                    Self::get_account_index(account_keys_hashmap, &nonce_account)?;
861
862                let initialize_ix = SystemInstruction::InitializeNonceAccount(nonce_authority);
863                let data = bincode::serialize(&initialize_ix).map_err(|e| {
864                    KoraError::SerializationError(format!(
865                        "Failed to serialize InitializeNonceAccount instruction: {}",
866                        e
867                    ))
868                })?;
869
870                // Accounts: [nonce_account, recent_blockhashes_sysvar, rent_sysvar]
871                // We only have nonce_account in the hashmap for inner instructions
872                Ok(CompiledInstruction {
873                    program_id_index,
874                    accounts: vec![nonce_account_idx],
875                    data,
876                })
877            }
878            PARSED_DATA_FIELD_ADVANCE_NONCE_ACCOUNT => {
879                let nonce_account =
880                    Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_NONCE_ACCOUNT)?;
881                let nonce_authority =
882                    Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_NONCE_AUTHORITY)?;
883
884                let nonce_account_idx =
885                    Self::get_account_index(account_keys_hashmap, &nonce_account)?;
886                let nonce_authority_idx =
887                    Self::get_account_index(account_keys_hashmap, &nonce_authority)?;
888
889                let advance_ix = SystemInstruction::AdvanceNonceAccount;
890                let data = bincode::serialize(&advance_ix).map_err(|e| {
891                    KoraError::SerializationError(format!(
892                        "Failed to serialize AdvanceNonceAccount instruction: {}",
893                        e
894                    ))
895                })?;
896
897                // Accounts: [nonce_account, recent_blockhashes_sysvar, nonce_authority]
898                // We only include accounts that are in the hashmap (from inner instructions)
899                Ok(CompiledInstruction {
900                    program_id_index,
901                    accounts: vec![nonce_account_idx, nonce_authority_idx],
902                    data,
903                })
904            }
905            PARSED_DATA_FIELD_AUTHORIZE_NONCE_ACCOUNT => {
906                let nonce_account =
907                    Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_NONCE_ACCOUNT)?;
908                let nonce_authority =
909                    Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_NONCE_AUTHORITY)?;
910                let new_authority =
911                    Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_NEW_AUTHORITY)?;
912
913                let nonce_account_idx =
914                    Self::get_account_index(account_keys_hashmap, &nonce_account)?;
915                let nonce_authority_idx =
916                    Self::get_account_index(account_keys_hashmap, &nonce_authority)?;
917
918                let authorize_ix = SystemInstruction::AuthorizeNonceAccount(new_authority);
919                let data = bincode::serialize(&authorize_ix).map_err(|e| {
920                    KoraError::SerializationError(format!(
921                        "Failed to serialize AuthorizeNonceAccount instruction: {}",
922                        e
923                    ))
924                })?;
925
926                // Accounts: [nonce_account, nonce_authority]
927                Ok(CompiledInstruction {
928                    program_id_index,
929                    accounts: vec![nonce_account_idx, nonce_authority_idx],
930                    data,
931                })
932            }
933            _ => {
934                Err(KoraError::InvalidTransaction(format!(
935                    "Unrecognized system instruction type '{}' in CPI — cannot validate fee payer policy",
936                    instruction_type
937                )))
938            }
939        }
940    }
941
942    /// Reconstruct SPL token program instructions from parsed format
943    fn reconstruct_spl_token_instruction(
944        parsed: &solana_transaction_status_client_types::ParsedInstruction,
945        account_keys_hashmap: &HashMap<Pubkey, u8>,
946    ) -> Result<CompiledInstruction, KoraError> {
947        let program_id = parsed.program_id.parse::<Pubkey>().map_err(|e| {
948            KoraError::SerializationError(format!("Invalid program ID: {}", sanitize_error!(e)))
949        })?;
950        let program_id_index = Self::get_account_index(account_keys_hashmap, &program_id)?;
951        let is_spl_token_program = program_id == spl_token_interface::ID;
952
953        let parsed_data = &parsed.parsed;
954        let instruction_type = Self::get_field_as_str(parsed_data, PARSED_DATA_FIELD_TYPE)?;
955        let info = parsed_data
956            .get(PARSED_DATA_FIELD_INFO)
957            .ok_or_else(|| KoraError::SerializationError("Missing 'info' field".to_string()))?;
958
959        match instruction_type {
960            PARSED_DATA_FIELD_TRANSFER => {
961                let source = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_SOURCE)?;
962                let destination = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_DESTINATION)?;
963                let authority = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_AUTHORITY)?;
964                let amount = Self::get_field_as_u64(info, PARSED_DATA_FIELD_AMOUNT)?;
965
966                let source_idx = Self::get_account_index(account_keys_hashmap, &source)?;
967                let destination_idx = Self::get_account_index(account_keys_hashmap, &destination)?;
968                let authority_idx = Self::get_account_index(account_keys_hashmap, &authority)?;
969
970                let data = if is_spl_token_program {
971                    spl_token_interface::instruction::TokenInstruction::Transfer { amount }.pack()
972                } else {
973                    #[allow(deprecated)]
974                    spl_token_2022_interface::instruction::TokenInstruction::Transfer { amount }
975                        .pack()
976                };
977
978                Ok(CompiledInstruction {
979                    program_id_index,
980                    accounts: vec![source_idx, destination_idx, authority_idx],
981                    data,
982                })
983            }
984            PARSED_DATA_FIELD_TRANSFER_CHECKED => {
985                let source = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_SOURCE)?;
986                let destination = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_DESTINATION)?;
987                let authority = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_AUTHORITY)?;
988                let mint = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_MINT)?;
989
990                let token_amount = info.get(PARSED_DATA_FIELD_TOKEN_AMOUNT).ok_or_else(|| {
991                    KoraError::SerializationError("Missing 'tokenAmount' field".to_string())
992                })?;
993                let amount = Self::get_field_as_u64(token_amount, PARSED_DATA_FIELD_AMOUNT)?;
994                let decimals =
995                    Self::get_field_as_u64(token_amount, PARSED_DATA_FIELD_DECIMALS)? as u8;
996
997                let source_idx = Self::get_account_index(account_keys_hashmap, &source)?;
998                let mint_idx = Self::get_account_index(account_keys_hashmap, &mint)?;
999                let destination_idx = Self::get_account_index(account_keys_hashmap, &destination)?;
1000                let authority_idx = Self::get_account_index(account_keys_hashmap, &authority)?;
1001
1002                let data = if is_spl_token_program {
1003                    spl_token_interface::instruction::TokenInstruction::TransferChecked {
1004                        amount,
1005                        decimals,
1006                    }
1007                    .pack()
1008                } else {
1009                    spl_token_2022_interface::instruction::TokenInstruction::TransferChecked {
1010                        amount,
1011                        decimals,
1012                    }
1013                    .pack()
1014                };
1015
1016                Ok(CompiledInstruction {
1017                    program_id_index,
1018                    accounts: vec![source_idx, mint_idx, destination_idx, authority_idx],
1019                    data,
1020                })
1021            }
1022            PARSED_DATA_FIELD_BURN | PARSED_DATA_FIELD_BURN_CHECKED => {
1023                let account = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_ACCOUNT)?;
1024                let authority = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_AUTHORITY)?;
1025
1026                let (amount, decimals) = if instruction_type == PARSED_DATA_FIELD_BURN_CHECKED {
1027                    let token_amount =
1028                        info.get(PARSED_DATA_FIELD_TOKEN_AMOUNT).ok_or_else(|| {
1029                            KoraError::SerializationError(
1030                                "Missing 'tokenAmount' field for burnChecked".to_string(),
1031                            )
1032                        })?;
1033                    let amount = Self::get_field_as_u64(token_amount, PARSED_DATA_FIELD_AMOUNT)?;
1034                    let decimals =
1035                        Self::get_field_as_u64(token_amount, PARSED_DATA_FIELD_DECIMALS)? as u8;
1036                    (amount, Some(decimals))
1037                } else {
1038                    let amount =
1039                        Self::get_field_as_u64(info, PARSED_DATA_FIELD_AMOUNT).unwrap_or(0);
1040                    (amount, None)
1041                };
1042
1043                let account_idx = Self::get_account_index(account_keys_hashmap, &account)?;
1044                let authority_idx = Self::get_account_index(account_keys_hashmap, &authority)?;
1045
1046                let accounts = if instruction_type == PARSED_DATA_FIELD_BURN_CHECKED {
1047                    let mint = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_MINT)?;
1048                    let mint_idx = Self::get_account_index(account_keys_hashmap, &mint)?;
1049                    vec![account_idx, mint_idx, authority_idx]
1050                } else if let Ok(mint) = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_MINT) {
1051                    // Solana's parser often includes mint for non-checked burn. If the
1052                    // mint index is unavailable, fall back to [source, authority].
1053                    if let Ok(mint_idx) = Self::get_account_index(account_keys_hashmap, &mint) {
1054                        vec![account_idx, mint_idx, authority_idx]
1055                    } else {
1056                        vec![account_idx, authority_idx]
1057                    }
1058                } else {
1059                    // Defensive fallback for providers omitting mint in parsed burn JSON.
1060                    vec![account_idx, authority_idx]
1061                };
1062
1063                let data = if instruction_type == PARSED_DATA_FIELD_BURN_CHECKED {
1064                    let decimals = decimals.unwrap(); // Safe because we set it above for burnChecked
1065                    if is_spl_token_program {
1066                        spl_token_interface::instruction::TokenInstruction::BurnChecked {
1067                            amount,
1068                            decimals,
1069                        }
1070                        .pack()
1071                    } else {
1072                        spl_token_2022_interface::instruction::TokenInstruction::BurnChecked {
1073                            amount,
1074                            decimals,
1075                        }
1076                        .pack()
1077                    }
1078                } else if is_spl_token_program {
1079                    spl_token_interface::instruction::TokenInstruction::Burn { amount }.pack()
1080                } else {
1081                    spl_token_2022_interface::instruction::TokenInstruction::Burn { amount }.pack()
1082                };
1083
1084                Ok(CompiledInstruction { program_id_index, accounts, data })
1085            }
1086            PARSED_DATA_FIELD_CLOSE_ACCOUNT => {
1087                let account = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_ACCOUNT)?;
1088                let destination = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_DESTINATION)?;
1089                let authority = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_OWNER)?;
1090
1091                let account_idx = Self::get_account_index(account_keys_hashmap, &account)?;
1092                let destination_idx = Self::get_account_index(account_keys_hashmap, &destination)?;
1093                let authority_idx = Self::get_account_index(account_keys_hashmap, &authority)?;
1094
1095                let data = if is_spl_token_program {
1096                    spl_token_interface::instruction::TokenInstruction::CloseAccount.pack()
1097                } else {
1098                    spl_token_2022_interface::instruction::TokenInstruction::CloseAccount.pack()
1099                };
1100
1101                Ok(CompiledInstruction {
1102                    program_id_index,
1103                    accounts: vec![account_idx, destination_idx, authority_idx],
1104                    data,
1105                })
1106            }
1107            PARSED_DATA_FIELD_APPROVE => {
1108                let source = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_SOURCE)?;
1109                let delegate = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_DELEGATE)?;
1110                let owner = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_OWNER)?;
1111                let amount = Self::get_field_as_u64(info, PARSED_DATA_FIELD_AMOUNT)?;
1112
1113                let source_idx = Self::get_account_index(account_keys_hashmap, &source)?;
1114                let delegate_idx = Self::get_account_index(account_keys_hashmap, &delegate)?;
1115                let owner_idx = Self::get_account_index(account_keys_hashmap, &owner)?;
1116
1117                let data = if is_spl_token_program {
1118                    spl_token_interface::instruction::TokenInstruction::Approve { amount }.pack()
1119                } else {
1120                    spl_token_2022_interface::instruction::TokenInstruction::Approve { amount }
1121                        .pack()
1122                };
1123
1124                Ok(CompiledInstruction {
1125                    program_id_index,
1126                    accounts: vec![source_idx, delegate_idx, owner_idx],
1127                    data,
1128                })
1129            }
1130            PARSED_DATA_FIELD_APPROVE_CHECKED => {
1131                let source = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_SOURCE)?;
1132                let delegate = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_DELEGATE)?;
1133                let owner = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_OWNER)?;
1134                let mint = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_MINT)?;
1135
1136                let token_amount = info.get(PARSED_DATA_FIELD_TOKEN_AMOUNT).ok_or_else(|| {
1137                    KoraError::SerializationError("Missing 'tokenAmount' field".to_string())
1138                })?;
1139                let amount = Self::get_field_as_u64(token_amount, PARSED_DATA_FIELD_AMOUNT)?;
1140                let decimals =
1141                    Self::get_field_as_u64(token_amount, PARSED_DATA_FIELD_DECIMALS)? as u8;
1142
1143                let source_idx = Self::get_account_index(account_keys_hashmap, &source)?;
1144                let mint_idx = Self::get_account_index(account_keys_hashmap, &mint)?;
1145                let delegate_idx = Self::get_account_index(account_keys_hashmap, &delegate)?;
1146                let owner_idx = Self::get_account_index(account_keys_hashmap, &owner)?;
1147
1148                let data = if is_spl_token_program {
1149                    spl_token_interface::instruction::TokenInstruction::ApproveChecked {
1150                        amount,
1151                        decimals,
1152                    }
1153                    .pack()
1154                } else {
1155                    spl_token_2022_interface::instruction::TokenInstruction::ApproveChecked {
1156                        amount,
1157                        decimals,
1158                    }
1159                    .pack()
1160                };
1161
1162                Ok(CompiledInstruction {
1163                    program_id_index,
1164                    accounts: vec![source_idx, mint_idx, delegate_idx, owner_idx],
1165                    data,
1166                })
1167            }
1168            PARSED_DATA_FIELD_REVOKE => {
1169                let source = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_SOURCE)?;
1170                let owner = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_OWNER)?;
1171
1172                let source_idx = Self::get_account_index(account_keys_hashmap, &source)?;
1173                let owner_idx = Self::get_account_index(account_keys_hashmap, &owner)?;
1174
1175                let data = if is_spl_token_program {
1176                    spl_token_interface::instruction::TokenInstruction::Revoke.pack()
1177                } else {
1178                    spl_token_2022_interface::instruction::TokenInstruction::Revoke.pack()
1179                };
1180
1181                Ok(CompiledInstruction {
1182                    program_id_index,
1183                    accounts: vec![source_idx, owner_idx],
1184                    data,
1185                })
1186            }
1187            PARSED_DATA_FIELD_SET_AUTHORITY => {
1188                let account = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_ACCOUNT)?;
1189                let current_authority =
1190                    Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_AUTHORITY)?;
1191
1192                let account_idx = Self::get_account_index(account_keys_hashmap, &account)?;
1193                let current_authority_idx =
1194                    Self::get_account_index(account_keys_hashmap, &current_authority)?;
1195
1196                // SetAuthority has variable data - we reconstruct minimal version
1197                // Real validation happens when checking if fee payer is the authority
1198                let data = vec![6];
1199
1200                Ok(CompiledInstruction {
1201                    program_id_index,
1202                    accounts: vec![account_idx, current_authority_idx],
1203                    data,
1204                })
1205            }
1206            PARSED_DATA_FIELD_MINT_TO => {
1207                let mint = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_MINT)?;
1208                let account = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_ACCOUNT)?;
1209                let mint_authority =
1210                    Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_MINT_AUTHORITY)?;
1211                let amount = Self::get_field_as_u64(info, PARSED_DATA_FIELD_AMOUNT)?;
1212
1213                let mint_idx = Self::get_account_index(account_keys_hashmap, &mint)?;
1214                let account_idx = Self::get_account_index(account_keys_hashmap, &account)?;
1215                let mint_authority_idx =
1216                    Self::get_account_index(account_keys_hashmap, &mint_authority)?;
1217
1218                let data = if is_spl_token_program {
1219                    spl_token_interface::instruction::TokenInstruction::MintTo { amount }.pack()
1220                } else {
1221                    spl_token_2022_interface::instruction::TokenInstruction::MintTo { amount }
1222                        .pack()
1223                };
1224
1225                Ok(CompiledInstruction {
1226                    program_id_index,
1227                    accounts: vec![mint_idx, account_idx, mint_authority_idx],
1228                    data,
1229                })
1230            }
1231            PARSED_DATA_FIELD_MINT_TO_CHECKED => {
1232                let mint = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_MINT)?;
1233                let account = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_ACCOUNT)?;
1234                let mint_authority =
1235                    Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_MINT_AUTHORITY)?;
1236
1237                let token_amount = info.get(PARSED_DATA_FIELD_TOKEN_AMOUNT).ok_or_else(|| {
1238                    KoraError::SerializationError("Missing 'tokenAmount' field".to_string())
1239                })?;
1240                let amount = Self::get_field_as_u64(token_amount, PARSED_DATA_FIELD_AMOUNT)?;
1241                let decimals =
1242                    Self::get_field_as_u64(token_amount, PARSED_DATA_FIELD_DECIMALS)? as u8;
1243
1244                let mint_idx = Self::get_account_index(account_keys_hashmap, &mint)?;
1245                let account_idx = Self::get_account_index(account_keys_hashmap, &account)?;
1246                let mint_authority_idx =
1247                    Self::get_account_index(account_keys_hashmap, &mint_authority)?;
1248
1249                let data = if is_spl_token_program {
1250                    spl_token_interface::instruction::TokenInstruction::MintToChecked {
1251                        amount,
1252                        decimals,
1253                    }
1254                    .pack()
1255                } else {
1256                    spl_token_2022_interface::instruction::TokenInstruction::MintToChecked {
1257                        amount,
1258                        decimals,
1259                    }
1260                    .pack()
1261                };
1262
1263                Ok(CompiledInstruction {
1264                    program_id_index,
1265                    accounts: vec![mint_idx, account_idx, mint_authority_idx],
1266                    data,
1267                })
1268            }
1269            PARSED_DATA_FIELD_INITIALIZE_MINT | PARSED_DATA_FIELD_INITIALIZE_MINT2 => {
1270                let mint = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_MINT)?;
1271                // mint_authority is in instruction data, not used for reconstruction
1272                let _mint_authority =
1273                    Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_MINT_AUTHORITY)?;
1274
1275                let mint_idx = Self::get_account_index(account_keys_hashmap, &mint)?;
1276
1277                // InitializeMint has discriminator only, authority is in data
1278                let data = if instruction_type == PARSED_DATA_FIELD_INITIALIZE_MINT {
1279                    vec![0] // InitializeMint discriminator
1280                } else {
1281                    vec![20] // InitializeMint2 discriminator
1282                };
1283
1284                Ok(CompiledInstruction { program_id_index, accounts: vec![mint_idx], data })
1285            }
1286            PARSED_DATA_FIELD_INITIALIZE_ACCOUNT
1287            | PARSED_DATA_FIELD_INITIALIZE_ACCOUNT2
1288            | PARSED_DATA_FIELD_INITIALIZE_ACCOUNT3 => {
1289                let account = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_ACCOUNT)?;
1290                let mint = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_MINT)?;
1291                let owner = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_OWNER)?;
1292
1293                let account_idx = Self::get_account_index(account_keys_hashmap, &account)?;
1294                let mint_idx = Self::get_account_index(account_keys_hashmap, &mint)?;
1295
1296                // Different variants have different account structures and discriminators
1297                let (data, accounts) = match instruction_type {
1298                    PARSED_DATA_FIELD_INITIALIZE_ACCOUNT => {
1299                        // InitializeAccount: [account, mint, owner, rent]
1300                        // Owner is in accounts, not data
1301                        let owner_idx = Self::get_account_index(account_keys_hashmap, &owner)?;
1302                        (vec![1], vec![account_idx, mint_idx, owner_idx])
1303                    }
1304                    PARSED_DATA_FIELD_INITIALIZE_ACCOUNT2 => {
1305                        // InitializeAccount2: [account, mint, rent], owner in data
1306                        (vec![16], vec![account_idx, mint_idx])
1307                    }
1308                    PARSED_DATA_FIELD_INITIALIZE_ACCOUNT3 => {
1309                        // InitializeAccount3: [account, mint], owner in data
1310                        (vec![18], vec![account_idx, mint_idx])
1311                    }
1312                    _ => unreachable!(),
1313                };
1314
1315                Ok(CompiledInstruction { program_id_index, accounts, data })
1316            }
1317            PARSED_DATA_FIELD_INITIALIZE_MULTISIG | PARSED_DATA_FIELD_INITIALIZE_MULTISIG2 => {
1318                let multisig = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_MULTISIG_ACCOUNT)?;
1319                let multisig_idx = Self::get_account_index(account_keys_hashmap, &multisig)?;
1320
1321                // Extract signer pubkeys from signers array (not currently used for reconstruction)
1322                let _signers_value = info.get(PARSED_DATA_FIELD_SIGNERS).ok_or_else(|| {
1323                    KoraError::SerializationError("Missing 'signers' field".to_string())
1324                })?;
1325
1326                // Discriminator based on instruction variant
1327                let data = if instruction_type == PARSED_DATA_FIELD_INITIALIZE_MULTISIG {
1328                    vec![2] // InitializeMultisig discriminator
1329                } else {
1330                    vec![19] // InitializeMultisig2 discriminator
1331                };
1332
1333                Ok(CompiledInstruction { program_id_index, accounts: vec![multisig_idx], data })
1334            }
1335            PARSED_DATA_FIELD_FREEZE_ACCOUNT => {
1336                let account = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_ACCOUNT)?;
1337                let mint = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_MINT)?;
1338                let freeze_authority =
1339                    Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_FREEZE_AUTHORITY)?;
1340
1341                let account_idx = Self::get_account_index(account_keys_hashmap, &account)?;
1342                let mint_idx = Self::get_account_index(account_keys_hashmap, &mint)?;
1343                let freeze_authority_idx =
1344                    Self::get_account_index(account_keys_hashmap, &freeze_authority)?;
1345
1346                let data = if is_spl_token_program {
1347                    spl_token_interface::instruction::TokenInstruction::FreezeAccount.pack()
1348                } else {
1349                    spl_token_2022_interface::instruction::TokenInstruction::FreezeAccount.pack()
1350                };
1351
1352                Ok(CompiledInstruction {
1353                    program_id_index,
1354                    accounts: vec![account_idx, mint_idx, freeze_authority_idx],
1355                    data,
1356                })
1357            }
1358            PARSED_DATA_FIELD_THAW_ACCOUNT => {
1359                let account = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_ACCOUNT)?;
1360                let mint = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_MINT)?;
1361                let freeze_authority =
1362                    Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_FREEZE_AUTHORITY)?;
1363
1364                let account_idx = Self::get_account_index(account_keys_hashmap, &account)?;
1365                let mint_idx = Self::get_account_index(account_keys_hashmap, &mint)?;
1366                let freeze_authority_idx =
1367                    Self::get_account_index(account_keys_hashmap, &freeze_authority)?;
1368
1369                let data = if is_spl_token_program {
1370                    spl_token_interface::instruction::TokenInstruction::ThawAccount.pack()
1371                } else {
1372                    spl_token_2022_interface::instruction::TokenInstruction::ThawAccount.pack()
1373                };
1374
1375                Ok(CompiledInstruction {
1376                    program_id_index,
1377                    accounts: vec![account_idx, mint_idx, freeze_authority_idx],
1378                    data,
1379                })
1380            }
1381            PARSED_DATA_FIELD_GET_ACCOUNT_DATA_SIZE => {
1382                let mint = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_MINT)?;
1383                let mint_idx = Self::get_account_index(account_keys_hashmap, &mint)?;
1384
1385                let data = if is_spl_token_program {
1386                    spl_token_interface::instruction::TokenInstruction::GetAccountDataSize.pack()
1387                } else {
1388                    spl_token_2022_interface::instruction::TokenInstruction::GetAccountDataSize {
1389                        extension_types: vec![],
1390                    }
1391                    .pack()
1392                };
1393
1394                Ok(CompiledInstruction {
1395                    program_id_index,
1396                    accounts: vec![mint_idx],
1397                    data,
1398                })
1399            }
1400            PARSED_DATA_FIELD_INITIALIZE_IMMUTABLE_OWNER => {
1401                let account = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_ACCOUNT)?;
1402                let account_idx = Self::get_account_index(account_keys_hashmap, &account)?;
1403
1404                let data = if is_spl_token_program {
1405                    spl_token_interface::instruction::TokenInstruction::InitializeImmutableOwner
1406                        .pack()
1407                } else {
1408                    spl_token_2022_interface::instruction::TokenInstruction::InitializeImmutableOwner
1409                        .pack()
1410                };
1411
1412                Ok(CompiledInstruction {
1413                    program_id_index,
1414                    accounts: vec![account_idx],
1415                    data,
1416                })
1417            }
1418            _ => {
1419                Err(KoraError::InvalidTransaction(format!(
1420                    "Unrecognized SPL Token instruction type '{}' in CPI — cannot validate fee payer policy",
1421                    instruction_type
1422                )))
1423            }
1424        }
1425    }
1426
1427    pub fn parse_system_instructions(
1428        transaction: &VersionedTransactionResolved,
1429    ) -> Result<HashMap<ParsedSystemInstructionType, Vec<ParsedSystemInstructionData>>, KoraError>
1430    {
1431        let mut parsed_instructions: HashMap<
1432            ParsedSystemInstructionType,
1433            Vec<ParsedSystemInstructionData>,
1434        > = HashMap::new();
1435
1436        for instruction in transaction.all_instructions.iter() {
1437            let program_id = instruction.program_id;
1438
1439            // Handle System Program transfers and account creation
1440            if program_id == SYSTEM_PROGRAM_ID {
1441                match bincode::deserialize::<SystemInstruction>(&instruction.data) {
1442                    Ok(SystemInstruction::CreateAccount { lamports, .. })
1443                    | Ok(SystemInstruction::CreateAccountWithSeed { lamports, .. }) => {
1444                        parse_system_instruction!(parsed_instructions, instruction, system_create_account, SystemCreateAccount, SystemCreateAccount {
1445                            lamports: lamports;
1446                            payer: instruction_indexes::system_create_account::PAYER_INDEX
1447                        });
1448                    }
1449                    Ok(SystemInstruction::Transfer { lamports }) => {
1450                        parse_system_instruction!(parsed_instructions, instruction, system_transfer, SystemTransfer, SystemTransfer {
1451                            lamports: lamports;
1452                            sender: instruction_indexes::system_transfer::SENDER_INDEX,
1453                            receiver: instruction_indexes::system_transfer::RECEIVER_INDEX
1454                        });
1455                    }
1456                    Ok(SystemInstruction::TransferWithSeed { lamports, .. }) => {
1457                        // Note: uses system_transfer_with_seed for validation but maps to SystemTransfer type
1458                        validate_number_accounts!(instruction, instruction_indexes::system_transfer_with_seed::REQUIRED_NUMBER_OF_ACCOUNTS);
1459                        parsed_instructions
1460                            .entry(ParsedSystemInstructionType::SystemTransfer)
1461                            .or_default()
1462                            .push(ParsedSystemInstructionData::SystemTransfer {
1463                                lamports,
1464                                sender: instruction.accounts[instruction_indexes::system_transfer_with_seed::SENDER_INDEX].pubkey,
1465                                receiver: instruction.accounts[instruction_indexes::system_transfer_with_seed::RECEIVER_INDEX].pubkey,
1466                            });
1467                    }
1468                    Ok(SystemInstruction::WithdrawNonceAccount(lamports)) => {
1469                        parse_system_instruction!(parsed_instructions, instruction, system_withdraw_nonce_account, SystemWithdrawNonceAccount, SystemWithdrawNonceAccount {
1470                            lamports: lamports;
1471                            nonce_authority: instruction_indexes::system_withdraw_nonce_account::NONCE_AUTHORITY_INDEX,
1472                            recipient: instruction_indexes::system_withdraw_nonce_account::RECIPIENT_INDEX
1473                        });
1474                    }
1475                    Ok(SystemInstruction::Assign { .. }) => {
1476                        parse_system_instruction!(
1477                            parsed_instructions,
1478                            instruction,
1479                            system_assign,
1480                            SystemAssign,
1481                            SystemAssign {
1482                                authority: instruction_indexes::system_assign::AUTHORITY_INDEX
1483                            }
1484                        );
1485                    }
1486                    Ok(SystemInstruction::AssignWithSeed { .. }) => {
1487                        // Note: uses system_assign_with_seed for validation but maps to SystemAssign type
1488                        validate_number_accounts!(instruction, instruction_indexes::system_assign_with_seed::REQUIRED_NUMBER_OF_ACCOUNTS);
1489                        parsed_instructions
1490                            .entry(ParsedSystemInstructionType::SystemAssign)
1491                            .or_default()
1492                            .push(ParsedSystemInstructionData::SystemAssign {
1493                                authority: instruction.accounts
1494                                    [instruction_indexes::system_assign_with_seed::AUTHORITY_INDEX]
1495                                    .pubkey,
1496                            });
1497                    }
1498                    Ok(SystemInstruction::Allocate { .. }) => {
1499                        parse_system_instruction!(
1500                            parsed_instructions,
1501                            instruction,
1502                            system_allocate,
1503                            SystemAllocate,
1504                            SystemAllocate {
1505                                account: instruction_indexes::system_allocate::ACCOUNT_INDEX
1506                            }
1507                        );
1508                    }
1509                    Ok(SystemInstruction::AllocateWithSeed { .. }) => {
1510                        // Note: uses system_allocate_with_seed for validation but maps to SystemAllocate type
1511                        validate_number_accounts!(instruction, instruction_indexes::system_allocate_with_seed::REQUIRED_NUMBER_OF_ACCOUNTS);
1512                        parsed_instructions
1513                            .entry(ParsedSystemInstructionType::SystemAllocate)
1514                            .or_default()
1515                            .push(ParsedSystemInstructionData::SystemAllocate {
1516                                account: instruction.accounts
1517                                    [instruction_indexes::system_allocate_with_seed::ACCOUNT_INDEX]
1518                                    .pubkey,
1519                            });
1520                    }
1521                    Ok(SystemInstruction::InitializeNonceAccount(authority)) => {
1522                        parse_system_instruction!(parsed_instructions, instruction, system_initialize_nonce_account, SystemInitializeNonceAccount, SystemInitializeNonceAccount {
1523                            nonce_authority: authority;
1524                            nonce_account: instruction_indexes::system_initialize_nonce_account::NONCE_ACCOUNT_INDEX
1525                        });
1526                    }
1527                    Ok(SystemInstruction::AdvanceNonceAccount) => {
1528                        parse_system_instruction!(parsed_instructions, instruction, system_advance_nonce_account, SystemAdvanceNonceAccount, SystemAdvanceNonceAccount {
1529                            nonce_account: instruction_indexes::system_advance_nonce_account::NONCE_ACCOUNT_INDEX,
1530                            nonce_authority: instruction_indexes::system_advance_nonce_account::NONCE_AUTHORITY_INDEX
1531                        });
1532                    }
1533                    Ok(SystemInstruction::AuthorizeNonceAccount(_new_authority)) => {
1534                        parse_system_instruction!(parsed_instructions, instruction, system_authorize_nonce_account, SystemAuthorizeNonceAccount, SystemAuthorizeNonceAccount {
1535                            nonce_account: instruction_indexes::system_authorize_nonce_account::NONCE_ACCOUNT_INDEX,
1536                            nonce_authority: instruction_indexes::system_authorize_nonce_account::NONCE_AUTHORITY_INDEX
1537                        });
1538                    }
1539                    // UpgradeNonceAccount: Not parsed - no authority parameter, cannot validate fee payer involvement
1540                    // Anyone can upgrade any nonce account without signing
1541                    Ok(SystemInstruction::UpgradeNonceAccount) => {
1542                        // Skip parsing
1543                    }
1544                    _ => {}
1545                }
1546            }
1547        }
1548        Ok(parsed_instructions)
1549    }
1550
1551    pub fn parse_token_instructions(
1552        transaction: &VersionedTransactionResolved,
1553    ) -> Result<HashMap<ParsedSPLInstructionType, Vec<ParsedSPLInstructionData>>, KoraError> {
1554        let mut parsed_instructions: HashMap<
1555            ParsedSPLInstructionType,
1556            Vec<ParsedSPLInstructionData>,
1557        > = HashMap::new();
1558
1559        for instruction in &transaction.all_instructions {
1560            let program_id = instruction.program_id;
1561
1562            if program_id == spl_token_interface::ID {
1563                if let Ok(spl_ix) =
1564                    spl_token_interface::instruction::TokenInstruction::unpack(&instruction.data)
1565                {
1566                    match spl_ix {
1567                        spl_token_interface::instruction::TokenInstruction::Transfer { amount } => {
1568                            validate_number_accounts!(instruction, instruction_indexes::spl_token_transfer::REQUIRED_NUMBER_OF_ACCOUNTS);
1569
1570                            parsed_instructions
1571                                .entry(ParsedSPLInstructionType::SplTokenTransfer)
1572                                .or_default()
1573                                .push(ParsedSPLInstructionData::SplTokenTransfer {
1574                                    owner: instruction.accounts[instruction_indexes::spl_token_transfer::OWNER_INDEX].pubkey,
1575                                    amount,
1576                                    mint: None,
1577                                    source_address: instruction.accounts[instruction_indexes::spl_token_transfer::SOURCE_ADDRESS_INDEX].pubkey,
1578                                    destination_address: instruction.accounts[instruction_indexes::spl_token_transfer::DESTINATION_ADDRESS_INDEX].pubkey,
1579                                    is_2022: false,
1580                                });
1581                        }
1582                        spl_token_interface::instruction::TokenInstruction::TransferChecked {
1583                            amount,
1584                            ..
1585                        } => {
1586                            validate_number_accounts!(instruction, instruction_indexes::spl_token_transfer_checked::REQUIRED_NUMBER_OF_ACCOUNTS);
1587
1588                            parsed_instructions
1589                                .entry(ParsedSPLInstructionType::SplTokenTransfer)
1590                                .or_default()
1591                                .push(ParsedSPLInstructionData::SplTokenTransfer {
1592                                    owner: instruction.accounts[instruction_indexes::spl_token_transfer_checked::OWNER_INDEX].pubkey,
1593                                    amount,
1594                                    mint: Some(instruction.accounts[instruction_indexes::spl_token_transfer_checked::MINT_INDEX].pubkey),
1595                                    source_address: instruction.accounts[instruction_indexes::spl_token_transfer_checked::SOURCE_ADDRESS_INDEX].pubkey,
1596                                    destination_address: instruction.accounts[instruction_indexes::spl_token_transfer_checked::DESTINATION_ADDRESS_INDEX].pubkey,
1597                                    is_2022: false,
1598                                });
1599                        }
1600                        spl_token_interface::instruction::TokenInstruction::Burn { .. } => {
1601                            let owner = Self::parse_burn_owner_with_mint_fallback(instruction)?;
1602                            Self::push_parsed_spl_instruction(
1603                                &mut parsed_instructions,
1604                                ParsedSPLInstructionType::SplTokenBurn,
1605                                ParsedSPLInstructionData::SplTokenBurn {
1606                                    owner,
1607                                    is_2022: false,
1608                                },
1609                            );
1610                        }
1611                        spl_token_interface::instruction::TokenInstruction::BurnChecked { .. } => {
1612                            validate_number_accounts!(
1613                                instruction,
1614                                instruction_indexes::spl_token_burn::REQUIRED_NUMBER_OF_ACCOUNTS
1615                            );
1616
1617                            parsed_instructions
1618                                .entry(ParsedSPLInstructionType::SplTokenBurn)
1619                                .or_default()
1620                                .push(ParsedSPLInstructionData::SplTokenBurn {
1621                                    owner: instruction.accounts
1622                                        [instruction_indexes::spl_token_burn::OWNER_INDEX]
1623                                        .pubkey,
1624                                    is_2022: false,
1625                                });
1626                        }
1627                        spl_token_interface::instruction::TokenInstruction::CloseAccount { .. } => {
1628                            validate_number_accounts!(instruction, instruction_indexes::spl_token_close_account::REQUIRED_NUMBER_OF_ACCOUNTS);
1629
1630                            parsed_instructions
1631                                .entry(ParsedSPLInstructionType::SplTokenCloseAccount)
1632                                .or_default()
1633                                .push(ParsedSPLInstructionData::SplTokenCloseAccount {
1634                                    owner: instruction.accounts
1635                                        [instruction_indexes::spl_token_close_account::OWNER_INDEX]
1636                                        .pubkey,
1637                                    is_2022: false,
1638                                });
1639                        }
1640                        spl_token_interface::instruction::TokenInstruction::Approve { .. } => {
1641                            validate_number_accounts!(
1642                                instruction,
1643                                instruction_indexes::spl_token_approve::REQUIRED_NUMBER_OF_ACCOUNTS
1644                            );
1645
1646                            parsed_instructions
1647                                .entry(ParsedSPLInstructionType::SplTokenApprove)
1648                                .or_default()
1649                                .push(ParsedSPLInstructionData::SplTokenApprove {
1650                                    owner: instruction.accounts
1651                                        [instruction_indexes::spl_token_approve::OWNER_INDEX]
1652                                        .pubkey,
1653                                    is_2022: false,
1654                                });
1655                        }
1656                        spl_token_interface::instruction::TokenInstruction::ApproveChecked { .. } => {
1657                            validate_number_accounts!(instruction, instruction_indexes::spl_token_approve_checked::REQUIRED_NUMBER_OF_ACCOUNTS);
1658
1659                            parsed_instructions
1660                                .entry(ParsedSPLInstructionType::SplTokenApprove)
1661                                .or_default()
1662                                .push(ParsedSPLInstructionData::SplTokenApprove {
1663                                    owner: instruction.accounts[instruction_indexes::spl_token_approve_checked::OWNER_INDEX].pubkey,
1664                                    is_2022: false,
1665                                });
1666                        }
1667                        spl_token_interface::instruction::TokenInstruction::Revoke => {
1668                            validate_number_accounts!(
1669                                instruction,
1670                                instruction_indexes::spl_token_revoke::REQUIRED_NUMBER_OF_ACCOUNTS
1671                            );
1672
1673                            parsed_instructions
1674                                .entry(ParsedSPLInstructionType::SplTokenRevoke)
1675                                .or_default()
1676                                .push(ParsedSPLInstructionData::SplTokenRevoke {
1677                                    owner: instruction.accounts
1678                                        [instruction_indexes::spl_token_revoke::OWNER_INDEX]
1679                                        .pubkey,
1680                                    is_2022: false,
1681                                });
1682                        }
1683                        spl_token_interface::instruction::TokenInstruction::SetAuthority { .. } => {
1684                            validate_number_accounts!(instruction, instruction_indexes::spl_token_set_authority::REQUIRED_NUMBER_OF_ACCOUNTS);
1685
1686                            parsed_instructions
1687                                .entry(ParsedSPLInstructionType::SplTokenSetAuthority)
1688                                .or_default()
1689                                .push(ParsedSPLInstructionData::SplTokenSetAuthority {
1690                                    authority: instruction.accounts[instruction_indexes::spl_token_set_authority::CURRENT_AUTHORITY_INDEX].pubkey,
1691                                    is_2022: false,
1692                                });
1693                        }
1694                        spl_token_interface::instruction::TokenInstruction::MintTo { .. } => {
1695                            validate_number_accounts!(
1696                                instruction,
1697                                instruction_indexes::spl_token_mint_to::REQUIRED_NUMBER_OF_ACCOUNTS
1698                            );
1699
1700                            parsed_instructions
1701                                .entry(ParsedSPLInstructionType::SplTokenMintTo)
1702                                .or_default()
1703                                .push(ParsedSPLInstructionData::SplTokenMintTo {
1704                                    mint_authority: instruction.accounts[instruction_indexes::spl_token_mint_to::MINT_AUTHORITY_INDEX].pubkey,
1705                                    is_2022: false,
1706                                });
1707                        }
1708                        spl_token_interface::instruction::TokenInstruction::MintToChecked { .. } => {
1709                            validate_number_accounts!(instruction, instruction_indexes::spl_token_mint_to_checked::REQUIRED_NUMBER_OF_ACCOUNTS);
1710
1711                            parsed_instructions
1712                                .entry(ParsedSPLInstructionType::SplTokenMintTo)
1713                                .or_default()
1714                                .push(ParsedSPLInstructionData::SplTokenMintTo {
1715                                    mint_authority: instruction.accounts[instruction_indexes::spl_token_mint_to_checked::MINT_AUTHORITY_INDEX].pubkey,
1716                                    is_2022: false,
1717                                });
1718                        }
1719                        spl_token_interface::instruction::TokenInstruction::InitializeMint {
1720                            mint_authority,
1721                            ..
1722                        } => {
1723                            validate_number_accounts!(instruction, instruction_indexes::spl_token_initialize_mint::REQUIRED_NUMBER_OF_ACCOUNTS);
1724
1725                            parsed_instructions
1726                                .entry(ParsedSPLInstructionType::SplTokenInitializeMint)
1727                                .or_default()
1728                                .push(ParsedSPLInstructionData::SplTokenInitializeMint {
1729                                    mint_authority,
1730                                    is_2022: false,
1731                                });
1732                        }
1733                        spl_token_interface::instruction::TokenInstruction::InitializeMint2 {
1734                            mint_authority,
1735                            ..
1736                        } => {
1737                            validate_number_accounts!(instruction, instruction_indexes::spl_token_initialize_mint2::REQUIRED_NUMBER_OF_ACCOUNTS);
1738
1739                            parsed_instructions
1740                                .entry(ParsedSPLInstructionType::SplTokenInitializeMint)
1741                                .or_default()
1742                                .push(ParsedSPLInstructionData::SplTokenInitializeMint {
1743                                    mint_authority,
1744                                    is_2022: false,
1745                                });
1746                        }
1747                        spl_token_interface::instruction::TokenInstruction::InitializeAccount => {
1748                            validate_number_accounts!(instruction, instruction_indexes::spl_token_initialize_account::REQUIRED_NUMBER_OF_ACCOUNTS);
1749
1750                            parsed_instructions
1751                                .entry(ParsedSPLInstructionType::SplTokenInitializeAccount)
1752                                .or_default()
1753                                .push(ParsedSPLInstructionData::SplTokenInitializeAccount {
1754                                    owner: instruction.accounts[instruction_indexes::spl_token_initialize_account::OWNER_INDEX].pubkey,
1755                                    is_2022: false,
1756                                });
1757                        }
1758                        spl_token_interface::instruction::TokenInstruction::InitializeAccount2 { owner } => {
1759                            validate_number_accounts!(instruction, instruction_indexes::spl_token_initialize_account2::REQUIRED_NUMBER_OF_ACCOUNTS);
1760
1761                            parsed_instructions
1762                                .entry(ParsedSPLInstructionType::SplTokenInitializeAccount)
1763                                .or_default()
1764                                .push(ParsedSPLInstructionData::SplTokenInitializeAccount {
1765                                    owner,
1766                                    is_2022: false,
1767                                });
1768                        }
1769                        spl_token_interface::instruction::TokenInstruction::InitializeAccount3 { owner } => {
1770                            validate_number_accounts!(instruction, instruction_indexes::spl_token_initialize_account3::REQUIRED_NUMBER_OF_ACCOUNTS);
1771
1772                            parsed_instructions
1773                                .entry(ParsedSPLInstructionType::SplTokenInitializeAccount)
1774                                .or_default()
1775                                .push(ParsedSPLInstructionData::SplTokenInitializeAccount {
1776                                    owner,
1777                                    is_2022: false,
1778                                });
1779                        }
1780                        spl_token_interface::instruction::TokenInstruction::InitializeMultisig { .. } => {
1781                            validate_number_accounts!(instruction, instruction_indexes::spl_token_initialize_multisig::REQUIRED_NUMBER_OF_ACCOUNTS);
1782
1783                            // Extract signers from accounts (skip first 2: multisig + rent sysvar)
1784                            let signers = Self::extract_multisig_signers(instruction, 2);
1785
1786                            parsed_instructions
1787                                .entry(ParsedSPLInstructionType::SplTokenInitializeMultisig)
1788                                .or_default()
1789                                .push(ParsedSPLInstructionData::SplTokenInitializeMultisig {
1790                                    signers,
1791                                    is_2022: false,
1792                                });
1793                        }
1794                        spl_token_interface::instruction::TokenInstruction::InitializeMultisig2 {
1795                            ..
1796                        } => {
1797                            validate_number_accounts!(instruction, instruction_indexes::spl_token_initialize_multisig2::REQUIRED_NUMBER_OF_ACCOUNTS);
1798
1799                            // Extract signers from accounts (skip first: multisig only)
1800                            let signers = Self::extract_multisig_signers(instruction, 1);
1801
1802                            parsed_instructions
1803                                .entry(ParsedSPLInstructionType::SplTokenInitializeMultisig)
1804                                .or_default()
1805                                .push(ParsedSPLInstructionData::SplTokenInitializeMultisig {
1806                                    signers,
1807                                    is_2022: false,
1808                                });
1809                        }
1810                        spl_token_interface::instruction::TokenInstruction::FreezeAccount => {
1811                            validate_number_accounts!(instruction, instruction_indexes::spl_token_freeze_account::REQUIRED_NUMBER_OF_ACCOUNTS);
1812
1813                            parsed_instructions
1814                                .entry(ParsedSPLInstructionType::SplTokenFreezeAccount)
1815                                .or_default()
1816                                .push(ParsedSPLInstructionData::SplTokenFreezeAccount {
1817                                    freeze_authority: instruction.accounts[instruction_indexes::spl_token_freeze_account::FREEZE_AUTHORITY_INDEX].pubkey,
1818                                    is_2022: false,
1819                                });
1820                        }
1821                        spl_token_interface::instruction::TokenInstruction::ThawAccount => {
1822                            validate_number_accounts!(instruction, instruction_indexes::spl_token_thaw_account::REQUIRED_NUMBER_OF_ACCOUNTS);
1823
1824                            parsed_instructions
1825                                .entry(ParsedSPLInstructionType::SplTokenThawAccount)
1826                                .or_default()
1827                                .push(ParsedSPLInstructionData::SplTokenThawAccount {
1828                                    freeze_authority: instruction.accounts[instruction_indexes::spl_token_thaw_account::FREEZE_AUTHORITY_INDEX].pubkey,
1829                                    is_2022: false,
1830                                });
1831                        }
1832                        _ => {}
1833                    };
1834                }
1835            } else if program_id == spl_token_2022_interface::ID {
1836                if let Ok(spl_ix) = spl_token_2022_interface::instruction::TokenInstruction::unpack(
1837                    &instruction.data,
1838                ) {
1839                    match spl_ix {
1840                        #[allow(deprecated)]
1841                        spl_token_2022_interface::instruction::TokenInstruction::Transfer { amount } => {
1842                            validate_number_accounts!(instruction, instruction_indexes::spl_token_transfer::REQUIRED_NUMBER_OF_ACCOUNTS);
1843
1844                            parsed_instructions
1845                                .entry(ParsedSPLInstructionType::SplTokenTransfer)
1846                                .or_default()
1847                                .push(ParsedSPLInstructionData::SplTokenTransfer {
1848                                    owner: instruction.accounts[instruction_indexes::spl_token_transfer::OWNER_INDEX].pubkey,
1849                                    amount,
1850                                    mint: None,
1851                                    source_address: instruction.accounts[instruction_indexes::spl_token_transfer::SOURCE_ADDRESS_INDEX].pubkey,
1852                                    destination_address: instruction.accounts[instruction_indexes::spl_token_transfer::DESTINATION_ADDRESS_INDEX].pubkey,
1853                                    is_2022: true,
1854                                });
1855                        }
1856                        spl_token_2022_interface::instruction::TokenInstruction::TransferChecked {
1857                            amount,
1858                            ..
1859                        } => {
1860                            validate_number_accounts!(instruction, instruction_indexes::spl_token_transfer_checked::REQUIRED_NUMBER_OF_ACCOUNTS);
1861
1862                            parsed_instructions
1863                                .entry(ParsedSPLInstructionType::SplTokenTransfer)
1864                                .or_default()
1865                                .push(ParsedSPLInstructionData::SplTokenTransfer {
1866                                    owner: instruction.accounts[instruction_indexes::spl_token_transfer_checked::OWNER_INDEX].pubkey,
1867                                    amount,
1868                                    mint: Some(instruction.accounts[instruction_indexes::spl_token_transfer_checked::MINT_INDEX].pubkey),
1869                                    source_address: instruction.accounts[instruction_indexes::spl_token_transfer_checked::SOURCE_ADDRESS_INDEX].pubkey,
1870                                    destination_address: instruction.accounts[instruction_indexes::spl_token_transfer_checked::DESTINATION_ADDRESS_INDEX].pubkey,
1871                                    is_2022: true,
1872                                });
1873                        }
1874                        spl_token_2022_interface::instruction::TokenInstruction::Burn { .. } => {
1875                            let owner = Self::parse_burn_owner_with_mint_fallback(instruction)?;
1876                            Self::push_parsed_spl_instruction(
1877                                &mut parsed_instructions,
1878                                ParsedSPLInstructionType::SplTokenBurn,
1879                                ParsedSPLInstructionData::SplTokenBurn {
1880                                    owner,
1881                                    is_2022: true,
1882                                },
1883                            );
1884                        }
1885                        spl_token_2022_interface::instruction::TokenInstruction::BurnChecked { .. } => {
1886                            validate_number_accounts!(
1887                                instruction,
1888                                instruction_indexes::spl_token_burn::REQUIRED_NUMBER_OF_ACCOUNTS
1889                            );
1890
1891                            parsed_instructions
1892                                .entry(ParsedSPLInstructionType::SplTokenBurn)
1893                                .or_default()
1894                                .push(ParsedSPLInstructionData::SplTokenBurn {
1895                                    owner: instruction.accounts
1896                                        [instruction_indexes::spl_token_burn::OWNER_INDEX]
1897                                        .pubkey,
1898                                    is_2022: true,
1899                                });
1900                        }
1901                        spl_token_2022_interface::instruction::TokenInstruction::CloseAccount { .. } => {
1902                            validate_number_accounts!(instruction, instruction_indexes::spl_token_close_account::REQUIRED_NUMBER_OF_ACCOUNTS);
1903
1904                            parsed_instructions
1905                                .entry(ParsedSPLInstructionType::SplTokenCloseAccount)
1906                                .or_default()
1907                                .push(ParsedSPLInstructionData::SplTokenCloseAccount {
1908                                    owner: instruction.accounts
1909                                        [instruction_indexes::spl_token_close_account::OWNER_INDEX]
1910                                        .pubkey,
1911                                    is_2022: true,
1912                                });
1913                        }
1914                        spl_token_2022_interface::instruction::TokenInstruction::Approve { .. } => {
1915                            validate_number_accounts!(
1916                                instruction,
1917                                instruction_indexes::spl_token_approve::REQUIRED_NUMBER_OF_ACCOUNTS
1918                            );
1919
1920                            parsed_instructions
1921                                .entry(ParsedSPLInstructionType::SplTokenApprove)
1922                                .or_default()
1923                                .push(ParsedSPLInstructionData::SplTokenApprove {
1924                                    owner: instruction.accounts
1925                                        [instruction_indexes::spl_token_approve::OWNER_INDEX]
1926                                        .pubkey,
1927                                    is_2022: true,
1928                                });
1929                        }
1930                        spl_token_2022_interface::instruction::TokenInstruction::ApproveChecked {
1931                            ..
1932                        } => {
1933                            validate_number_accounts!(instruction, instruction_indexes::spl_token_approve_checked::REQUIRED_NUMBER_OF_ACCOUNTS);
1934
1935                            parsed_instructions
1936                                .entry(ParsedSPLInstructionType::SplTokenApprove)
1937                                .or_default()
1938                                .push(ParsedSPLInstructionData::SplTokenApprove {
1939                                    owner: instruction.accounts[instruction_indexes::spl_token_approve_checked::OWNER_INDEX].pubkey,
1940                                    is_2022: true,
1941                                });
1942                        }
1943                        spl_token_2022_interface::instruction::TokenInstruction::Revoke => {
1944                            validate_number_accounts!(
1945                                instruction,
1946                                instruction_indexes::spl_token_revoke::REQUIRED_NUMBER_OF_ACCOUNTS
1947                            );
1948
1949                            parsed_instructions
1950                                .entry(ParsedSPLInstructionType::SplTokenRevoke)
1951                                .or_default()
1952                                .push(ParsedSPLInstructionData::SplTokenRevoke {
1953                                    owner: instruction.accounts
1954                                        [instruction_indexes::spl_token_revoke::OWNER_INDEX]
1955                                        .pubkey,
1956                                    is_2022: true,
1957                                });
1958                        }
1959                        spl_token_2022_interface::instruction::TokenInstruction::SetAuthority { .. } => {
1960                            validate_number_accounts!(instruction, instruction_indexes::spl_token_set_authority::REQUIRED_NUMBER_OF_ACCOUNTS);
1961
1962                            parsed_instructions
1963                                .entry(ParsedSPLInstructionType::SplTokenSetAuthority)
1964                                .or_default()
1965                                .push(ParsedSPLInstructionData::SplTokenSetAuthority {
1966                                    authority: instruction.accounts[instruction_indexes::spl_token_set_authority::CURRENT_AUTHORITY_INDEX].pubkey,
1967                                    is_2022: true,
1968                                });
1969                        }
1970                        spl_token_2022_interface::instruction::TokenInstruction::MintTo { .. } => {
1971                            validate_number_accounts!(
1972                                instruction,
1973                                instruction_indexes::spl_token_mint_to::REQUIRED_NUMBER_OF_ACCOUNTS
1974                            );
1975
1976                            parsed_instructions
1977                                .entry(ParsedSPLInstructionType::SplTokenMintTo)
1978                                .or_default()
1979                                .push(ParsedSPLInstructionData::SplTokenMintTo {
1980                                    mint_authority: instruction.accounts[instruction_indexes::spl_token_mint_to::MINT_AUTHORITY_INDEX].pubkey,
1981                                    is_2022: true,
1982                                });
1983                        }
1984                        spl_token_2022_interface::instruction::TokenInstruction::MintToChecked { .. } => {
1985                            validate_number_accounts!(instruction, instruction_indexes::spl_token_mint_to_checked::REQUIRED_NUMBER_OF_ACCOUNTS);
1986
1987                            parsed_instructions
1988                                .entry(ParsedSPLInstructionType::SplTokenMintTo)
1989                                .or_default()
1990                                .push(ParsedSPLInstructionData::SplTokenMintTo {
1991                                    mint_authority: instruction.accounts[instruction_indexes::spl_token_mint_to_checked::MINT_AUTHORITY_INDEX].pubkey,
1992                                    is_2022: true,
1993                                });
1994                        }
1995                        spl_token_2022_interface::instruction::TokenInstruction::InitializeMint {
1996                            mint_authority,
1997                            ..
1998                        } => {
1999                            validate_number_accounts!(instruction, instruction_indexes::spl_token_initialize_mint::REQUIRED_NUMBER_OF_ACCOUNTS);
2000
2001                            parsed_instructions
2002                                .entry(ParsedSPLInstructionType::SplTokenInitializeMint)
2003                                .or_default()
2004                                .push(ParsedSPLInstructionData::SplTokenInitializeMint {
2005                                    mint_authority,
2006                                    is_2022: true,
2007                                });
2008                        }
2009                        spl_token_2022_interface::instruction::TokenInstruction::InitializeMint2 {
2010                            mint_authority,
2011                            ..
2012                        } => {
2013                            validate_number_accounts!(instruction, instruction_indexes::spl_token_initialize_mint2::REQUIRED_NUMBER_OF_ACCOUNTS);
2014
2015                            parsed_instructions
2016                                .entry(ParsedSPLInstructionType::SplTokenInitializeMint)
2017                                .or_default()
2018                                .push(ParsedSPLInstructionData::SplTokenInitializeMint {
2019                                    mint_authority,
2020                                    is_2022: true,
2021                                });
2022                        }
2023                        spl_token_2022_interface::instruction::TokenInstruction::InitializeAccount => {
2024                            validate_number_accounts!(instruction, instruction_indexes::spl_token_initialize_account::REQUIRED_NUMBER_OF_ACCOUNTS);
2025
2026                            parsed_instructions
2027                                .entry(ParsedSPLInstructionType::SplTokenInitializeAccount)
2028                                .or_default()
2029                                .push(ParsedSPLInstructionData::SplTokenInitializeAccount {
2030                                    owner: instruction.accounts[instruction_indexes::spl_token_initialize_account::OWNER_INDEX].pubkey,
2031                                    is_2022: true,
2032                                });
2033                        }
2034                        spl_token_2022_interface::instruction::TokenInstruction::InitializeAccount2 {
2035                            owner,
2036                        } => {
2037                            validate_number_accounts!(instruction, instruction_indexes::spl_token_initialize_account2::REQUIRED_NUMBER_OF_ACCOUNTS);
2038
2039                            parsed_instructions
2040                                .entry(ParsedSPLInstructionType::SplTokenInitializeAccount)
2041                                .or_default()
2042                                .push(ParsedSPLInstructionData::SplTokenInitializeAccount {
2043                                    owner,
2044                                    is_2022: true,
2045                                });
2046                        }
2047                        spl_token_2022_interface::instruction::TokenInstruction::InitializeAccount3 {
2048                            owner,
2049                        } => {
2050                            validate_number_accounts!(instruction, instruction_indexes::spl_token_initialize_account3::REQUIRED_NUMBER_OF_ACCOUNTS);
2051
2052                            parsed_instructions
2053                                .entry(ParsedSPLInstructionType::SplTokenInitializeAccount)
2054                                .or_default()
2055                                .push(ParsedSPLInstructionData::SplTokenInitializeAccount {
2056                                    owner,
2057                                    is_2022: true,
2058                                });
2059                        }
2060                        spl_token_2022_interface::instruction::TokenInstruction::InitializeMultisig {
2061                            ..
2062                        } => {
2063                            validate_number_accounts!(instruction, instruction_indexes::spl_token_initialize_multisig::REQUIRED_NUMBER_OF_ACCOUNTS);
2064
2065                            // Extract signers from accounts (skip first 2: multisig + rent sysvar)
2066                            let signers = Self::extract_multisig_signers(instruction, 2);
2067
2068                            parsed_instructions
2069                                .entry(ParsedSPLInstructionType::SplTokenInitializeMultisig)
2070                                .or_default()
2071                                .push(ParsedSPLInstructionData::SplTokenInitializeMultisig {
2072                                    signers,
2073                                    is_2022: true,
2074                                });
2075                        }
2076                        spl_token_2022_interface::instruction::TokenInstruction::InitializeMultisig2 {
2077                            ..
2078                        } => {
2079                            validate_number_accounts!(instruction, instruction_indexes::spl_token_initialize_multisig2::REQUIRED_NUMBER_OF_ACCOUNTS);
2080
2081                            // Extract signers from accounts (skip first: multisig only)
2082                            let signers = Self::extract_multisig_signers(instruction, 1);
2083
2084                            parsed_instructions
2085                                .entry(ParsedSPLInstructionType::SplTokenInitializeMultisig)
2086                                .or_default()
2087                                .push(ParsedSPLInstructionData::SplTokenInitializeMultisig {
2088                                    signers,
2089                                    is_2022: true,
2090                                });
2091                        }
2092                        spl_token_2022_interface::instruction::TokenInstruction::FreezeAccount => {
2093                            validate_number_accounts!(instruction, instruction_indexes::spl_token_freeze_account::REQUIRED_NUMBER_OF_ACCOUNTS);
2094
2095                            parsed_instructions
2096                                .entry(ParsedSPLInstructionType::SplTokenFreezeAccount)
2097                                .or_default()
2098                                .push(ParsedSPLInstructionData::SplTokenFreezeAccount {
2099                                    freeze_authority: instruction.accounts[instruction_indexes::spl_token_freeze_account::FREEZE_AUTHORITY_INDEX].pubkey,
2100                                    is_2022: true,
2101                                });
2102                        }
2103                        spl_token_2022_interface::instruction::TokenInstruction::ThawAccount => {
2104                            validate_number_accounts!(instruction, instruction_indexes::spl_token_thaw_account::REQUIRED_NUMBER_OF_ACCOUNTS);
2105
2106                            parsed_instructions
2107                                .entry(ParsedSPLInstructionType::SplTokenThawAccount)
2108                                .or_default()
2109                                .push(ParsedSPLInstructionData::SplTokenThawAccount {
2110                                    freeze_authority: instruction.accounts[instruction_indexes::spl_token_thaw_account::FREEZE_AUTHORITY_INDEX].pubkey,
2111                                    is_2022: true,
2112                                });
2113                        }
2114                        _ => {}
2115                    };
2116                }
2117            }
2118        }
2119        Ok(parsed_instructions)
2120    }
2121}
2122
2123#[cfg(test)]
2124mod tests {
2125
2126    use super::*;
2127    use solana_sdk::message::{AccountKeys, Message};
2128    use solana_transaction_status::parse_instruction;
2129
2130    fn create_parsed_system_transfer(
2131        source: &Pubkey,
2132        destination: &Pubkey,
2133        lamports: u64,
2134    ) -> Result<solana_transaction_status_client_types::ParsedInstruction, Box<dyn std::error::Error>>
2135    {
2136        let solana_instruction =
2137            solana_system_interface::instruction::transfer(source, destination, lamports);
2138
2139        let message = Message::new(&[solana_instruction], None);
2140        let compiled_instruction = &message.instructions[0];
2141
2142        let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None);
2143
2144        let parsed = parse_instruction::parse(
2145            &SYSTEM_PROGRAM_ID,
2146            compiled_instruction,
2147            &account_keys_for_parsing,
2148            None,
2149        )?;
2150
2151        Ok(parsed)
2152    }
2153
2154    fn create_parsed_system_transfer_with_seed(
2155        source: &Pubkey,
2156        destination: &Pubkey,
2157        lamports: u64,
2158        source_base: &Pubkey,
2159        seed: &str,
2160        source_owner: &Pubkey,
2161    ) -> Result<solana_transaction_status_client_types::ParsedInstruction, Box<dyn std::error::Error>>
2162    {
2163        let solana_instruction = solana_system_interface::instruction::transfer_with_seed(
2164            source,
2165            source_base,
2166            seed.to_string(),
2167            source_owner,
2168            destination,
2169            lamports,
2170        );
2171
2172        let message = Message::new(&[solana_instruction], None);
2173        let compiled_instruction = &message.instructions[0];
2174
2175        let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None);
2176
2177        let parsed = parse_instruction::parse(
2178            &SYSTEM_PROGRAM_ID,
2179            compiled_instruction,
2180            &account_keys_for_parsing,
2181            None,
2182        )?;
2183
2184        Ok(parsed)
2185    }
2186
2187    fn create_parsed_system_create_account(
2188        source: &Pubkey,
2189        new_account: &Pubkey,
2190        lamports: u64,
2191        space: u64,
2192        owner: &Pubkey,
2193    ) -> Result<solana_transaction_status_client_types::ParsedInstruction, Box<dyn std::error::Error>>
2194    {
2195        let solana_instruction = solana_system_interface::instruction::create_account(
2196            source,
2197            new_account,
2198            lamports,
2199            space,
2200            owner,
2201        );
2202
2203        let message = Message::new(&[solana_instruction], None);
2204        let compiled_instruction = &message.instructions[0];
2205
2206        let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None);
2207
2208        let parsed = parse_instruction::parse(
2209            &SYSTEM_PROGRAM_ID,
2210            compiled_instruction,
2211            &account_keys_for_parsing,
2212            None,
2213        )?;
2214
2215        Ok(parsed)
2216    }
2217
2218    fn create_parsed_system_create_account_with_seed(
2219        source: &Pubkey,
2220        new_account: &Pubkey,
2221        base: &Pubkey,
2222        seed: &str,
2223        lamports: u64,
2224        space: u64,
2225        owner: &Pubkey,
2226    ) -> Result<solana_transaction_status_client_types::ParsedInstruction, Box<dyn std::error::Error>>
2227    {
2228        let solana_instruction = solana_system_interface::instruction::create_account_with_seed(
2229            source,
2230            new_account,
2231            base,
2232            seed,
2233            lamports,
2234            space,
2235            owner,
2236        );
2237
2238        let message = Message::new(&[solana_instruction], None);
2239        let compiled_instruction = &message.instructions[0];
2240
2241        let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None);
2242
2243        let parsed = parse_instruction::parse(
2244            &SYSTEM_PROGRAM_ID,
2245            compiled_instruction,
2246            &account_keys_for_parsing,
2247            None,
2248        )?;
2249
2250        Ok(parsed)
2251    }
2252
2253    fn create_parsed_system_assign(
2254        account: &Pubkey,
2255        owner: &Pubkey,
2256    ) -> Result<solana_transaction_status_client_types::ParsedInstruction, Box<dyn std::error::Error>>
2257    {
2258        let solana_instruction = solana_system_interface::instruction::assign(account, owner);
2259
2260        let message = Message::new(&[solana_instruction], None);
2261        let compiled_instruction = &message.instructions[0];
2262
2263        let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None);
2264
2265        let parsed = parse_instruction::parse(
2266            &SYSTEM_PROGRAM_ID,
2267            compiled_instruction,
2268            &account_keys_for_parsing,
2269            None,
2270        )?;
2271
2272        Ok(parsed)
2273    }
2274
2275    fn create_parsed_system_assign_with_seed(
2276        account: &Pubkey,
2277        base: &Pubkey,
2278        seed: &str,
2279        owner: &Pubkey,
2280    ) -> Result<solana_transaction_status_client_types::ParsedInstruction, Box<dyn std::error::Error>>
2281    {
2282        let solana_instruction =
2283            solana_system_interface::instruction::assign_with_seed(account, base, seed, owner);
2284
2285        let message = Message::new(&[solana_instruction], None);
2286        let compiled_instruction = &message.instructions[0];
2287
2288        let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None);
2289
2290        let parsed = parse_instruction::parse(
2291            &SYSTEM_PROGRAM_ID,
2292            compiled_instruction,
2293            &account_keys_for_parsing,
2294            None,
2295        )?;
2296
2297        Ok(parsed)
2298    }
2299
2300    fn create_parsed_system_withdraw_nonce_account(
2301        nonce_account: &Pubkey,
2302        nonce_authority: &Pubkey,
2303        recipient: &Pubkey,
2304        lamports: u64,
2305    ) -> Result<solana_transaction_status_client_types::ParsedInstruction, Box<dyn std::error::Error>>
2306    {
2307        let solana_instruction = solana_system_interface::instruction::withdraw_nonce_account(
2308            nonce_account,
2309            nonce_authority,
2310            recipient,
2311            lamports,
2312        );
2313
2314        let message = Message::new(&[solana_instruction], None);
2315        let compiled_instruction = &message.instructions[0];
2316
2317        let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None);
2318
2319        let parsed = parse_instruction::parse(
2320            &SYSTEM_PROGRAM_ID,
2321            compiled_instruction,
2322            &account_keys_for_parsing,
2323            None,
2324        )?;
2325
2326        Ok(parsed)
2327    }
2328
2329    fn create_parsed_spl_token_transfer(
2330        source: &Pubkey,
2331        destination: &Pubkey,
2332        authority: &Pubkey,
2333        amount: u64,
2334    ) -> Result<solana_transaction_status_client_types::ParsedInstruction, Box<dyn std::error::Error>>
2335    {
2336        let solana_instruction = spl_token_interface::instruction::transfer(
2337            &spl_token_interface::ID,
2338            source,
2339            destination,
2340            authority,
2341            &[],
2342            amount,
2343        )?;
2344
2345        let message = Message::new(&[solana_instruction], None);
2346        let compiled_instruction = &message.instructions[0];
2347
2348        let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None);
2349
2350        let parsed = parse_instruction::parse(
2351            &spl_token_interface::ID,
2352            compiled_instruction,
2353            &account_keys_for_parsing,
2354            None,
2355        )?;
2356
2357        Ok(parsed)
2358    }
2359
2360    fn create_parsed_spl_token_transfer_checked(
2361        source: &Pubkey,
2362        mint: &Pubkey,
2363        destination: &Pubkey,
2364        authority: &Pubkey,
2365        amount: u64,
2366        decimals: u8,
2367    ) -> Result<solana_transaction_status_client_types::ParsedInstruction, Box<dyn std::error::Error>>
2368    {
2369        let solana_instruction = spl_token_interface::instruction::transfer_checked(
2370            &spl_token_interface::ID,
2371            source,
2372            mint,
2373            destination,
2374            authority,
2375            &[],
2376            amount,
2377            decimals,
2378        )?;
2379
2380        let message = Message::new(&[solana_instruction], None);
2381        let compiled_instruction = &message.instructions[0];
2382
2383        let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None);
2384
2385        let parsed = parse_instruction::parse(
2386            &spl_token_interface::ID,
2387            compiled_instruction,
2388            &account_keys_for_parsing,
2389            None,
2390        )?;
2391
2392        Ok(parsed)
2393    }
2394
2395    fn create_parsed_spl_token_burn(
2396        account: &Pubkey,
2397        mint: &Pubkey,
2398        authority: &Pubkey,
2399        amount: u64,
2400    ) -> Result<solana_transaction_status_client_types::ParsedInstruction, Box<dyn std::error::Error>>
2401    {
2402        let solana_instruction = spl_token_interface::instruction::burn(
2403            &spl_token_interface::ID,
2404            account,
2405            mint,
2406            authority,
2407            &[],
2408            amount,
2409        )?;
2410
2411        let message = Message::new(&[solana_instruction], None);
2412        let compiled_instruction = &message.instructions[0];
2413
2414        let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None);
2415
2416        let parsed = parse_instruction::parse(
2417            &spl_token_interface::ID,
2418            compiled_instruction,
2419            &account_keys_for_parsing,
2420            None,
2421        )?;
2422
2423        Ok(parsed)
2424    }
2425
2426    fn create_parsed_spl_token_burn_checked(
2427        account: &Pubkey,
2428        mint: &Pubkey,
2429        authority: &Pubkey,
2430        amount: u64,
2431        decimals: u8,
2432    ) -> Result<solana_transaction_status_client_types::ParsedInstruction, Box<dyn std::error::Error>>
2433    {
2434        let solana_instruction = spl_token_interface::instruction::burn_checked(
2435            &spl_token_interface::ID,
2436            account,
2437            mint,
2438            authority,
2439            &[],
2440            amount,
2441            decimals,
2442        )?;
2443
2444        let message = Message::new(&[solana_instruction], None);
2445        let compiled_instruction = &message.instructions[0];
2446
2447        let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None);
2448
2449        let parsed = parse_instruction::parse(
2450            &spl_token_interface::ID,
2451            compiled_instruction,
2452            &account_keys_for_parsing,
2453            None,
2454        )?;
2455
2456        Ok(parsed)
2457    }
2458
2459    fn create_parsed_spl_token_close_account(
2460        account: &Pubkey,
2461        destination: &Pubkey,
2462        authority: &Pubkey,
2463    ) -> Result<solana_transaction_status_client_types::ParsedInstruction, Box<dyn std::error::Error>>
2464    {
2465        let solana_instruction = spl_token_interface::instruction::close_account(
2466            &spl_token_interface::ID,
2467            account,
2468            destination,
2469            authority,
2470            &[],
2471        )?;
2472
2473        let message = Message::new(&[solana_instruction], None);
2474        let compiled_instruction = &message.instructions[0];
2475
2476        let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None);
2477
2478        let parsed = parse_instruction::parse(
2479            &spl_token_interface::ID,
2480            compiled_instruction,
2481            &account_keys_for_parsing,
2482            None,
2483        )?;
2484
2485        Ok(parsed)
2486    }
2487
2488    fn create_parsed_spl_token_approve(
2489        source: &Pubkey,
2490        delegate: &Pubkey,
2491        authority: &Pubkey,
2492        amount: u64,
2493    ) -> Result<solana_transaction_status_client_types::ParsedInstruction, Box<dyn std::error::Error>>
2494    {
2495        let solana_instruction = spl_token_interface::instruction::approve(
2496            &spl_token_interface::ID,
2497            source,
2498            delegate,
2499            authority,
2500            &[],
2501            amount,
2502        )?;
2503
2504        let message = Message::new(&[solana_instruction], None);
2505        let compiled_instruction = &message.instructions[0];
2506
2507        let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None);
2508
2509        let parsed = parse_instruction::parse(
2510            &spl_token_interface::ID,
2511            compiled_instruction,
2512            &account_keys_for_parsing,
2513            None,
2514        )?;
2515
2516        Ok(parsed)
2517    }
2518
2519    fn create_parsed_spl_token_approve_checked(
2520        source: &Pubkey,
2521        mint: &Pubkey,
2522        delegate: &Pubkey,
2523        authority: &Pubkey,
2524        amount: u64,
2525        decimals: u8,
2526    ) -> Result<solana_transaction_status_client_types::ParsedInstruction, Box<dyn std::error::Error>>
2527    {
2528        let solana_instruction = spl_token_interface::instruction::approve_checked(
2529            &spl_token_interface::ID,
2530            source,
2531            mint,
2532            delegate,
2533            authority,
2534            &[],
2535            amount,
2536            decimals,
2537        )?;
2538
2539        let message = Message::new(&[solana_instruction], None);
2540        let compiled_instruction = &message.instructions[0];
2541
2542        let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None);
2543
2544        let parsed = parse_instruction::parse(
2545            &spl_token_interface::ID,
2546            compiled_instruction,
2547            &account_keys_for_parsing,
2548            None,
2549        )?;
2550
2551        Ok(parsed)
2552    }
2553
2554    fn create_parsed_token2022_transfer(
2555        source: &Pubkey,
2556        destination: &Pubkey,
2557        authority: &Pubkey,
2558        amount: u64,
2559    ) -> Result<solana_transaction_status_client_types::ParsedInstruction, Box<dyn std::error::Error>>
2560    {
2561        #[allow(deprecated)]
2562        let solana_instruction = spl_token_2022_interface::instruction::transfer(
2563            &spl_token_2022_interface::ID,
2564            source,
2565            destination,
2566            authority,
2567            &[],
2568            amount,
2569        )?;
2570
2571        let message = Message::new(&[solana_instruction], None);
2572        let compiled_instruction = &message.instructions[0];
2573
2574        let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None);
2575
2576        let parsed = parse_instruction::parse(
2577            &spl_token_2022_interface::ID,
2578            compiled_instruction,
2579            &account_keys_for_parsing,
2580            None,
2581        )?;
2582
2583        Ok(parsed)
2584    }
2585
2586    fn create_parsed_token2022_transfer_checked(
2587        source: &Pubkey,
2588        mint: &Pubkey,
2589        destination: &Pubkey,
2590        authority: &Pubkey,
2591        amount: u64,
2592        decimals: u8,
2593    ) -> Result<solana_transaction_status_client_types::ParsedInstruction, Box<dyn std::error::Error>>
2594    {
2595        let solana_instruction = spl_token_2022_interface::instruction::transfer_checked(
2596            &spl_token_2022_interface::ID,
2597            source,
2598            mint,
2599            destination,
2600            authority,
2601            &[],
2602            amount,
2603            decimals,
2604        )?;
2605
2606        let message = Message::new(&[solana_instruction], None);
2607        let compiled_instruction = &message.instructions[0];
2608
2609        let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None);
2610
2611        let parsed = parse_instruction::parse(
2612            &spl_token_2022_interface::ID,
2613            compiled_instruction,
2614            &account_keys_for_parsing,
2615            None,
2616        )?;
2617
2618        Ok(parsed)
2619    }
2620
2621    fn create_parsed_token2022_burn(
2622        account: &Pubkey,
2623        mint: &Pubkey,
2624        authority: &Pubkey,
2625        amount: u64,
2626    ) -> Result<solana_transaction_status_client_types::ParsedInstruction, Box<dyn std::error::Error>>
2627    {
2628        let solana_instruction = spl_token_2022_interface::instruction::burn(
2629            &spl_token_2022_interface::ID,
2630            account,
2631            mint,
2632            authority,
2633            &[],
2634            amount,
2635        )?;
2636
2637        let message = Message::new(&[solana_instruction], None);
2638        let compiled_instruction = &message.instructions[0];
2639
2640        let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None);
2641
2642        let parsed = parse_instruction::parse(
2643            &spl_token_2022_interface::ID,
2644            compiled_instruction,
2645            &account_keys_for_parsing,
2646            None,
2647        )?;
2648
2649        Ok(parsed)
2650    }
2651
2652    fn create_parsed_token2022_burn_checked(
2653        account: &Pubkey,
2654        mint: &Pubkey,
2655        authority: &Pubkey,
2656        amount: u64,
2657        decimals: u8,
2658    ) -> Result<solana_transaction_status_client_types::ParsedInstruction, Box<dyn std::error::Error>>
2659    {
2660        let solana_instruction = spl_token_2022_interface::instruction::burn_checked(
2661            &spl_token_2022_interface::ID,
2662            account,
2663            mint,
2664            authority,
2665            &[],
2666            amount,
2667            decimals,
2668        )?;
2669
2670        let message = Message::new(&[solana_instruction], None);
2671        let compiled_instruction = &message.instructions[0];
2672
2673        let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None);
2674
2675        let parsed = parse_instruction::parse(
2676            &spl_token_2022_interface::ID,
2677            compiled_instruction,
2678            &account_keys_for_parsing,
2679            None,
2680        )?;
2681
2682        Ok(parsed)
2683    }
2684
2685    fn create_parsed_token2022_close_account(
2686        account: &Pubkey,
2687        destination: &Pubkey,
2688        authority: &Pubkey,
2689    ) -> Result<solana_transaction_status_client_types::ParsedInstruction, Box<dyn std::error::Error>>
2690    {
2691        let solana_instruction = spl_token_2022_interface::instruction::close_account(
2692            &spl_token_2022_interface::ID,
2693            account,
2694            destination,
2695            authority,
2696            &[],
2697        )?;
2698
2699        let message = Message::new(&[solana_instruction], None);
2700        let compiled_instruction = &message.instructions[0];
2701
2702        let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None);
2703
2704        let parsed = parse_instruction::parse(
2705            &spl_token_2022_interface::ID,
2706            compiled_instruction,
2707            &account_keys_for_parsing,
2708            None,
2709        )?;
2710
2711        Ok(parsed)
2712    }
2713
2714    fn create_parsed_token2022_approve(
2715        source: &Pubkey,
2716        delegate: &Pubkey,
2717        authority: &Pubkey,
2718        amount: u64,
2719    ) -> Result<solana_transaction_status_client_types::ParsedInstruction, Box<dyn std::error::Error>>
2720    {
2721        let solana_instruction = spl_token_2022_interface::instruction::approve(
2722            &spl_token_2022_interface::ID,
2723            source,
2724            delegate,
2725            authority,
2726            &[],
2727            amount,
2728        )?;
2729
2730        let message = Message::new(&[solana_instruction], None);
2731        let compiled_instruction = &message.instructions[0];
2732
2733        let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None);
2734
2735        let parsed = parse_instruction::parse(
2736            &spl_token_2022_interface::ID,
2737            compiled_instruction,
2738            &account_keys_for_parsing,
2739            None,
2740        )?;
2741
2742        Ok(parsed)
2743    }
2744
2745    fn create_parsed_token2022_approve_checked(
2746        source: &Pubkey,
2747        mint: &Pubkey,
2748        delegate: &Pubkey,
2749        authority: &Pubkey,
2750        amount: u64,
2751        decimals: u8,
2752    ) -> Result<solana_transaction_status_client_types::ParsedInstruction, Box<dyn std::error::Error>>
2753    {
2754        let solana_instruction = spl_token_2022_interface::instruction::approve_checked(
2755            &spl_token_2022_interface::ID,
2756            source,
2757            mint,
2758            delegate,
2759            authority,
2760            &[],
2761            amount,
2762            decimals,
2763        )?;
2764
2765        let message = Message::new(&[solana_instruction], None);
2766        let compiled_instruction = &message.instructions[0];
2767
2768        let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None);
2769
2770        let parsed = parse_instruction::parse(
2771            &spl_token_2022_interface::ID,
2772            compiled_instruction,
2773            &account_keys_for_parsing,
2774            None,
2775        )?;
2776
2777        Ok(parsed)
2778    }
2779
2780    fn create_parsed_token2022_get_account_data_size(
2781        mint: &Pubkey,
2782    ) -> Result<solana_transaction_status_client_types::ParsedInstruction, Box<dyn std::error::Error>>
2783    {
2784        let solana_instruction = spl_token_2022_interface::instruction::get_account_data_size(
2785            &spl_token_2022_interface::ID,
2786            mint,
2787            &[],
2788        )?;
2789
2790        let message = Message::new(&[solana_instruction], None);
2791        let compiled_instruction = &message.instructions[0];
2792
2793        let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None);
2794
2795        let parsed = parse_instruction::parse(
2796            &spl_token_2022_interface::ID,
2797            compiled_instruction,
2798            &account_keys_for_parsing,
2799            None,
2800        )?;
2801
2802        Ok(parsed)
2803    }
2804
2805    #[test]
2806    fn test_get_field_as_u64() {
2807        // Valid JSON number
2808        let valid_number = serde_json::json!({
2809            "amount": 1000
2810        });
2811        assert_eq!(IxUtils::get_field_as_u64(&valid_number, "amount").unwrap(), 1000);
2812
2813        // Valid JSON string containing a number
2814        let valid_string_number = serde_json::json!({
2815            "amount": "2000"
2816        });
2817        assert_eq!(IxUtils::get_field_as_u64(&valid_string_number, "amount").unwrap(), 2000);
2818
2819        // Missing field
2820        let missing_field = serde_json::json!({
2821            "other": 3000
2822        });
2823        let err = IxUtils::get_field_as_u64(&missing_field, "amount").unwrap_err();
2824        assert!(matches!(err, crate::error::KoraError::SerializationError(_)));
2825
2826        // Invalid string that cannot be parsed as a u64
2827        let invalid_string = serde_json::json!({
2828            "amount": "invalid"
2829        });
2830        let err = IxUtils::get_field_as_u64(&invalid_string, "amount").unwrap_err();
2831        assert!(matches!(err, crate::error::KoraError::SerializationError(_)));
2832
2833        // JSON null or other unexpected type
2834        let null_value = serde_json::json!({
2835            "amount": null
2836        });
2837        let err = IxUtils::get_field_as_u64(&null_value, "amount").unwrap_err();
2838        assert!(matches!(err, crate::error::KoraError::SerializationError(_)));
2839
2840        // Unexpected type (array)
2841        let array_value = serde_json::json!({
2842            "amount": [1, 2, 3]
2843        });
2844        let err = IxUtils::get_field_as_u64(&array_value, "amount").unwrap_err();
2845        assert!(matches!(err, crate::error::KoraError::SerializationError(_)));
2846    }
2847
2848    #[test]
2849    fn test_parse_system_instructions_transfer() {
2850        use crate::transaction::versioned_transaction::VersionedTransactionResolved;
2851        use solana_message::{Message, VersionedMessage};
2852        use solana_sdk::{
2853            signature::{Keypair, Signer},
2854            transaction::VersionedTransaction,
2855        };
2856        use solana_system_interface::instruction::transfer;
2857
2858        let sender = Keypair::new();
2859        let receiver = Pubkey::new_unique();
2860        let lamports = 1000u64;
2861
2862        let instruction = transfer(&sender.pubkey(), &receiver, lamports);
2863
2864        let message =
2865            VersionedMessage::Legacy(Message::new(&[instruction], Some(&sender.pubkey())));
2866        let tx = VersionedTransaction::try_new(message, &[&sender]).unwrap();
2867
2868        let resolved_tx = VersionedTransactionResolved::from_kora_built_transaction(&tx)
2869            .expect("Failed to create resolved transaction");
2870
2871        let parsed_instructions = IxUtils::parse_system_instructions(&resolved_tx)
2872            .expect("Failed to parse system instructions");
2873
2874        let transfers = parsed_instructions
2875            .get(&ParsedSystemInstructionType::SystemTransfer)
2876            .expect("Expected SystemTransfer instructions");
2877
2878        assert_eq!(transfers.len(), 1);
2879
2880        match &transfers[0] {
2881            ParsedSystemInstructionData::SystemTransfer {
2882                lamports: parsed_lamports,
2883                sender: parsed_sender,
2884                receiver: parsed_receiver,
2885            } => {
2886                assert_eq!(*parsed_lamports, lamports);
2887                assert_eq!(*parsed_sender, sender.pubkey());
2888                assert_eq!(*parsed_receiver, receiver);
2889            }
2890            _ => panic!("Expected SystemTransfer variant"),
2891        }
2892    }
2893
2894    #[test]
2895    fn test_parse_token_instructions_transfer() {
2896        use crate::transaction::versioned_transaction::VersionedTransactionResolved;
2897        use solana_message::{Message, VersionedMessage};
2898        use solana_sdk::{
2899            signature::{Keypair, Signer},
2900            transaction::VersionedTransaction,
2901        };
2902        use spl_token_interface::instruction::transfer;
2903
2904        let owner = Keypair::new();
2905        let source_address = Pubkey::new_unique();
2906        let destination_address = Pubkey::new_unique();
2907        let amount = 5000u64;
2908
2909        let instruction = transfer(
2910            &spl_token_interface::ID,
2911            &source_address,
2912            &destination_address,
2913            &owner.pubkey(),
2914            &[],
2915            amount,
2916        )
2917        .expect("Failed to create transfer instruction");
2918
2919        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&owner.pubkey())));
2920        let tx = VersionedTransaction::try_new(message, &[&owner]).unwrap();
2921
2922        let resolved_tx = VersionedTransactionResolved::from_kora_built_transaction(&tx)
2923            .expect("Failed to create resolved transaction");
2924
2925        let parsed_instructions = IxUtils::parse_token_instructions(&resolved_tx)
2926            .expect("Failed to parse token instructions");
2927
2928        let transfers = parsed_instructions
2929            .get(&ParsedSPLInstructionType::SplTokenTransfer)
2930            .expect("Expected SplTokenTransfer instructions");
2931
2932        assert_eq!(transfers.len(), 1);
2933
2934        match &transfers[0] {
2935            ParsedSPLInstructionData::SplTokenTransfer {
2936                amount: parsed_amount,
2937                owner: parsed_owner,
2938                mint,
2939                source_address: parsed_source,
2940                destination_address: parsed_destination,
2941                is_2022,
2942            } => {
2943                assert_eq!(*parsed_amount, amount);
2944                assert_eq!(*parsed_owner, owner.pubkey());
2945                assert_eq!(*parsed_source, source_address);
2946                assert_eq!(*parsed_destination, destination_address);
2947                assert!(!is_2022);
2948                assert!(mint.is_none());
2949            }
2950            _ => panic!("Expected SplTokenTransfer variant"),
2951        }
2952    }
2953
2954    #[test]
2955    fn test_parse_token_2022_instructions_transfer_checked() {
2956        use crate::transaction::versioned_transaction::VersionedTransactionResolved;
2957        use solana_message::{Message, VersionedMessage};
2958        use solana_sdk::{
2959            signature::{Keypair, Signer},
2960            transaction::VersionedTransaction,
2961        };
2962        use spl_token_2022_interface::instruction::transfer_checked;
2963
2964        let owner = Keypair::new();
2965        let source_address = Pubkey::new_unique();
2966        let mint = Pubkey::new_unique();
2967        let destination_address = Pubkey::new_unique();
2968        let amount = 7500u64;
2969        let decimals = 6u8;
2970
2971        let instruction = transfer_checked(
2972            &spl_token_2022_interface::ID,
2973            &source_address,
2974            &mint,
2975            &destination_address,
2976            &owner.pubkey(),
2977            &[],
2978            amount,
2979            decimals,
2980        )
2981        .expect("Failed to create transfer_checked instruction");
2982
2983        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&owner.pubkey())));
2984        let tx = VersionedTransaction::try_new(message, &[&owner]).unwrap();
2985
2986        let resolved_tx = VersionedTransactionResolved::from_kora_built_transaction(&tx)
2987            .expect("Failed to create resolved transaction");
2988
2989        let parsed_instructions = IxUtils::parse_token_instructions(&resolved_tx)
2990            .expect("Failed to parse token instructions");
2991
2992        let transfers = parsed_instructions
2993            .get(&ParsedSPLInstructionType::SplTokenTransfer)
2994            .expect("Expected SplTokenTransfer instructions");
2995
2996        assert_eq!(transfers.len(), 1);
2997
2998        match &transfers[0] {
2999            ParsedSPLInstructionData::SplTokenTransfer {
3000                amount: parsed_amount,
3001                owner: parsed_owner,
3002                mint: parsed_mint,
3003                source_address: parsed_source,
3004                destination_address: parsed_destination,
3005                is_2022,
3006            } => {
3007                assert_eq!(*parsed_amount, amount);
3008                assert_eq!(*parsed_owner, owner.pubkey());
3009                assert_eq!(*parsed_source, source_address);
3010                assert_eq!(*parsed_destination, destination_address);
3011                assert!(*is_2022);
3012                assert_eq!(parsed_mint.unwrap(), mint);
3013            }
3014            _ => panic!("Expected SplTokenTransfer variant"),
3015        }
3016    }
3017
3018    #[test]
3019    fn test_parse_token_instructions_burn_checked_requires_three_accounts() {
3020        use crate::transaction::versioned_transaction::VersionedTransactionResolved;
3021        use solana_message::{Message, VersionedMessage};
3022        use solana_sdk::{
3023            instruction::{AccountMeta, Instruction},
3024            signature::{Keypair, Signer},
3025            transaction::VersionedTransaction,
3026        };
3027
3028        let payer = Keypair::new();
3029        let source = Pubkey::new_unique();
3030        let authority = Pubkey::new_unique();
3031        let burn_checked_data = spl_token_interface::instruction::TokenInstruction::BurnChecked {
3032            amount: 10,
3033            decimals: 6,
3034        }
3035        .pack();
3036
3037        let malformed_burn_checked = Instruction {
3038            program_id: spl_token_interface::ID,
3039            accounts: vec![
3040                AccountMeta::new(source, false),
3041                AccountMeta::new_readonly(authority, false),
3042            ],
3043            data: burn_checked_data,
3044        };
3045
3046        let message = VersionedMessage::Legacy(Message::new(
3047            &[malformed_burn_checked],
3048            Some(&payer.pubkey()),
3049        ));
3050        let tx = VersionedTransaction::try_new(message, &[&payer]).unwrap();
3051        let resolved_tx = VersionedTransactionResolved::from_kora_built_transaction(&tx)
3052            .expect("Failed to create resolved transaction");
3053
3054        let result = IxUtils::parse_token_instructions(&resolved_tx);
3055        assert!(result.is_err(), "BurnChecked with 2 accounts must be rejected");
3056    }
3057
3058    #[test]
3059    fn test_parse_token_2022_instructions_burn_checked_requires_three_accounts() {
3060        use crate::transaction::versioned_transaction::VersionedTransactionResolved;
3061        use solana_message::{Message, VersionedMessage};
3062        use solana_sdk::{
3063            instruction::{AccountMeta, Instruction},
3064            signature::{Keypair, Signer},
3065            transaction::VersionedTransaction,
3066        };
3067
3068        let payer = Keypair::new();
3069        let source = Pubkey::new_unique();
3070        let authority = Pubkey::new_unique();
3071        let burn_checked_data =
3072            spl_token_2022_interface::instruction::TokenInstruction::BurnChecked {
3073                amount: 10,
3074                decimals: 6,
3075            }
3076            .pack();
3077
3078        let malformed_burn_checked = Instruction {
3079            program_id: spl_token_2022_interface::ID,
3080            accounts: vec![
3081                AccountMeta::new(source, false),
3082                AccountMeta::new_readonly(authority, false),
3083            ],
3084            data: burn_checked_data,
3085        };
3086
3087        let message = VersionedMessage::Legacy(Message::new(
3088            &[malformed_burn_checked],
3089            Some(&payer.pubkey()),
3090        ));
3091        let tx = VersionedTransaction::try_new(message, &[&payer]).unwrap();
3092        let resolved_tx = VersionedTransactionResolved::from_kora_built_transaction(&tx)
3093            .expect("Failed to create resolved transaction");
3094
3095        let result = IxUtils::parse_token_instructions(&resolved_tx);
3096        assert!(result.is_err(), "Token-2022 BurnChecked with 2 accounts must be rejected");
3097    }
3098
3099    #[test]
3100    fn test_uncompile_instructions() {
3101        let program_id = Pubkey::new_unique();
3102        let account1 = Pubkey::new_unique();
3103        let account2 = Pubkey::new_unique();
3104
3105        let account_keys = vec![program_id, account1, account2];
3106        let compiled_ix = CompiledInstruction {
3107            program_id_index: 0,
3108            accounts: vec![1, 2], // indices into account_keys
3109            data: vec![1, 2, 3],
3110        };
3111
3112        let instructions = IxUtils::uncompile_instructions(&[compiled_ix], &account_keys).unwrap();
3113
3114        assert_eq!(instructions.len(), 1);
3115        let uncompiled = &instructions[0];
3116        assert_eq!(uncompiled.program_id, program_id);
3117        assert_eq!(uncompiled.accounts.len(), 2);
3118        assert_eq!(uncompiled.accounts[0].pubkey, account1);
3119        assert_eq!(uncompiled.accounts[1].pubkey, account2);
3120        assert_eq!(uncompiled.data, vec![1, 2, 3]);
3121    }
3122
3123    #[test]
3124    fn test_reconstruct_instruction_from_ui_compiled() {
3125        let program_id = Pubkey::new_unique();
3126        let account1 = Pubkey::new_unique();
3127        let mut account_keys = vec![program_id, account1];
3128
3129        let ui_compiled = solana_transaction_status_client_types::UiCompiledInstruction {
3130            program_id_index: 0,
3131            accounts: vec![1],
3132            data: bs58::encode(&[1, 2, 3]).into_string(),
3133            stack_height: None,
3134        };
3135
3136        let result = IxUtils::reconstruct_instruction_from_ui(
3137            &UiInstruction::Compiled(ui_compiled),
3138            &mut account_keys,
3139        );
3140
3141        assert!(result.is_ok());
3142        let compiled = result.unwrap();
3143        assert_eq!(compiled.program_id_index, 0);
3144        assert_eq!(compiled.accounts, vec![1]);
3145        assert_eq!(compiled.data, vec![1, 2, 3]);
3146    }
3147
3148    #[test]
3149    fn test_reconstruct_partially_decoded_instruction() {
3150        let program_id = Pubkey::new_unique();
3151        let account1 = Pubkey::new_unique();
3152        let account2 = Pubkey::new_unique();
3153        let mut account_keys = vec![program_id, account1, account2];
3154
3155        let partial = solana_transaction_status_client_types::UiPartiallyDecodedInstruction {
3156            program_id: program_id.to_string(),
3157            accounts: vec![account1.to_string(), account2.to_string()],
3158            data: bs58::encode(&[5, 6, 7]).into_string(),
3159            stack_height: None,
3160        };
3161
3162        let ui_parsed = UiParsedInstruction::PartiallyDecoded(partial);
3163
3164        let result = IxUtils::reconstruct_instruction_from_ui(
3165            &UiInstruction::Parsed(ui_parsed),
3166            &mut account_keys,
3167        );
3168
3169        assert!(result.is_ok());
3170        let compiled = result.unwrap();
3171        assert_eq!(compiled.program_id_index, 0);
3172        assert_eq!(compiled.accounts, vec![1, 2]); // account1, account2 indices
3173        assert_eq!(compiled.data, vec![5, 6, 7]);
3174    }
3175
3176    #[test]
3177    fn test_reconstruct_partially_decoded_with_cpi_pda_accounts() {
3178        // Simulate a CPI inner instruction where the PDA authority is NOT in
3179        // the outer transaction's account keys (e.g. Kamino lending authority).
3180        let program_id = Pubkey::new_unique();
3181        let known_account = Pubkey::new_unique();
3182        let cpi_pda_account = Pubkey::new_unique(); // Not in original keys
3183        let mut account_keys = vec![program_id, known_account];
3184
3185        let partial = solana_transaction_status_client_types::UiPartiallyDecodedInstruction {
3186            program_id: program_id.to_string(),
3187            accounts: vec![known_account.to_string(), cpi_pda_account.to_string()],
3188            data: bs58::encode(&[8, 9, 10]).into_string(),
3189            stack_height: None,
3190        };
3191
3192        let ui_parsed = UiParsedInstruction::PartiallyDecoded(partial);
3193
3194        let result = IxUtils::reconstruct_instruction_from_ui(
3195            &UiInstruction::Parsed(ui_parsed),
3196            &mut account_keys,
3197        );
3198
3199        assert!(result.is_ok(), "Should succeed even with unknown CPI PDA account");
3200        let compiled = result.unwrap();
3201        assert_eq!(compiled.program_id_index, 0);
3202        // Both accounts should be present — the PDA gets a synthetic index
3203        assert_eq!(compiled.accounts.len(), 2);
3204        assert_eq!(compiled.accounts[0], 1); // known_account at index 1
3205        assert_eq!(compiled.accounts[1], 2); // cpi_pda_account extended at index 2
3206        assert_eq!(compiled.data, vec![8, 9, 10]);
3207
3208        // The account_keys vec should have been extended with the PDA
3209        assert_eq!(account_keys.len(), 3);
3210        assert_eq!(account_keys[2], cpi_pda_account);
3211    }
3212
3213    #[test]
3214    fn test_reconstruct_partially_decoded_with_unknown_program_id() {
3215        // When even the program ID is not in the original account keys (CPI to
3216        // a program not referenced in the outer transaction).
3217        let known_account = Pubkey::new_unique();
3218        let cpi_program = Pubkey::new_unique(); // Not in original keys
3219        let cpi_account = Pubkey::new_unique(); // Not in original keys
3220        let mut account_keys = vec![known_account];
3221
3222        let partial = solana_transaction_status_client_types::UiPartiallyDecodedInstruction {
3223            program_id: cpi_program.to_string(),
3224            accounts: vec![known_account.to_string(), cpi_account.to_string()],
3225            data: bs58::encode(&[1]).into_string(),
3226            stack_height: None,
3227        };
3228
3229        let ui_parsed = UiParsedInstruction::PartiallyDecoded(partial);
3230
3231        let result = IxUtils::reconstruct_instruction_from_ui(
3232            &UiInstruction::Parsed(ui_parsed),
3233            &mut account_keys,
3234        );
3235
3236        assert!(result.is_ok(), "Should succeed even with unknown program ID");
3237        let compiled = result.unwrap();
3238        // Program ID gets index 1, cpi_account gets index 2
3239        assert_eq!(compiled.program_id_index, 1);
3240        assert_eq!(compiled.accounts[0], 0); // known_account at original index 0
3241        assert_eq!(compiled.accounts[1], 2); // cpi_account extended at index 2
3242        assert_eq!(account_keys.len(), 3);
3243    }
3244
3245    #[test]
3246    fn test_reconstruct_partially_decoded_rejects_program_index_overflow() {
3247        let mut account_keys: Vec<Pubkey> = (0..256).map(|_| Pubkey::new_unique()).collect();
3248        let cpi_program = Pubkey::new_unique(); // not in account_keys
3249
3250        let partial = solana_transaction_status_client_types::UiPartiallyDecodedInstruction {
3251            program_id: cpi_program.to_string(),
3252            accounts: vec![account_keys[0].to_string()],
3253            data: bs58::encode(&[1]).into_string(),
3254            stack_height: None,
3255        };
3256
3257        let ui_parsed = UiParsedInstruction::PartiallyDecoded(partial);
3258
3259        let result = IxUtils::reconstruct_instruction_from_ui(
3260            &UiInstruction::Parsed(ui_parsed),
3261            &mut account_keys,
3262        );
3263
3264        assert!(result.is_err(), "should fail when program index would overflow u8");
3265        assert_eq!(account_keys.len(), 256, "account keys should remain unchanged on failure");
3266    }
3267
3268    #[test]
3269    fn test_reconstruct_partially_decoded_rejects_account_index_overflow() {
3270        let mut account_keys: Vec<Pubkey> = (0..256).map(|_| Pubkey::new_unique()).collect();
3271        let program_id = account_keys[0];
3272        let known_account = account_keys[1];
3273        let cpi_account = Pubkey::new_unique(); // not in account_keys
3274
3275        let partial = solana_transaction_status_client_types::UiPartiallyDecodedInstruction {
3276            program_id: program_id.to_string(),
3277            accounts: vec![known_account.to_string(), cpi_account.to_string()],
3278            data: bs58::encode(&[2]).into_string(),
3279            stack_height: None,
3280        };
3281
3282        let ui_parsed = UiParsedInstruction::PartiallyDecoded(partial);
3283
3284        let result = IxUtils::reconstruct_instruction_from_ui(
3285            &UiInstruction::Parsed(ui_parsed),
3286            &mut account_keys,
3287        );
3288
3289        assert!(result.is_err(), "should fail when account index would overflow u8");
3290        assert_eq!(account_keys.len(), 256, "account keys should remain unchanged on failure");
3291    }
3292
3293    #[test]
3294    fn test_reconstruct_partially_decoded_rolls_back_on_mid_loop_overflow() {
3295        let mut account_keys: Vec<Pubkey> = (0..254).map(|_| Pubkey::new_unique()).collect();
3296        let program_id = account_keys[0];
3297        let known_account = account_keys[1];
3298        let cpi_account_1 = Pubkey::new_unique();
3299        let cpi_account_2 = Pubkey::new_unique();
3300        let cpi_account_3 = Pubkey::new_unique(); // triggers overflow after two pushes
3301
3302        let partial = solana_transaction_status_client_types::UiPartiallyDecodedInstruction {
3303            program_id: program_id.to_string(),
3304            accounts: vec![
3305                known_account.to_string(),
3306                cpi_account_1.to_string(),
3307                cpi_account_2.to_string(),
3308                cpi_account_3.to_string(),
3309            ],
3310            data: bs58::encode(&[3]).into_string(),
3311            stack_height: None,
3312        };
3313
3314        let ui_parsed = UiParsedInstruction::PartiallyDecoded(partial);
3315
3316        let result = IxUtils::reconstruct_instruction_from_ui(
3317            &UiInstruction::Parsed(ui_parsed),
3318            &mut account_keys,
3319        );
3320
3321        assert!(result.is_err(), "should fail when account index overflows mid-loop");
3322        assert_eq!(account_keys.len(), 254, "account keys should roll back to snapshot length");
3323    }
3324
3325    #[test]
3326    fn test_reconstruct_system_transfer_instruction() {
3327        let source = Pubkey::new_unique();
3328        let destination = Pubkey::new_unique();
3329        let system_program_id = SYSTEM_PROGRAM_ID;
3330        let account_keys = vec![system_program_id, source, destination];
3331        let lamports = 1000000u64;
3332
3333        let transfer_instruction =
3334            solana_system_interface::instruction::transfer(&source, &destination, lamports);
3335
3336        let solana_parsed_transfer = create_parsed_system_transfer(&source, &destination, lamports)
3337            .expect("Failed to create authentic parsed instruction");
3338
3339        let result = IxUtils::reconstruct_system_instruction(
3340            &solana_parsed_transfer,
3341            &IxUtils::build_account_keys_hashmap(&account_keys),
3342        );
3343
3344        assert!(result.is_ok());
3345        let compiled = result.unwrap();
3346        assert_eq!(compiled.program_id_index, 0);
3347        assert_eq!(compiled.accounts, vec![1, 2]); // source, destination indices
3348        assert_eq!(compiled.data, transfer_instruction.data);
3349    }
3350
3351    #[test]
3352    fn test_reconstruct_system_transfer_with_seed_instruction() {
3353        let source = Pubkey::new_unique();
3354        let destination = Pubkey::new_unique();
3355        let source_base = Pubkey::new_unique();
3356        let source_owner = Pubkey::new_unique();
3357        let system_program_id = SYSTEM_PROGRAM_ID;
3358        let account_keys = vec![system_program_id, source, source_base, destination];
3359        let lamports = 5000000u64;
3360
3361        let instruction = solana_system_interface::instruction::transfer_with_seed(
3362            &source,
3363            &source_base,
3364            "test_seed".to_string(),
3365            &source_owner,
3366            &destination,
3367            lamports,
3368        );
3369
3370        let solana_parsed = create_parsed_system_transfer_with_seed(
3371            &source,
3372            &destination,
3373            lamports,
3374            &source_base,
3375            "test_seed",
3376            &source_owner,
3377        )
3378        .expect("Failed to create parsed instruction");
3379
3380        let result = IxUtils::reconstruct_system_instruction(
3381            &solana_parsed,
3382            &IxUtils::build_account_keys_hashmap(&account_keys),
3383        );
3384
3385        assert!(result.is_ok());
3386        let compiled = result.unwrap();
3387        assert_eq!(compiled.program_id_index, 0);
3388        assert_eq!(compiled.accounts, vec![1, 2, 3]); // source, source_base, destination indices
3389        assert_eq!(compiled.data, instruction.data);
3390    }
3391
3392    #[test]
3393    fn test_reconstruct_system_create_account_instruction() {
3394        let source = Pubkey::new_unique();
3395        let new_account = Pubkey::new_unique();
3396        let owner = Pubkey::new_unique();
3397        let system_program_id = SYSTEM_PROGRAM_ID;
3398        let account_keys = vec![system_program_id, source, new_account];
3399        let lamports = 2000000u64;
3400        let space = 165u64;
3401
3402        let instruction = solana_system_interface::instruction::create_account(
3403            &source,
3404            &new_account,
3405            lamports,
3406            space,
3407            &owner,
3408        );
3409
3410        let solana_parsed =
3411            create_parsed_system_create_account(&source, &new_account, lamports, space, &owner)
3412                .expect("Failed to create parsed instruction");
3413
3414        let result = IxUtils::reconstruct_system_instruction(
3415            &solana_parsed,
3416            &IxUtils::build_account_keys_hashmap(&account_keys),
3417        );
3418
3419        assert!(result.is_ok());
3420        let compiled = result.unwrap();
3421        assert_eq!(compiled.program_id_index, 0);
3422        assert_eq!(compiled.accounts, vec![1, 2]); // source, new_account indices
3423        assert_eq!(compiled.data, instruction.data);
3424    }
3425
3426    #[test]
3427    fn test_reconstruct_system_create_account_with_seed_instruction() {
3428        let source = Pubkey::new_unique();
3429        let new_account = Pubkey::new_unique();
3430        let base = Pubkey::new_unique();
3431        let owner = Pubkey::new_unique();
3432        let system_program_id = SYSTEM_PROGRAM_ID;
3433        let account_keys = vec![system_program_id, source, new_account, base];
3434        let lamports = 3000000u64;
3435        let space = 200u64;
3436
3437        let instruction = solana_system_interface::instruction::create_account_with_seed(
3438            &source,
3439            &new_account,
3440            &base,
3441            "test_seed_create",
3442            lamports,
3443            space,
3444            &owner,
3445        );
3446
3447        let solana_parsed = create_parsed_system_create_account_with_seed(
3448            &source,
3449            &new_account,
3450            &base,
3451            "test_seed_create",
3452            lamports,
3453            space,
3454            &owner,
3455        )
3456        .expect("Failed to create parsed instruction");
3457
3458        let result = IxUtils::reconstruct_system_instruction(
3459            &solana_parsed,
3460            &IxUtils::build_account_keys_hashmap(&account_keys),
3461        );
3462
3463        assert!(result.is_ok());
3464        let compiled = result.unwrap();
3465        assert_eq!(compiled.program_id_index, 0);
3466        assert_eq!(compiled.accounts, vec![1, 2, 3]); // source, new_account, base indices
3467        assert_eq!(compiled.data, instruction.data);
3468    }
3469
3470    #[test]
3471    fn test_reconstruct_system_assign_instruction() {
3472        let account = Pubkey::new_unique();
3473        let owner = Pubkey::new_unique();
3474        let system_program_id = SYSTEM_PROGRAM_ID;
3475        let account_keys = vec![system_program_id, account];
3476
3477        let instruction = solana_system_interface::instruction::assign(&account, &owner);
3478
3479        let solana_parsed = create_parsed_system_assign(&account, &owner)
3480            .expect("Failed to create parsed instruction");
3481
3482        let result = IxUtils::reconstruct_system_instruction(
3483            &solana_parsed,
3484            &IxUtils::build_account_keys_hashmap(&account_keys),
3485        );
3486
3487        assert!(result.is_ok());
3488        let compiled = result.unwrap();
3489        assert_eq!(compiled.program_id_index, 0);
3490        assert_eq!(compiled.accounts, vec![1]); // account index
3491        assert_eq!(compiled.data, instruction.data);
3492    }
3493
3494    #[test]
3495    fn test_reconstruct_system_assign_with_seed_instruction() {
3496        let account = Pubkey::new_unique();
3497        let base = Pubkey::new_unique();
3498        let owner = Pubkey::new_unique();
3499        let system_program_id = SYSTEM_PROGRAM_ID;
3500        let account_keys = vec![system_program_id, account, base];
3501
3502        let instruction = solana_system_interface::instruction::assign_with_seed(
3503            &account,
3504            &base,
3505            "test_assign_seed",
3506            &owner,
3507        );
3508
3509        let solana_parsed =
3510            create_parsed_system_assign_with_seed(&account, &base, "test_assign_seed", &owner)
3511                .expect("Failed to create parsed instruction");
3512
3513        let result = IxUtils::reconstruct_system_instruction(
3514            &solana_parsed,
3515            &IxUtils::build_account_keys_hashmap(&account_keys),
3516        );
3517
3518        assert!(result.is_ok());
3519        let compiled = result.unwrap();
3520        assert_eq!(compiled.program_id_index, 0);
3521        assert_eq!(compiled.accounts, vec![1, 2]); // account, base indices
3522        assert_eq!(compiled.data, instruction.data);
3523    }
3524
3525    #[test]
3526    fn test_reconstruct_system_withdraw_nonce_account_instruction() {
3527        let nonce_account = Pubkey::new_unique();
3528        let recipient = Pubkey::new_unique();
3529        let nonce_authority = Pubkey::new_unique();
3530        let system_program_id = SYSTEM_PROGRAM_ID;
3531        let account_keys = vec![system_program_id, nonce_account, recipient, nonce_authority];
3532        let lamports = 1500000u64;
3533
3534        let instruction = solana_system_interface::instruction::withdraw_nonce_account(
3535            &nonce_account,
3536            &nonce_authority,
3537            &recipient,
3538            lamports,
3539        );
3540
3541        let solana_parsed = create_parsed_system_withdraw_nonce_account(
3542            &nonce_account,
3543            &nonce_authority,
3544            &recipient,
3545            lamports,
3546        )
3547        .expect("Failed to create parsed instruction");
3548
3549        let result = IxUtils::reconstruct_system_instruction(
3550            &solana_parsed,
3551            &IxUtils::build_account_keys_hashmap(&account_keys),
3552        );
3553
3554        assert!(result.is_ok());
3555        let compiled = result.unwrap();
3556        assert_eq!(compiled.program_id_index, 0);
3557        assert_eq!(compiled.accounts, vec![1, 2, 3]); // nonce_account, recipient, nonce_authority indices
3558        assert_eq!(compiled.data, instruction.data);
3559    }
3560
3561    #[test]
3562    fn test_reconstruct_spl_token_transfer_instruction() {
3563        let source = Pubkey::new_unique();
3564        let destination = Pubkey::new_unique();
3565        let authority = Pubkey::new_unique();
3566        let token_program_id = spl_token_interface::ID;
3567        let account_keys = vec![token_program_id, source, destination, authority];
3568        let amount = 1000000u64;
3569
3570        let transfer_instruction = spl_token_interface::instruction::transfer(
3571            &spl_token_interface::ID,
3572            &source,
3573            &destination,
3574            &authority,
3575            &[],
3576            amount,
3577        )
3578        .expect("Failed to create transfer instruction");
3579
3580        let solana_parsed_transfer =
3581            create_parsed_spl_token_transfer(&source, &destination, &authority, amount)
3582                .expect("Failed to create parsed instruction");
3583
3584        let result = IxUtils::reconstruct_spl_token_instruction(
3585            &solana_parsed_transfer,
3586            &IxUtils::build_account_keys_hashmap(&account_keys),
3587        );
3588
3589        assert!(result.is_ok());
3590        let compiled = result.unwrap();
3591        assert_eq!(compiled.program_id_index, 0);
3592        assert_eq!(compiled.accounts, vec![1, 2, 3]); // source, destination, authority indices
3593        assert_eq!(compiled.data, transfer_instruction.data);
3594    }
3595
3596    #[test]
3597    fn test_reconstruct_spl_token_transfer_checked_instruction() {
3598        let source = Pubkey::new_unique();
3599        let destination = Pubkey::new_unique();
3600        let authority = Pubkey::new_unique();
3601        let mint = Pubkey::new_unique();
3602        let token_program_id = spl_token_interface::ID;
3603        let account_keys = vec![token_program_id, source, mint, destination, authority];
3604        let amount = 2000000u64;
3605        let decimals = 6u8;
3606
3607        let instruction = spl_token_interface::instruction::transfer_checked(
3608            &spl_token_interface::ID,
3609            &source,
3610            &mint,
3611            &destination,
3612            &authority,
3613            &[],
3614            amount,
3615            decimals,
3616        )
3617        .expect("Failed to create transfer_checked instruction");
3618
3619        let solana_parsed = create_parsed_spl_token_transfer_checked(
3620            &source,
3621            &mint,
3622            &destination,
3623            &authority,
3624            amount,
3625            decimals,
3626        )
3627        .expect("Failed to create parsed instruction");
3628
3629        let result = IxUtils::reconstruct_spl_token_instruction(
3630            &solana_parsed,
3631            &IxUtils::build_account_keys_hashmap(&account_keys),
3632        );
3633
3634        assert!(result.is_ok());
3635        let compiled = result.unwrap();
3636        assert_eq!(compiled.program_id_index, 0);
3637        assert_eq!(compiled.accounts, vec![1, 2, 3, 4]); // source, mint, destination, authority indices
3638        assert_eq!(compiled.data, instruction.data);
3639    }
3640
3641    #[test]
3642    fn test_reconstruct_spl_token_burn_instruction() {
3643        let account = Pubkey::new_unique();
3644        let mint = Pubkey::new_unique();
3645        let authority = Pubkey::new_unique();
3646        let token_program_id = spl_token_interface::ID;
3647        let account_keys = vec![token_program_id, account, mint, authority];
3648        let amount = 500000u64;
3649
3650        let instruction = spl_token_interface::instruction::burn(
3651            &spl_token_interface::ID,
3652            &account,
3653            &mint,
3654            &authority,
3655            &[],
3656            amount,
3657        )
3658        .expect("Failed to create burn instruction");
3659
3660        let solana_parsed = create_parsed_spl_token_burn(&account, &mint, &authority, amount)
3661            .expect("Failed to create parsed instruction");
3662
3663        let result = IxUtils::reconstruct_spl_token_instruction(
3664            &solana_parsed,
3665            &IxUtils::build_account_keys_hashmap(&account_keys),
3666        );
3667
3668        assert!(result.is_ok());
3669        let compiled = result.unwrap();
3670        assert_eq!(compiled.program_id_index, 0);
3671        assert_eq!(compiled.accounts, vec![1, 2, 3]); // account, mint, authority indices (mint included when present in parsed data)
3672        assert_eq!(compiled.data, instruction.data);
3673    }
3674
3675    #[test]
3676    fn test_reconstruct_spl_token_burn_instruction_falls_back_without_mint_index() {
3677        let account = Pubkey::new_unique();
3678        let mint = Pubkey::new_unique();
3679        let authority = Pubkey::new_unique();
3680        let token_program_id = spl_token_interface::ID;
3681        // Deliberately omit mint from account keys to validate fallback behavior.
3682        let account_keys = vec![token_program_id, account, authority];
3683        let amount = 500000u64;
3684
3685        let instruction = spl_token_interface::instruction::burn(
3686            &spl_token_interface::ID,
3687            &account,
3688            &mint,
3689            &authority,
3690            &[],
3691            amount,
3692        )
3693        .expect("Failed to create burn instruction");
3694
3695        let solana_parsed = create_parsed_spl_token_burn(&account, &mint, &authority, amount)
3696            .expect("Failed to create parsed instruction");
3697
3698        let result = IxUtils::reconstruct_spl_token_instruction(
3699            &solana_parsed,
3700            &IxUtils::build_account_keys_hashmap(&account_keys),
3701        );
3702
3703        assert!(result.is_ok());
3704        let compiled = result.unwrap();
3705        assert_eq!(compiled.program_id_index, 0);
3706        assert_eq!(compiled.accounts, vec![1, 2]); // account, authority fallback
3707        assert_eq!(compiled.data, instruction.data);
3708    }
3709
3710    #[test]
3711    fn test_reconstruct_spl_token_burn_checked_instruction() {
3712        let account = Pubkey::new_unique();
3713        let authority = Pubkey::new_unique();
3714        let mint = Pubkey::new_unique();
3715        let token_program_id = spl_token_interface::ID;
3716        let account_keys = vec![token_program_id, account, mint, authority];
3717        let amount = 750000u64;
3718        let decimals = 6u8;
3719
3720        let instruction = spl_token_interface::instruction::burn_checked(
3721            &spl_token_interface::ID,
3722            &account,
3723            &mint,
3724            &authority,
3725            &[],
3726            amount,
3727            decimals,
3728        )
3729        .expect("Failed to create burn_checked instruction");
3730
3731        let solana_parsed =
3732            create_parsed_spl_token_burn_checked(&account, &mint, &authority, amount, decimals)
3733                .expect("Failed to create parsed instruction");
3734
3735        let result = IxUtils::reconstruct_spl_token_instruction(
3736            &solana_parsed,
3737            &IxUtils::build_account_keys_hashmap(&account_keys),
3738        );
3739
3740        assert!(result.is_ok());
3741        let compiled = result.unwrap();
3742        assert_eq!(compiled.program_id_index, 0);
3743        assert_eq!(compiled.accounts, vec![1, 2, 3]); // account, mint, authority indices
3744        assert_eq!(compiled.data, instruction.data);
3745    }
3746
3747    #[test]
3748    fn test_reconstruct_spl_token_close_account_instruction() {
3749        let account = Pubkey::new_unique();
3750        let destination = Pubkey::new_unique();
3751        let authority = Pubkey::new_unique();
3752        let token_program_id = spl_token_interface::ID;
3753        let account_keys = vec![token_program_id, account, destination, authority];
3754
3755        let instruction = spl_token_interface::instruction::close_account(
3756            &spl_token_interface::ID,
3757            &account,
3758            &destination,
3759            &authority,
3760            &[],
3761        )
3762        .expect("Failed to create close_account instruction");
3763
3764        let solana_parsed =
3765            create_parsed_spl_token_close_account(&account, &destination, &authority)
3766                .expect("Failed to create parsed instruction");
3767
3768        let result = IxUtils::reconstruct_spl_token_instruction(
3769            &solana_parsed,
3770            &IxUtils::build_account_keys_hashmap(&account_keys),
3771        );
3772
3773        assert!(result.is_ok());
3774        let compiled = result.unwrap();
3775        assert_eq!(compiled.program_id_index, 0);
3776        assert_eq!(compiled.accounts, vec![1, 2, 3]); // account, destination, authority indices
3777        assert_eq!(compiled.data, instruction.data);
3778    }
3779
3780    #[test]
3781    fn test_reconstruct_spl_token_approve_instruction() {
3782        let source = Pubkey::new_unique();
3783        let delegate = Pubkey::new_unique();
3784        let owner = Pubkey::new_unique();
3785        let token_program_id = spl_token_interface::ID;
3786        let account_keys = vec![token_program_id, source, delegate, owner];
3787        let amount = 1000000u64;
3788
3789        let instruction = spl_token_interface::instruction::approve(
3790            &spl_token_interface::ID,
3791            &source,
3792            &delegate,
3793            &owner,
3794            &[],
3795            amount,
3796        )
3797        .expect("Failed to create approve instruction");
3798
3799        let solana_parsed = create_parsed_spl_token_approve(&source, &delegate, &owner, amount)
3800            .expect("Failed to create parsed instruction");
3801
3802        let result = IxUtils::reconstruct_spl_token_instruction(
3803            &solana_parsed,
3804            &IxUtils::build_account_keys_hashmap(&account_keys),
3805        );
3806
3807        assert!(result.is_ok());
3808        let compiled = result.unwrap();
3809        assert_eq!(compiled.program_id_index, 0);
3810        assert_eq!(compiled.accounts, vec![1, 2, 3]); // source, delegate, owner indices
3811        assert_eq!(compiled.data, instruction.data);
3812    }
3813
3814    #[test]
3815    fn test_reconstruct_spl_token_approve_checked_instruction() {
3816        let source = Pubkey::new_unique();
3817        let delegate = Pubkey::new_unique();
3818        let owner = Pubkey::new_unique();
3819        let mint = Pubkey::new_unique();
3820        let token_program_id = spl_token_interface::ID;
3821        let account_keys = vec![token_program_id, source, mint, delegate, owner];
3822        let amount = 2500000u64;
3823        let decimals = 6u8;
3824
3825        let instruction = spl_token_interface::instruction::approve_checked(
3826            &spl_token_interface::ID,
3827            &source,
3828            &mint,
3829            &delegate,
3830            &owner,
3831            &[],
3832            amount,
3833            decimals,
3834        )
3835        .expect("Failed to create approve_checked instruction");
3836
3837        let solana_parsed = create_parsed_spl_token_approve_checked(
3838            &source, &mint, &delegate, &owner, amount, decimals,
3839        )
3840        .expect("Failed to create parsed instruction");
3841
3842        let result = IxUtils::reconstruct_spl_token_instruction(
3843            &solana_parsed,
3844            &IxUtils::build_account_keys_hashmap(&account_keys),
3845        );
3846
3847        assert!(result.is_ok());
3848        let compiled = result.unwrap();
3849        assert_eq!(compiled.program_id_index, 0);
3850        assert_eq!(compiled.accounts, vec![1, 2, 3, 4]); // source, mint, delegate, owner indices
3851        assert_eq!(compiled.data, instruction.data);
3852    }
3853
3854    #[test]
3855    fn test_reconstruct_token2022_transfer_instruction() {
3856        let source = Pubkey::new_unique();
3857        let destination = Pubkey::new_unique();
3858        let authority = Pubkey::new_unique();
3859        let token_program_id = spl_token_2022_interface::ID;
3860        let account_keys = vec![token_program_id, source, destination, authority];
3861        let amount = 1500000u64;
3862
3863        #[allow(deprecated)]
3864        let instruction = spl_token_2022_interface::instruction::transfer(
3865            &spl_token_2022_interface::ID,
3866            &source,
3867            &destination,
3868            &authority,
3869            &[],
3870            amount,
3871        )
3872        .expect("Failed to create transfer instruction");
3873
3874        let solana_parsed =
3875            create_parsed_token2022_transfer(&source, &destination, &authority, amount)
3876                .expect("Failed to create parsed instruction");
3877
3878        let result = IxUtils::reconstruct_spl_token_instruction(
3879            &solana_parsed,
3880            &IxUtils::build_account_keys_hashmap(&account_keys),
3881        );
3882
3883        assert!(result.is_ok());
3884        let compiled = result.unwrap();
3885        assert_eq!(compiled.program_id_index, 0);
3886        assert_eq!(compiled.accounts, vec![1, 2, 3]); // source, destination, authority indices
3887        assert_eq!(compiled.data, instruction.data);
3888    }
3889
3890    #[test]
3891    fn test_reconstruct_token2022_transfer_checked_instruction() {
3892        let source = Pubkey::new_unique();
3893        let destination = Pubkey::new_unique();
3894        let authority = Pubkey::new_unique();
3895        let mint = Pubkey::new_unique();
3896        let token_program_id = spl_token_2022_interface::ID;
3897        let account_keys = vec![token_program_id, source, mint, destination, authority];
3898        let amount = 3000000u64;
3899        let decimals = 6u8;
3900
3901        let instruction = spl_token_2022_interface::instruction::transfer_checked(
3902            &spl_token_2022_interface::ID,
3903            &source,
3904            &mint,
3905            &destination,
3906            &authority,
3907            &[],
3908            amount,
3909            decimals,
3910        )
3911        .expect("Failed to create transfer_checked instruction");
3912
3913        let solana_parsed = create_parsed_token2022_transfer_checked(
3914            &source,
3915            &mint,
3916            &destination,
3917            &authority,
3918            amount,
3919            decimals,
3920        )
3921        .expect("Failed to create parsed instruction");
3922
3923        let result = IxUtils::reconstruct_spl_token_instruction(
3924            &solana_parsed,
3925            &IxUtils::build_account_keys_hashmap(&account_keys),
3926        );
3927
3928        assert!(result.is_ok());
3929        let compiled = result.unwrap();
3930        assert_eq!(compiled.program_id_index, 0);
3931        assert_eq!(compiled.accounts, vec![1, 2, 3, 4]); // source, mint, destination, authority indices
3932        assert_eq!(compiled.data, instruction.data);
3933    }
3934
3935    #[test]
3936    fn test_reconstruct_token2022_get_account_data_size_instruction() {
3937        let mint = Pubkey::new_unique();
3938        let token_program_id = spl_token_2022_interface::ID;
3939        let account_keys = vec![token_program_id, mint];
3940
3941        let instruction = spl_token_2022_interface::instruction::get_account_data_size(
3942            &spl_token_2022_interface::ID,
3943            &mint,
3944            &[],
3945        )
3946        .expect("Failed to create get_account_data_size instruction");
3947
3948        let solana_parsed = create_parsed_token2022_get_account_data_size(&mint)
3949            .expect("Failed to create parsed instruction");
3950
3951        let result = IxUtils::reconstruct_spl_token_instruction(
3952            &solana_parsed,
3953            &IxUtils::build_account_keys_hashmap(&account_keys),
3954        );
3955
3956        assert!(result.is_ok());
3957        let compiled = result.unwrap();
3958        assert_eq!(compiled.program_id_index, 0);
3959        assert_eq!(compiled.accounts, vec![1]); // mint index
3960        assert_eq!(compiled.data, instruction.data);
3961    }
3962
3963    #[test]
3964    fn test_reconstruct_token2022_burn_instruction() {
3965        let account = Pubkey::new_unique();
3966        let mint = Pubkey::new_unique();
3967        let authority = Pubkey::new_unique();
3968        let token_program_id = spl_token_2022_interface::ID;
3969        let account_keys = vec![token_program_id, account, mint, authority];
3970        let amount = 800000u64;
3971
3972        let instruction = spl_token_2022_interface::instruction::burn(
3973            &spl_token_2022_interface::ID,
3974            &account,
3975            &mint,
3976            &authority,
3977            &[],
3978            amount,
3979        )
3980        .expect("Failed to create burn instruction");
3981
3982        let solana_parsed = create_parsed_token2022_burn(&account, &mint, &authority, amount)
3983            .expect("Failed to create parsed instruction");
3984
3985        let result = IxUtils::reconstruct_spl_token_instruction(
3986            &solana_parsed,
3987            &IxUtils::build_account_keys_hashmap(&account_keys),
3988        );
3989
3990        assert!(result.is_ok());
3991        let compiled = result.unwrap();
3992        assert_eq!(compiled.program_id_index, 0);
3993        assert_eq!(compiled.accounts, vec![1, 2, 3]); // account, mint, authority indices (mint included when present in parsed data)
3994        assert_eq!(compiled.data, instruction.data);
3995    }
3996
3997    #[test]
3998    fn test_reconstruct_token2022_burn_checked_instruction() {
3999        let account = Pubkey::new_unique();
4000        let authority = Pubkey::new_unique();
4001        let mint = Pubkey::new_unique();
4002        let token_program_id = spl_token_2022_interface::ID;
4003        let account_keys = vec![token_program_id, account, mint, authority];
4004        let amount = 900000u64;
4005        let decimals = 6u8;
4006
4007        let instruction = spl_token_2022_interface::instruction::burn_checked(
4008            &spl_token_2022_interface::ID,
4009            &account,
4010            &mint,
4011            &authority,
4012            &[],
4013            amount,
4014            decimals,
4015        )
4016        .expect("Failed to create burn_checked instruction");
4017
4018        let solana_parsed =
4019            create_parsed_token2022_burn_checked(&account, &mint, &authority, amount, decimals)
4020                .expect("Failed to create parsed instruction");
4021
4022        let result = IxUtils::reconstruct_spl_token_instruction(
4023            &solana_parsed,
4024            &IxUtils::build_account_keys_hashmap(&account_keys),
4025        );
4026
4027        assert!(result.is_ok());
4028        let compiled = result.unwrap();
4029        assert_eq!(compiled.program_id_index, 0);
4030        assert_eq!(compiled.accounts, vec![1, 2, 3]); // account, mint, authority indices
4031        assert_eq!(compiled.data, instruction.data);
4032    }
4033
4034    #[test]
4035    fn test_reconstruct_token2022_close_account_instruction() {
4036        let account = Pubkey::new_unique();
4037        let destination = Pubkey::new_unique();
4038        let authority = Pubkey::new_unique();
4039        let token_program_id = spl_token_2022_interface::ID;
4040        let account_keys = vec![token_program_id, account, destination, authority];
4041
4042        let instruction = spl_token_2022_interface::instruction::close_account(
4043            &spl_token_2022_interface::ID,
4044            &account,
4045            &destination,
4046            &authority,
4047            &[],
4048        )
4049        .expect("Failed to create close_account instruction");
4050
4051        let solana_parsed =
4052            create_parsed_token2022_close_account(&account, &destination, &authority)
4053                .expect("Failed to create parsed instruction");
4054
4055        let result = IxUtils::reconstruct_spl_token_instruction(
4056            &solana_parsed,
4057            &IxUtils::build_account_keys_hashmap(&account_keys),
4058        );
4059
4060        assert!(result.is_ok());
4061        let compiled = result.unwrap();
4062        assert_eq!(compiled.program_id_index, 0);
4063        assert_eq!(compiled.accounts, vec![1, 2, 3]); // account, destination, authority indices
4064        assert_eq!(compiled.data, instruction.data);
4065    }
4066
4067    #[test]
4068    fn test_reconstruct_token2022_approve_instruction() {
4069        let source = Pubkey::new_unique();
4070        let delegate = Pubkey::new_unique();
4071        let owner = Pubkey::new_unique();
4072        let token_program_id = spl_token_2022_interface::ID;
4073        let account_keys = vec![token_program_id, source, delegate, owner];
4074        let amount = 1200000u64;
4075
4076        let instruction = spl_token_2022_interface::instruction::approve(
4077            &spl_token_2022_interface::ID,
4078            &source,
4079            &delegate,
4080            &owner,
4081            &[],
4082            amount,
4083        )
4084        .expect("Failed to create approve instruction");
4085
4086        let solana_parsed = create_parsed_token2022_approve(&source, &delegate, &owner, amount)
4087            .expect("Failed to create parsed instruction");
4088
4089        let result = IxUtils::reconstruct_spl_token_instruction(
4090            &solana_parsed,
4091            &IxUtils::build_account_keys_hashmap(&account_keys),
4092        );
4093
4094        assert!(result.is_ok());
4095        let compiled = result.unwrap();
4096        assert_eq!(compiled.program_id_index, 0);
4097        assert_eq!(compiled.accounts, vec![1, 2, 3]); // source, delegate, owner indices
4098        assert_eq!(compiled.data, instruction.data);
4099    }
4100
4101    #[test]
4102    fn test_reconstruct_token2022_approve_checked_instruction() {
4103        let source = Pubkey::new_unique();
4104        let delegate = Pubkey::new_unique();
4105        let owner = Pubkey::new_unique();
4106        let mint = Pubkey::new_unique();
4107        let token_program_id = spl_token_2022_interface::ID;
4108        let account_keys = vec![token_program_id, source, mint, delegate, owner];
4109        let amount = 3500000u64;
4110        let decimals = 6u8;
4111
4112        let instruction = spl_token_2022_interface::instruction::approve_checked(
4113            &spl_token_2022_interface::ID,
4114            &source,
4115            &mint,
4116            &delegate,
4117            &owner,
4118            &[],
4119            amount,
4120            decimals,
4121        )
4122        .expect("Failed to create approve_checked instruction");
4123
4124        let solana_parsed = create_parsed_token2022_approve_checked(
4125            &source, &mint, &delegate, &owner, amount, decimals,
4126        )
4127        .expect("Failed to create parsed instruction");
4128
4129        let result = IxUtils::reconstruct_spl_token_instruction(
4130            &solana_parsed,
4131            &IxUtils::build_account_keys_hashmap(&account_keys),
4132        );
4133
4134        assert!(result.is_ok());
4135        let compiled = result.unwrap();
4136        assert_eq!(compiled.program_id_index, 0);
4137        assert_eq!(compiled.accounts, vec![1, 2, 3, 4]); // source, mint, delegate, owner indices
4138        assert_eq!(compiled.data, instruction.data);
4139    }
4140
4141    #[test]
4142    fn test_dispatch_routes_spl_token_via_program_id() {
4143        let source = Pubkey::new_unique();
4144        let destination = Pubkey::new_unique();
4145        let authority = Pubkey::new_unique();
4146        let token_program_id = spl_token_interface::ID;
4147        let mut account_keys = vec![token_program_id, source, destination, authority];
4148        let amount = 1000000u64;
4149
4150        let parsed = create_parsed_spl_token_transfer(&source, &destination, &authority, amount)
4151            .expect("Failed to create parsed instruction");
4152
4153        let ui_instruction = UiInstruction::Parsed(UiParsedInstruction::Parsed(parsed));
4154
4155        let result = IxUtils::reconstruct_instruction_from_ui(&ui_instruction, &mut account_keys);
4156
4157        assert!(result.is_ok(), "SPL token transfer should be dispatched via program_id");
4158        let compiled = result.unwrap();
4159        assert_eq!(compiled.program_id_index, 0);
4160        assert_eq!(compiled.accounts, vec![1, 2, 3]);
4161    }
4162
4163    #[test]
4164    fn test_dispatch_routes_token2022_via_program_id() {
4165        let source = Pubkey::new_unique();
4166        let destination = Pubkey::new_unique();
4167        let authority = Pubkey::new_unique();
4168        let token_program_id = spl_token_2022_interface::ID;
4169        let mut account_keys = vec![token_program_id, source, destination, authority];
4170        let amount = 1000000u64;
4171
4172        let parsed = create_parsed_token2022_transfer(&source, &destination, &authority, amount)
4173            .expect("Failed to create parsed instruction");
4174
4175        let ui_instruction = UiInstruction::Parsed(UiParsedInstruction::Parsed(parsed));
4176
4177        let result = IxUtils::reconstruct_instruction_from_ui(&ui_instruction, &mut account_keys);
4178
4179        assert!(result.is_ok(), "Token2022 transfer should be dispatched via program_id");
4180        let compiled = result.unwrap();
4181        assert_eq!(compiled.program_id_index, 0);
4182        assert_eq!(compiled.accounts, vec![1, 2, 3]);
4183    }
4184
4185    #[test]
4186    fn test_dispatch_routes_system_program_via_program_id() {
4187        let source = Pubkey::new_unique();
4188        let destination = Pubkey::new_unique();
4189        let system_program_id = SYSTEM_PROGRAM_ID;
4190        let mut account_keys = vec![system_program_id, source, destination];
4191        let lamports = 1000000u64;
4192
4193        let parsed = create_parsed_system_transfer(&source, &destination, lamports)
4194            .expect("Failed to create parsed instruction");
4195
4196        let ui_instruction = UiInstruction::Parsed(UiParsedInstruction::Parsed(parsed));
4197
4198        let result = IxUtils::reconstruct_instruction_from_ui(&ui_instruction, &mut account_keys);
4199
4200        assert!(result.is_ok(), "System transfer should be dispatched via program_id");
4201        let compiled = result.unwrap();
4202        assert_eq!(compiled.program_id_index, 0);
4203    }
4204
4205    #[test]
4206    fn test_reconstruct_unsupported_program_creates_stub() {
4207        let unsupported_program = Pubkey::new_unique();
4208        let mut account_keys = vec![unsupported_program];
4209
4210        let parsed_instruction = solana_transaction_status_client_types::ParsedInstruction {
4211            program: "unsupported".to_string(),
4212            program_id: unsupported_program.to_string(),
4213            parsed: serde_json::json!({
4214                "type": "unknownInstruction",
4215                "info": {
4216                    "someField": "someValue"
4217                }
4218            }),
4219            stack_height: None,
4220        };
4221
4222        let ui_instruction = UiInstruction::Parsed(UiParsedInstruction::Parsed(parsed_instruction));
4223
4224        let result = IxUtils::reconstruct_instruction_from_ui(&ui_instruction, &mut account_keys);
4225
4226        assert!(result.is_ok());
4227        let compiled = result.unwrap();
4228
4229        assert_eq!(compiled.program_id_index, 0);
4230        assert!(compiled.accounts.is_empty());
4231        assert!(compiled.data.is_empty());
4232    }
4233}