Skip to main content

light_program_test/program_test/
rpc.rs

1use std::{fmt::Debug, marker::Send};
2
3use anchor_lang::pubkey;
4use async_trait::async_trait;
5use borsh::BorshDeserialize;
6use light_client::{
7    indexer::{CompressedAccount, CompressedTokenAccount, Context, Indexer, Response, TreeInfo},
8    interface::{AccountInterface, MintInterface, MintState, TokenAccountInterface},
9    rpc::{LightClientConfig, Rpc, RpcError},
10};
11use light_compressed_account::TreeType;
12use light_event::{
13    error::ParseIndexerEventError,
14    event::{BatchPublicTransactionEvent, PublicTransactionEvent},
15    parse::event_from_light_transaction,
16};
17use solana_rpc_client_api::config::RpcSendTransactionConfig;
18use solana_sdk::{
19    account::Account,
20    address_lookup_table::AddressLookupTableAccount,
21    clock::{Clock, Slot},
22    hash::Hash,
23    instruction::Instruction,
24    pubkey::Pubkey,
25    rent::Rent,
26    signature::{Keypair, Signature},
27    transaction::Transaction,
28};
29use solana_transaction_status_client_types::TransactionStatus;
30
31use crate::{
32    indexer::{TestIndexer, TestIndexerExtensions},
33    litesvm_extensions::LiteSvmExtensions,
34    program_test::LightProgramTest,
35};
36
37#[async_trait]
38impl Rpc for LightProgramTest {
39    async fn new(_config: LightClientConfig) -> Result<Self, RpcError>
40    where
41        Self: Sized,
42    {
43        Err(RpcError::CustomError(
44            "LightProgramTest::new is not supported in program-test context".into(),
45        ))
46    }
47
48    fn get_payer(&self) -> &Keypair {
49        &self.payer
50    }
51
52    fn get_url(&self) -> String {
53        "get_url doesn't make sense for LightProgramTest".to_string()
54    }
55
56    async fn health(&self) -> Result<(), RpcError> {
57        Ok(())
58    }
59
60    async fn get_program_accounts(
61        &self,
62        program_id: &Pubkey,
63    ) -> Result<Vec<(Pubkey, Account)>, RpcError> {
64        Ok(self.context.get_program_accounts(program_id))
65    }
66
67    async fn get_program_accounts_with_discriminator(
68        &self,
69        program_id: &Pubkey,
70        discriminator: &[u8],
71    ) -> Result<Vec<(Pubkey, Account)>, RpcError> {
72        let all_accounts = self.context.get_program_accounts(program_id);
73        Ok(all_accounts
74            .into_iter()
75            .filter(|(_, account)| {
76                account.data.len() >= discriminator.len()
77                    && &account.data[..discriminator.len()] == discriminator
78            })
79            .collect())
80    }
81
82    async fn confirm_transaction(&self, _transaction: Signature) -> Result<bool, RpcError> {
83        Ok(true)
84    }
85
86    async fn get_account(&self, address: Pubkey) -> Result<Option<Account>, RpcError> {
87        Ok(self.context.get_account(&address))
88    }
89
90    async fn get_multiple_accounts(
91        &self,
92        addresses: &[Pubkey],
93    ) -> Result<Vec<Option<Account>>, RpcError> {
94        Ok(addresses
95            .iter()
96            .map(|address| self.context.get_account(address))
97            .collect())
98    }
99
100    async fn get_minimum_balance_for_rent_exemption(
101        &self,
102        data_len: usize,
103    ) -> Result<u64, RpcError> {
104        let rent = self.context.get_sysvar::<Rent>();
105
106        Ok(rent.minimum_balance(data_len))
107    }
108
109    async fn airdrop_lamports(
110        &mut self,
111        to: &Pubkey,
112        lamports: u64,
113    ) -> Result<Signature, RpcError> {
114        let res = self.context.airdrop(to, lamports).map_err(|e| e.err)?;
115        Ok(res.signature)
116    }
117
118    async fn get_balance(&self, pubkey: &Pubkey) -> Result<u64, RpcError> {
119        Ok(self.context.get_balance(pubkey).unwrap())
120    }
121
122    async fn get_latest_blockhash(&mut self) -> Result<(Hash, u64), RpcError> {
123        let slot = self.get_slot().await?;
124        let hash = self.context.latest_blockhash();
125        Ok((hash, slot))
126    }
127
128    async fn get_slot(&self) -> Result<u64, RpcError> {
129        Ok(self.context.get_sysvar::<Clock>().slot)
130    }
131
132    async fn get_transaction_slot(&self, _signature: &Signature) -> Result<u64, RpcError> {
133        unimplemented!();
134    }
135
136    async fn get_signature_statuses(
137        &self,
138        _signatures: &[Signature],
139    ) -> Result<Vec<Option<TransactionStatus>>, RpcError> {
140        Err(RpcError::CustomError(
141            "get_signature_statuses is unimplemented for LightProgramTest".to_string(),
142        ))
143    }
144
145    async fn send_transaction(&self, _transaction: &Transaction) -> Result<Signature, RpcError> {
146        Err(RpcError::CustomError(
147            "send_transaction is unimplemented for ProgramTestConnection".to_string(),
148        ))
149    }
150
151    async fn send_transaction_with_config(
152        &self,
153        _transaction: &Transaction,
154        _config: RpcSendTransactionConfig,
155    ) -> Result<Signature, RpcError> {
156        Err(RpcError::CustomError(
157            "send_transaction_with_config is unimplemented for ProgramTestConnection".to_string(),
158        ))
159    }
160
161    async fn process_transaction(
162        &mut self,
163        transaction: Transaction,
164    ) -> Result<Signature, RpcError> {
165        let sig = *transaction.signatures.first().unwrap();
166        if self.indexer.is_some() {
167            // Delegate to _send_transaction_with_batched_event which handles counter, logging and pre_context
168            self._send_transaction_with_batched_event(transaction)
169                .await?;
170        } else {
171            // Cache the current context before transaction execution
172            let pre_context_snapshot = self.context.clone();
173
174            // Handle transaction directly without logging (logging should be done elsewhere)
175            self.transaction_counter += 1;
176            let _res = self.context.send_transaction(transaction).map_err(|x| {
177                if self.config.log_failed_tx {
178                    println!("{}", x.meta.pretty_logs());
179                }
180
181                RpcError::TransactionError(x.err)
182            })?;
183
184            self.maybe_print_logs(_res.pretty_logs());
185
186            // Update pre_context only after successful transaction execution
187            self.pre_context = Some(pre_context_snapshot);
188        }
189        Ok(sig)
190    }
191
192    async fn process_transaction_with_context(
193        &mut self,
194        transaction: Transaction,
195    ) -> Result<(Signature, Slot), RpcError> {
196        let sig = *transaction.signatures.first().unwrap();
197
198        // Cache the current context before transaction execution
199        let pre_context_snapshot = self.context.clone();
200
201        self.transaction_counter += 1;
202        let _res = self.context.send_transaction(transaction).map_err(|x| {
203            if self.config.log_failed_tx {
204                println!("{}", x.meta.pretty_logs());
205            }
206            RpcError::TransactionError(x.err)
207        })?;
208
209        let slot = self.context.get_sysvar::<Clock>().slot;
210        self.maybe_print_logs(_res.pretty_logs());
211
212        // Update pre_context only after successful transaction execution
213        self.pre_context = Some(pre_context_snapshot);
214
215        Ok((sig, slot))
216    }
217
218    async fn create_and_send_transaction_with_event<T>(
219        &mut self,
220        instructions: &[Instruction],
221        payer: &Pubkey,
222        signers: &[&Keypair],
223    ) -> Result<Option<(T, Signature, u64)>, RpcError>
224    where
225        T: BorshDeserialize + Send + Debug,
226    {
227        self._create_and_send_transaction_with_event::<T>(instructions, payer, signers)
228            .await
229    }
230
231    async fn create_and_send_transaction_with_batched_event(
232        &mut self,
233        instructions: &[Instruction],
234        payer: &Pubkey,
235        signers: &[&Keypair],
236    ) -> Result<Option<(Vec<BatchPublicTransactionEvent>, Signature, Slot)>, RpcError> {
237        self._create_and_send_transaction_with_batched_event(instructions, payer, signers)
238            .await
239    }
240
241    async fn create_and_send_transaction_with_public_event(
242        &mut self,
243        instruction: &[Instruction],
244        payer: &Pubkey,
245        signers: &[&Keypair],
246    ) -> Result<Option<(PublicTransactionEvent, Signature, Slot)>, RpcError> {
247        let event = self
248            ._create_and_send_transaction_with_batched_event(instruction, payer, signers)
249            .await?;
250        let event = event.map(|e| (e.0[0].event.clone(), e.1, e.2));
251
252        Ok(event)
253    }
254
255    fn indexer(&self) -> Result<&impl Indexer, RpcError> {
256        self.indexer.as_ref().ok_or(RpcError::IndexerNotInitialized)
257    }
258
259    fn indexer_mut(&mut self) -> Result<&mut impl Indexer, RpcError> {
260        self.indexer.as_mut().ok_or(RpcError::IndexerNotInitialized)
261    }
262
263    /// Fetch the latest state tree addresses from the cluster.
264    async fn get_latest_active_state_trees(&mut self) -> Result<Vec<TreeInfo>, RpcError> {
265        #[cfg(not(feature = "v2"))]
266        return Ok(self
267            .test_accounts
268            .v1_state_trees
269            .iter()
270            .copied()
271            .map(|tree| tree.into())
272            .collect());
273        #[cfg(feature = "v2")]
274        return Ok(self
275            .test_accounts
276            .v2_state_trees
277            .iter()
278            .map(|tree| (*tree).into())
279            .collect());
280    }
281
282    /// Fetch the latest state tree addresses from the cluster.
283    fn get_state_tree_infos(&self) -> Vec<TreeInfo> {
284        #[cfg(not(feature = "v2"))]
285        return self
286            .test_accounts
287            .v1_state_trees
288            .iter()
289            .copied()
290            .map(|tree| tree.into())
291            .collect();
292        #[cfg(feature = "v2")]
293        return self
294            .test_accounts
295            .v2_state_trees
296            .iter()
297            .map(|tree| (*tree).into())
298            .collect();
299    }
300
301    /// Gets a random active state tree.
302    /// State trees are cached and have to be fetched or set.
303    fn get_random_state_tree_info(&self) -> Result<TreeInfo, RpcError> {
304        use rand::Rng;
305        let mut rng = rand::thread_rng();
306        #[cfg(not(feature = "v2"))]
307        {
308            if self.test_accounts.v1_state_trees.is_empty() {
309                return Err(RpcError::NoStateTreesAvailable);
310            }
311            Ok(self.test_accounts.v1_state_trees
312                [rng.gen_range(0..self.test_accounts.v1_state_trees.len())]
313            .into())
314        }
315        #[cfg(feature = "v2")]
316        {
317            if self.test_accounts.v2_state_trees.is_empty() {
318                return Err(RpcError::NoStateTreesAvailable);
319            }
320            Ok(self.test_accounts.v2_state_trees
321                [rng.gen_range(0..self.test_accounts.v2_state_trees.len())]
322            .into())
323        }
324    }
325
326    /// Gets a random v1 state tree.
327    /// State trees are cached and have to be fetched or set.
328    fn get_random_state_tree_info_v1(&self) -> Result<TreeInfo, RpcError> {
329        use rand::Rng;
330        let mut rng = rand::thread_rng();
331        if self.test_accounts.v1_state_trees.is_empty() {
332            return Err(RpcError::NoStateTreesAvailable);
333        }
334        Ok(self.test_accounts.v1_state_trees
335            [rng.gen_range(0..self.test_accounts.v1_state_trees.len())]
336        .into())
337    }
338
339    fn get_address_tree_v1(&self) -> TreeInfo {
340        TreeInfo {
341            tree: pubkey!("amt1Ayt45jfbdw5YSo7iz6WZxUmnZsQTYXy82hVwyC2"),
342            queue: pubkey!("aq1S9z4reTSQAdgWHGD2zDaS39sjGrAxbR31vxJ2F4F"),
343            cpi_context: None,
344            next_tree_info: None,
345            tree_type: TreeType::AddressV1,
346        }
347    }
348
349    fn get_address_tree_v2(&self) -> TreeInfo {
350        TreeInfo {
351            tree: pubkey!("amt2kaJA14v3urZbZvnc5v2np8jqvc4Z8zDep5wbtzx"),
352            queue: pubkey!("amt2kaJA14v3urZbZvnc5v2np8jqvc4Z8zDep5wbtzx"),
353            cpi_context: None,
354            next_tree_info: None,
355            tree_type: TreeType::AddressV2,
356        }
357    }
358
359    async fn create_and_send_versioned_transaction<'a>(
360        &'a mut self,
361        _instructions: &'a [Instruction],
362        _payer: &'a Pubkey,
363        _signers: &'a [&'a Keypair],
364        _address_lookup_tables: &'a [AddressLookupTableAccount],
365    ) -> Result<Signature, RpcError> {
366        unimplemented!(
367            "create_and_send_versioned_transaction is unimplemented for LightProgramTest"
368        );
369    }
370
371    async fn get_account_interface(
372        &self,
373        address: &Pubkey,
374        _config: Option<light_client::indexer::IndexerRpcConfig>,
375    ) -> Result<Response<Option<AccountInterface>>, RpcError> {
376        let slot = self.context.get_sysvar::<Clock>().slot;
377
378        // Hot: check on-chain first
379        if let Some(account) = self.context.get_account(address) {
380            if account.lamports > 0 {
381                return Ok(Response {
382                    context: Context { slot },
383                    value: Some(AccountInterface::hot(*address, account)),
384                });
385            }
386        }
387
388        // Cold: check TestIndexer by onchain pubkey (mirrors Photon behavior)
389        if let Some(indexer) = self.indexer.as_ref() {
390            // First try: lookup by onchain_pubkey (for accounts with DECOMPRESSED_PDA_DISCRIMINATOR)
391            if let Some(compressed_with_ctx) =
392                indexer.find_compressed_account_by_onchain_pubkey(&address.to_bytes())
393            {
394                let owner: Pubkey = compressed_with_ctx.compressed_account.owner.into();
395                let compressed: CompressedAccount = compressed_with_ctx.clone().try_into().map_err(
396                    |e| {
397                        RpcError::CustomError(format!(
398                            "CompressedAccountWithMerkleContext conversion failed for address {}: {:?}",
399                            address, e
400                        ))
401                    },
402                )?;
403
404                return Ok(Response {
405                    context: Context { slot },
406                    value: Some(AccountInterface::cold(*address, compressed, owner)),
407                });
408            }
409
410            // Second try: lookup by PDA seed (for accounts whose address was derived from this pubkey)
411            if let Some(compressed_with_ctx) =
412                indexer.find_compressed_account_by_pda_seed(&address.to_bytes())
413            {
414                let owner: Pubkey = compressed_with_ctx.compressed_account.owner.into();
415                let compressed: CompressedAccount = compressed_with_ctx.clone().try_into().map_err(
416                    |e| {
417                        RpcError::CustomError(format!(
418                            "CompressedAccountWithMerkleContext conversion failed for PDA seed {}: {:?}",
419                            address, e
420                        ))
421                    },
422                )?;
423
424                return Ok(Response {
425                    context: Context { slot },
426                    value: Some(AccountInterface::cold(*address, compressed, owner)),
427                });
428            }
429        }
430
431        Ok(Response {
432            context: Context { slot },
433            value: None,
434        })
435    }
436
437    async fn get_token_account_interface(
438        &self,
439        address: &Pubkey,
440        _config: Option<light_client::indexer::IndexerRpcConfig>,
441    ) -> Result<Response<Option<TokenAccountInterface>>, RpcError> {
442        use light_sdk::constants::LIGHT_TOKEN_PROGRAM_ID;
443
444        let light_token_program_id: Pubkey = LIGHT_TOKEN_PROGRAM_ID.into();
445        let slot = self.context.get_sysvar::<Clock>().slot;
446
447        // Hot: check on-chain first (must be owned by LIGHT_TOKEN_PROGRAM_ID)
448        if let Some(account) = self.context.get_account(address) {
449            if account.lamports > 0 && account.owner == light_token_program_id {
450                match TokenAccountInterface::hot(*address, account) {
451                    Ok(iface) => {
452                        return Ok(Response {
453                            context: Context { slot },
454                            value: Some(iface),
455                        });
456                    }
457                    Err(_) => {
458                        // Fall through to cold lookup if parsing failed
459                    }
460                }
461            }
462        }
463
464        // Cold: check TestIndexer by onchain_pubkey, PDA seed, or token_data.owner
465        if let Some(indexer) = self.indexer.as_ref() {
466            // First try: lookup by onchain_pubkey (for accounts with DECOMPRESSED_PDA_DISCRIMINATOR)
467            let token_acc = indexer
468                .find_token_account_by_onchain_pubkey(&address.to_bytes())
469                .or_else(|| {
470                    // Second try: lookup by PDA seed (for accounts whose address was derived from this pubkey)
471                    indexer.find_token_account_by_pda_seed(&address.to_bytes())
472                });
473
474            if let Some(token_acc) = token_acc {
475                // Convert to CompressedTokenAccount
476                let compressed_account: CompressedAccount = token_acc
477                    .compressed_account
478                    .clone()
479                    .try_into()
480                    .map_err(|e| RpcError::CustomError(format!("conversion error: {:?}", e)))?;
481
482                let compressed_token = CompressedTokenAccount {
483                    token: token_acc.token_data.clone(),
484                    account: compressed_account,
485                };
486
487                return Ok(Response {
488                    context: Context { slot },
489                    value: Some(TokenAccountInterface::cold(
490                        *address,
491                        compressed_token,
492                        *address, // owner = hot address for program-owned tokens
493                        light_token_program_id,
494                    )),
495                });
496            }
497
498            // Third try: lookup by token_data.owner (for tokens where owner == address)
499            let result = indexer
500                .get_compressed_token_accounts_by_owner(address, None, None)
501                .await
502                .map_err(|e| RpcError::CustomError(format!("indexer error: {}", e)))?;
503
504            let items = result.value.items;
505            if items.len() > 1 {
506                return Err(RpcError::CustomError(format!(
507                    "Ambiguous lookup: found {} compressed token accounts for address {}. \
508                     Use get_compressed_token_accounts_by_owner for multiple accounts.",
509                    items.len(),
510                    address
511                )));
512            }
513
514            if let Some(token_acc) = items.into_iter().next() {
515                let key = token_acc
516                    .account
517                    .address
518                    .map(Pubkey::new_from_array)
519                    .unwrap_or(*address);
520                return Ok(Response {
521                    context: Context { slot },
522                    value: Some(TokenAccountInterface::cold(
523                        key,
524                        token_acc,
525                        *address, // owner = hot address for program-owned tokens
526                        light_token_program_id,
527                    )),
528                });
529            }
530        }
531
532        Ok(Response {
533            context: Context { slot },
534            value: None,
535        })
536    }
537
538    async fn get_associated_token_account_interface(
539        &self,
540        owner: &Pubkey,
541        mint: &Pubkey,
542        _config: Option<light_client::indexer::IndexerRpcConfig>,
543    ) -> Result<Response<Option<TokenAccountInterface>>, RpcError> {
544        use light_client::indexer::GetCompressedTokenAccountsByOwnerOrDelegateOptions;
545        use light_sdk::constants::LIGHT_TOKEN_PROGRAM_ID;
546        use light_token::instruction::derive_token_ata;
547
548        let ata = derive_token_ata(owner, mint);
549        let light_token_program_id: Pubkey = LIGHT_TOKEN_PROGRAM_ID.into();
550        let slot = self.context.get_sysvar::<Clock>().slot;
551
552        // First try: on-chain (hot) lookup
553        // We handle this directly instead of using get_token_account_interface
554        // because we need to control owner_override for ata_bump() to work
555        if let Some(account) = self.context.get_account(&ata) {
556            if account.lamports > 0 && account.owner == light_token_program_id {
557                match TokenAccountInterface::hot(ata, account) {
558                    Ok(iface) => {
559                        return Ok(Response {
560                            context: Context { slot },
561                            value: Some(iface),
562                        });
563                    }
564                    Err(_) => {
565                        // Fall through to cold lookup if parsing failed
566                    }
567                }
568            }
569        }
570
571        // Cold: search compressed tokens by ata_pubkey + mint
572        // In Light Protocol, token_data.owner is the token account pubkey (ATA), not wallet owner
573        // But we need to pass the wallet owner for TokenAccountInterface::cold so ata_bump() works
574        if let Some(indexer) = self.indexer.as_ref() {
575            let options = Some(GetCompressedTokenAccountsByOwnerOrDelegateOptions {
576                mint: Some(*mint),
577                ..Default::default()
578            });
579            let result = indexer
580                .get_compressed_token_accounts_by_owner(&ata, options, None)
581                .await
582                .map_err(|e| RpcError::CustomError(format!("indexer error: {}", e)))?;
583
584            let items = result.value.items;
585            if items.len() > 1 {
586                return Err(RpcError::CustomError(format!(
587                    "Ambiguous lookup: found {} compressed token accounts for ATA {} (owner: {}, mint: {}). \
588                     Use get_compressed_token_accounts_by_owner for multiple accounts.",
589                    items.len(),
590                    ata,
591                    owner,
592                    mint
593                )));
594            }
595
596            if let Some(token_acc) = items.into_iter().next() {
597                return Ok(Response {
598                    context: Context { slot },
599                    value: Some(TokenAccountInterface::cold(
600                        ata, // key = ATA pubkey (derived, so we use it directly)
601                        token_acc,
602                        *owner, // owner_override = wallet owner (for ata_bump() to work)
603                        light_token_program_id,
604                    )),
605                });
606            }
607        }
608
609        Ok(Response {
610            context: Context { slot },
611            value: None,
612        })
613    }
614
615    async fn get_multiple_account_interfaces(
616        &self,
617        addresses: Vec<&Pubkey>,
618        _config: Option<light_client::indexer::IndexerRpcConfig>,
619    ) -> Result<Response<Vec<Option<AccountInterface>>>, RpcError> {
620        let slot = self.context.get_sysvar::<Clock>().slot;
621        let mut results: Vec<Option<AccountInterface>> = vec![None; addresses.len()];
622
623        // Batch fetch on-chain accounts (hot path)
624        let owned_addresses: Vec<Pubkey> = addresses.iter().map(|a| **a).collect();
625        let on_chain_accounts: Vec<Option<Account>> = owned_addresses
626            .iter()
627            .map(|addr| self.context.get_account(addr))
628            .collect();
629
630        // Track which addresses still need cold lookup
631        let mut cold_lookup_indices: Vec<usize> = Vec::new();
632        let mut cold_lookup_pubkeys: Vec<[u8; 32]> = Vec::new();
633
634        for (i, (address, maybe_account)) in addresses
635            .iter()
636            .zip(on_chain_accounts.into_iter())
637            .enumerate()
638        {
639            if let Some(account) = maybe_account {
640                if account.lamports > 0 {
641                    results[i] = Some(AccountInterface::hot(**address, account));
642                    continue;
643                }
644            }
645            // Not found on-chain or has 0 lamports, need cold lookup
646            cold_lookup_indices.push(i);
647            cold_lookup_pubkeys.push(address.to_bytes());
648        }
649
650        // Batch lookup cold accounts from TestIndexer
651        if !cold_lookup_pubkeys.is_empty() {
652            if let Some(indexer) = self.indexer.as_ref() {
653                let cold_results = indexer
654                    .find_multiple_compressed_accounts_by_onchain_pubkeys(&cold_lookup_pubkeys);
655
656                for (lookup_idx, maybe_compressed) in cold_results.into_iter().enumerate() {
657                    let original_idx = cold_lookup_indices[lookup_idx];
658                    if let Some(compressed_with_ctx) = maybe_compressed {
659                        let owner: Pubkey = compressed_with_ctx.compressed_account.owner.into();
660                        let compressed: CompressedAccount =
661                            compressed_with_ctx.clone().try_into().map_err(|e| {
662                                RpcError::CustomError(format!("conversion error: {:?}", e))
663                            })?;
664
665                        results[original_idx] = Some(AccountInterface::cold(
666                            *addresses[original_idx],
667                            compressed,
668                            owner,
669                        ));
670                    }
671                }
672            }
673        }
674
675        Ok(Response {
676            context: Context { slot },
677            value: results,
678        })
679    }
680
681    async fn get_mint_interface(
682        &self,
683        address: &Pubkey,
684        config: Option<light_client::indexer::IndexerRpcConfig>,
685    ) -> Result<Response<Option<MintInterface>>, RpcError> {
686        use borsh::BorshDeserialize;
687        use light_compressed_account::address::derive_address;
688        use light_token_interface::{state::Mint, MINT_ADDRESS_TREE};
689
690        let slot = self.context.get_sysvar::<Clock>().slot;
691        let address_tree = Pubkey::new_from_array(MINT_ADDRESS_TREE);
692        let compressed_address = derive_address(
693            &address.to_bytes(),
694            &address_tree.to_bytes(),
695            &light_token_interface::LIGHT_TOKEN_PROGRAM_ID,
696        );
697
698        // 1. Try hot (on-chain) first
699        if let Some(account) = self.context.get_account(address) {
700            if account.lamports > 0 {
701                return Ok(Response {
702                    context: Context { slot },
703                    value: Some(MintInterface {
704                        mint: *address,
705                        address_tree,
706                        compressed_address,
707                        state: MintState::Hot { account },
708                    }),
709                });
710            }
711        }
712
713        // 2. Fall back to cold (compressed) via indexer
714        let indexer = self
715            .indexer
716            .as_ref()
717            .ok_or_else(|| RpcError::CustomError("Indexer not initialized".to_string()))?;
718
719        let resp = indexer
720            .get_compressed_account(compressed_address, config)
721            .await
722            .map_err(|e| RpcError::CustomError(format!("Indexer error: {e}")))?;
723
724        let value = match resp.value {
725            Some(compressed) => {
726                // Parse mint data from compressed account
727                let mint_data = compressed
728                    .data
729                    .as_ref()
730                    .and_then(|d| {
731                        if d.data.is_empty() {
732                            None
733                        } else {
734                            Mint::try_from_slice(&d.data).ok()
735                        }
736                    })
737                    .ok_or_else(|| {
738                        RpcError::CustomError(
739                            "Missing or invalid mint data in compressed account".into(),
740                        )
741                    })?;
742
743                Some(MintInterface {
744                    mint: *address,
745                    address_tree,
746                    compressed_address,
747                    state: MintState::Cold {
748                        compressed,
749                        mint_data,
750                    },
751                })
752            }
753            None => None,
754        };
755
756        Ok(Response {
757            context: Context { slot },
758            value,
759        })
760    }
761}
762
763impl LightProgramTest {
764    fn maybe_print_logs(&self, logs: impl std::fmt::Display) {
765        // Use enhanced logging if enabled and RUST_BACKTRACE is set
766        if crate::logging::should_use_enhanced_logging(&self.config) {
767            // Enhanced logging will be handled in the transaction processing methods
768            return;
769        }
770
771        // Fallback to basic logging
772        if !self.config.no_logs && cfg!(debug_assertions) && std::env::var("RUST_BACKTRACE").is_ok()
773        {
774            println!("{}", logs);
775        }
776    }
777
778    async fn _send_transaction_with_batched_event(
779        &mut self,
780        transaction: Transaction,
781    ) -> Result<Option<(Vec<BatchPublicTransactionEvent>, Signature, Slot)>, RpcError> {
782        let mut vec = Vec::new();
783
784        let signature = transaction.signatures[0];
785        let transaction_for_logging = transaction.clone(); // Clone for logging
786
787        // Capture lightweight pre-transaction account states for logging
788        let pre_states = crate::logging::capture_account_states(&self.context, &transaction);
789        // Clone context for test assertions (get_pre_transaction_account needs full account data)
790        let pre_context_snapshot = self.context.clone();
791
792        // Simulate the transaction. Currently, in banks-client/server, only
793        // simulations are able to track CPIs. Therefore, simulating is the
794        // only way to retrieve the event.
795        let simulation_result = self.context.simulate_transaction(transaction.clone());
796
797        // Transaction was successful, execute it.
798        self.transaction_counter += 1;
799        let transaction_result = self.context.send_transaction(transaction.clone());
800        let slot = self.context.get_sysvar::<Clock>().slot;
801
802        // Capture post-transaction account states for logging
803        let post_states =
804            crate::logging::capture_account_states(&self.context, &transaction_for_logging);
805
806        // Always try enhanced logging for file output (both success and failure)
807        if crate::logging::should_use_enhanced_logging(&self.config) {
808            crate::logging::log_transaction_enhanced(
809                &self.config,
810                &transaction_for_logging,
811                &transaction_result,
812                &signature,
813                slot,
814                self.transaction_counter,
815                Some(&pre_states),
816                Some(&post_states),
817            );
818        }
819
820        // Handle transaction result after logging
821        let _res = transaction_result.as_ref().map_err(|x| {
822            // Prevent duplicate prints for failing tx.
823            if self.config.log_failed_tx {
824                crate::logging::log_transaction_enhanced_with_console(
825                    &self.config,
826                    &transaction_for_logging,
827                    &transaction_result,
828                    &signature,
829                    slot,
830                    self.transaction_counter,
831                    true, // Enable console output
832                    Some(&pre_states),
833                    Some(&post_states),
834                );
835            }
836            RpcError::TransactionError(x.err.clone())
837        })?;
838
839        // Console logging - if RUST_BACKTRACE is set, print to console too
840        if !self.config.no_logs && std::env::var("RUST_BACKTRACE").is_ok() {
841            if crate::logging::should_use_enhanced_logging(&self.config) {
842                // Print enhanced logs to console
843                crate::logging::log_transaction_enhanced_with_console(
844                    &self.config,
845                    &transaction_for_logging,
846                    &transaction_result,
847                    &signature,
848                    slot,
849                    self.transaction_counter,
850                    true, // Enable console output
851                    Some(&pre_states),
852                    Some(&post_states),
853                );
854
855                // if self.config.log_light_protocol_events {
856                //     if let Some(ref event_data) = event {
857                //         println!("event:\n {:?}", event_data);
858                //     }
859                // }
860            } else {
861                // Fallback to basic log printing
862                self.maybe_print_logs(_res.pretty_logs());
863            }
864        }
865
866        let simulation_result = simulation_result.unwrap();
867        // Try old event deserialization.
868        let event = simulation_result
869            .meta
870            .inner_instructions
871            .iter()
872            .flatten()
873            .find_map(|inner_instruction| {
874                PublicTransactionEvent::try_from_slice(&inner_instruction.instruction.data).ok()
875            });
876        let event = if let Some(event) = event {
877            Some(vec![BatchPublicTransactionEvent {
878                event,
879                ..Default::default()
880            }])
881        } else {
882            // If PublicTransactionEvent wasn't successful deserialize new event.
883            let mut vec_accounts = Vec::<Vec<Pubkey>>::new();
884            let mut program_ids = Vec::new();
885
886            transaction.message.instructions.iter().for_each(|i| {
887                program_ids.push(transaction.message.account_keys[i.program_id_index as usize]);
888                vec.push(i.data.clone());
889                vec_accounts.push(
890                    i.accounts
891                        .iter()
892                        .map(|x| transaction.message.account_keys[*x as usize])
893                        .collect(),
894                );
895            });
896            simulation_result
897                .meta
898                .inner_instructions
899                .iter()
900                .flatten()
901                .find_map(|inner_instruction| {
902                    vec.push(inner_instruction.instruction.data.clone());
903                    program_ids.push(
904                        transaction.message.account_keys
905                            [inner_instruction.instruction.program_id_index as usize],
906                    );
907                    vec_accounts.push(
908                        inner_instruction
909                            .instruction
910                            .accounts
911                            .iter()
912                            .map(|x| transaction.message.account_keys[*x as usize])
913                            .collect(),
914                    );
915                    None::<PublicTransactionEvent>
916                });
917
918            event_from_light_transaction(
919                &program_ids.iter().map(|x| (*x).into()).collect::<Vec<_>>(),
920                vec.as_slice(),
921                vec_accounts
922                    .iter()
923                    .map(|inner_vec| inner_vec.iter().map(|x| (*x).into()).collect())
924                    .collect(),
925            )
926            .or(Ok::<
927                Option<Vec<BatchPublicTransactionEvent>>,
928                ParseIndexerEventError,
929            >(None))?
930        };
931        if self.config.log_light_protocol_events {
932            println!("event:\n {:?}", event);
933        }
934        let event = event.map(|e| (e, signature, slot));
935
936        if let Some(indexer) = self.indexer.as_mut() {
937            if let Some(events) = event.as_ref() {
938                for event in events.0.iter() {
939                    <TestIndexer as TestIndexerExtensions>::add_compressed_accounts_with_token_data(
940                        indexer,
941                        slot,
942                        &event.event,
943                    );
944                }
945            }
946        }
947
948        // Update pre_context only after successful transaction execution
949        self.pre_context = Some(pre_context_snapshot);
950
951        Ok(event)
952    }
953
954    async fn _create_and_send_transaction_with_event<T>(
955        &mut self,
956        instruction: &[Instruction],
957        payer: &Pubkey,
958        signers: &[&Keypair],
959    ) -> Result<Option<(T, Signature, Slot)>, RpcError>
960    where
961        T: BorshDeserialize + Send + Debug,
962    {
963        let transaction = Transaction::new_signed_with_payer(
964            instruction,
965            Some(payer),
966            signers,
967            self.context.latest_blockhash(),
968        );
969
970        let signature = transaction.signatures[0];
971
972        // Cache the current context before transaction execution
973        let pre_context_snapshot = self.context.clone();
974
975        // Simulate the transaction. Currently, in banks-client/server, only
976        // simulations are able to track CPIs. Therefore, simulating is the
977        // only way to retrieve the event.
978        let simulation_result = self
979            .context
980            .simulate_transaction(transaction.clone())
981            .map_err(|x| RpcError::from(x.err))?;
982
983        let event = simulation_result
984            .meta
985            .inner_instructions
986            .iter()
987            .flatten()
988            .find_map(|inner_instruction| {
989                T::try_from_slice(&inner_instruction.instruction.data).ok()
990            });
991        // If transaction was successful, execute it.
992        self.transaction_counter += 1;
993        let _res = self.context.send_transaction(transaction).map_err(|x| {
994            if self.config.log_failed_tx {
995                println!("{}", x.meta.pretty_logs());
996            }
997            RpcError::TransactionError(x.err)
998        })?;
999        self.maybe_print_logs(_res.pretty_logs());
1000
1001        // Update pre_context only after successful transaction execution
1002        self.pre_context = Some(pre_context_snapshot);
1003
1004        let slot = self.get_slot().await?;
1005        let result = event.map(|event| (event, signature, slot));
1006        Ok(result)
1007    }
1008
1009    async fn _create_and_send_transaction_with_batched_event(
1010        &mut self,
1011        instruction: &[Instruction],
1012        payer: &Pubkey,
1013        signers: &[&Keypair],
1014    ) -> Result<Option<(Vec<BatchPublicTransactionEvent>, Signature, Slot)>, RpcError> {
1015        let transaction = Transaction::new_signed_with_payer(
1016            instruction,
1017            Some(payer),
1018            signers,
1019            self.context.latest_blockhash(),
1020        );
1021
1022        self._send_transaction_with_batched_event(transaction).await
1023    }
1024}