Skip to main content

light_client/rpc/
client.rs

1use std::{
2    fmt::{Debug, Display, Formatter},
3    time::Duration,
4};
5
6use async_trait::async_trait;
7use borsh::BorshDeserialize;
8use bs58;
9use light_compressed_account::TreeType;
10use light_event::{
11    event::{BatchPublicTransactionEvent, PublicTransactionEvent},
12    parse::event_from_light_transaction,
13};
14use solana_account::Account;
15use solana_clock::Slot;
16use solana_commitment_config::CommitmentConfig;
17use solana_hash::Hash;
18use solana_instruction::Instruction;
19use solana_keypair::Keypair;
20use solana_message::{v0, AddressLookupTableAccount, VersionedMessage};
21use solana_pubkey::{pubkey, Pubkey};
22use solana_rpc_client::rpc_client::RpcClient;
23use solana_rpc_client_api::config::{RpcSendTransactionConfig, RpcTransactionConfig};
24use solana_signature::Signature;
25use solana_transaction::{versioned::VersionedTransaction, Transaction};
26use solana_transaction_status_client_types::{
27    option_serializer::OptionSerializer, TransactionStatus, UiInstruction, UiTransactionEncoding,
28};
29use tokio::time::{sleep, Instant};
30use tracing::warn;
31
32use super::LightClientConfig;
33#[cfg(not(feature = "v2"))]
34use crate::rpc::get_light_state_tree_infos::{
35    default_state_tree_lookup_tables, get_light_state_tree_infos,
36};
37use crate::{
38    indexer::{
39        photon_indexer::PhotonIndexer, AccountInterface as IndexerAccountInterface, Indexer,
40        IndexerRpcConfig, Response, TokenAccountInterface as IndexerTokenAccountInterface,
41        TreeInfo,
42    },
43    interface::{AccountInterface, MintInterface, MintState, TokenAccountInterface},
44    rpc::{errors::RpcError, merkle_tree::MerkleTreeExt, Rpc},
45};
46
47/// V2 batched state trees.
48#[cfg(feature = "v2")]
49pub(crate) fn default_v2_state_trees() -> [TreeInfo; 5] {
50    [
51        TreeInfo {
52            tree: pubkey!("bmt1LryLZUMmF7ZtqESaw7wifBXLfXHQYoE4GAmrahU"),
53            queue: pubkey!("oq1na8gojfdUhsfCpyjNt6h4JaDWtHf1yQj4koBWfto"),
54            cpi_context: Some(pubkey!("cpi15BoVPKgEPw5o8wc2T816GE7b378nMXnhH3Xbq4y")),
55            next_tree_info: None,
56            tree_type: TreeType::StateV2,
57        },
58        TreeInfo {
59            tree: pubkey!("bmt2UxoBxB9xWev4BkLvkGdapsz6sZGkzViPNph7VFi"),
60            queue: pubkey!("oq2UkeMsJLfXt2QHzim242SUi3nvjJs8Pn7Eac9H9vg"),
61            cpi_context: Some(pubkey!("cpi2yGapXUR3As5SjnHBAVvmApNiLsbeZpF3euWnW6B")),
62            next_tree_info: None,
63            tree_type: TreeType::StateV2,
64        },
65        TreeInfo {
66            tree: pubkey!("bmt3ccLd4bqSVZVeCJnH1F6C8jNygAhaDfxDwePyyGb"),
67            queue: pubkey!("oq3AxjekBWgo64gpauB6QtuZNesuv19xrhaC1ZM1THQ"),
68            cpi_context: Some(pubkey!("cpi3mbwMpSX8FAGMZVP85AwxqCaQMfEk9Em1v8QK9Rf")),
69            next_tree_info: None,
70            tree_type: TreeType::StateV2,
71        },
72        TreeInfo {
73            tree: pubkey!("bmt4d3p1a4YQgk9PeZv5s4DBUmbF5NxqYpk9HGjQsd8"),
74            queue: pubkey!("oq4ypwvVGzCUMoiKKHWh4S1SgZJ9vCvKpcz6RT6A8dq"),
75            cpi_context: Some(pubkey!("cpi4yyPDc4bCgHAnsenunGA8Y77j3XEDyjgfyCKgcoc")),
76            next_tree_info: None,
77            tree_type: TreeType::StateV2,
78        },
79        TreeInfo {
80            tree: pubkey!("bmt5yU97jC88YXTuSukYHa8Z5Bi2ZDUtmzfkDTA2mG2"),
81            queue: pubkey!("oq5oh5ZR3yGomuQgFduNDzjtGvVWfDRGLuDVjv9a96P"),
82            cpi_context: Some(pubkey!("cpi5ZTjdgYpZ1Xr7B1cMLLUE81oTtJbNNAyKary2nV6")),
83            next_tree_info: None,
84            tree_type: TreeType::StateV2,
85        },
86    ]
87}
88
89pub enum RpcUrl {
90    Testnet,
91    Devnet,
92    Localnet,
93    ZKTestnet,
94    Custom(String),
95}
96
97impl Display for RpcUrl {
98    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
99        let str = match self {
100            RpcUrl::Testnet => "https://api.testnet.solana.com".to_string(),
101            RpcUrl::Devnet => "https://api.devnet.solana.com".to_string(),
102            RpcUrl::Localnet => "http://localhost:8899".to_string(),
103            RpcUrl::ZKTestnet => "https://zk-testnet.helius.dev:8899".to_string(),
104            RpcUrl::Custom(url) => url.clone(),
105        };
106        write!(f, "{}", str)
107    }
108}
109
110#[derive(Clone, Debug, Copy)]
111pub struct RetryConfig {
112    pub max_retries: u32,
113    pub retry_delay: Duration,
114    /// Max Light slot timeout in time based on solana slot length and light
115    /// slot length.
116    pub timeout: Duration,
117}
118
119impl Default for RetryConfig {
120    fn default() -> Self {
121        RetryConfig {
122            max_retries: 10,
123            retry_delay: Duration::from_secs(1),
124            timeout: Duration::from_secs(60),
125        }
126    }
127}
128
129#[allow(dead_code)]
130pub struct LightClient {
131    pub client: RpcClient,
132    pub payer: Keypair,
133    pub retry_config: RetryConfig,
134    pub indexer: Option<PhotonIndexer>,
135    pub state_merkle_trees: Vec<TreeInfo>,
136}
137
138impl Debug for LightClient {
139    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
140        write!(f, "LightClient {{ client: {:?} }}", self.client.url())
141    }
142}
143
144impl LightClient {
145    pub async fn new_with_retry(
146        config: LightClientConfig,
147        retry_config: Option<RetryConfig>,
148    ) -> Result<Self, RpcError> {
149        let payer = Keypair::new();
150        let commitment_config = config
151            .commitment_config
152            .unwrap_or(CommitmentConfig::confirmed());
153        let client = RpcClient::new_with_commitment(config.url.to_string(), commitment_config);
154        let retry_config = retry_config.unwrap_or_default();
155
156        let indexer = config.photon_url.map(PhotonIndexer::new);
157
158        let mut new = Self {
159            client,
160            payer,
161            retry_config,
162            indexer,
163            state_merkle_trees: Vec::new(),
164        };
165        if config.fetch_active_tree {
166            new.get_latest_active_state_trees().await?;
167        }
168        Ok(new)
169    }
170
171    pub fn add_indexer(&mut self, url: String) {
172        self.indexer = Some(PhotonIndexer::new(url));
173    }
174
175    /// Detects the network type based on the RPC URL. V1 only.
176    #[cfg(not(feature = "v2"))]
177    fn detect_network(&self) -> RpcUrl {
178        let url = self.client.url();
179
180        if url.contains("devnet") {
181            RpcUrl::Devnet
182        } else if url.contains("testnet") {
183            RpcUrl::Testnet
184        } else if url.contains("localhost") || url.contains("127.0.0.1") {
185            RpcUrl::Localnet
186        } else if url.contains("zk-testnet") {
187            RpcUrl::ZKTestnet
188        } else {
189            // Default to mainnet for production URLs and custom URLs
190            RpcUrl::Custom(url.to_string())
191        }
192    }
193
194    async fn retry<F, Fut, T>(&self, operation: F) -> Result<T, RpcError>
195    where
196        F: Fn() -> Fut,
197        Fut: std::future::Future<Output = Result<T, RpcError>>,
198    {
199        let mut attempts = 0;
200        let start_time = Instant::now();
201        loop {
202            match operation().await {
203                Ok(result) => return Ok(result),
204                Err(e) => {
205                    let retry = self.should_retry(&e);
206                    if retry {
207                        attempts += 1;
208                        if attempts >= self.retry_config.max_retries
209                            || start_time.elapsed() >= self.retry_config.timeout
210                        {
211                            return Err(e);
212                        }
213                        warn!(
214                            "Operation failed, retrying in {:?} (attempt {}/{}): {:?}",
215                            self.retry_config.retry_delay,
216                            attempts,
217                            self.retry_config.max_retries,
218                            e
219                        );
220                        sleep(self.retry_config.retry_delay).await;
221                    } else {
222                        return Err(e);
223                    }
224                }
225            }
226        }
227    }
228
229    async fn _create_and_send_transaction_with_batched_event(
230        &mut self,
231        instructions: &[Instruction],
232        payer: &Pubkey,
233        signers: &[&Keypair],
234    ) -> Result<Option<(Vec<BatchPublicTransactionEvent>, Signature, Slot)>, RpcError> {
235        let latest_blockhash = self.client.get_latest_blockhash()?;
236
237        let mut instructions_vec = vec![
238            solana_compute_budget_interface::ComputeBudgetInstruction::set_compute_unit_limit(
239                1_000_000,
240            ),
241        ];
242        instructions_vec.extend_from_slice(instructions);
243
244        let transaction = Transaction::new_signed_with_payer(
245            instructions_vec.as_slice(),
246            Some(payer),
247            signers,
248            latest_blockhash,
249        );
250
251        let (signature, slot) = self
252            .process_transaction_with_context(transaction.clone())
253            .await?;
254
255        let mut vec = Vec::new();
256        let mut vec_accounts = Vec::new();
257        let mut program_ids = Vec::new();
258        instructions_vec.iter().for_each(|x| {
259            program_ids.push(light_compressed_account::Pubkey::new_from_array(
260                x.program_id.to_bytes(),
261            ));
262            vec.push(x.data.clone());
263            vec_accounts.push(
264                x.accounts
265                    .iter()
266                    .map(|x| light_compressed_account::Pubkey::new_from_array(x.pubkey.to_bytes()))
267                    .collect(),
268            );
269        });
270        {
271            let rpc_transaction_config = RpcTransactionConfig {
272                encoding: Some(UiTransactionEncoding::Base64),
273                commitment: Some(self.client.commitment()),
274                ..Default::default()
275            };
276            let transaction = self
277                .client
278                .get_transaction_with_config(&signature, rpc_transaction_config)
279                .map_err(|e| RpcError::CustomError(e.to_string()))?;
280            let decoded_transaction = transaction
281                .transaction
282                .transaction
283                .decode()
284                .clone()
285                .unwrap();
286            let account_keys = decoded_transaction.message.static_account_keys();
287            let meta = transaction.transaction.meta.as_ref().ok_or_else(|| {
288                RpcError::CustomError("Transaction missing metadata information".to_string())
289            })?;
290            if meta.status.is_err() {
291                return Err(RpcError::CustomError(
292                    "Transaction status indicates an error".to_string(),
293                ));
294            }
295
296            let inner_instructions = match &meta.inner_instructions {
297                OptionSerializer::Some(i) => i,
298                OptionSerializer::None => {
299                    return Err(RpcError::CustomError(
300                        "No inner instructions found".to_string(),
301                    ));
302                }
303                OptionSerializer::Skip => {
304                    return Err(RpcError::CustomError(
305                        "No inner instructions found".to_string(),
306                    ));
307                }
308            };
309
310            for ix in inner_instructions.iter() {
311                for ui_instruction in ix.instructions.iter() {
312                    match ui_instruction {
313                        UiInstruction::Compiled(ui_compiled_instruction) => {
314                            let accounts = &ui_compiled_instruction.accounts;
315                            let data = bs58::decode(&ui_compiled_instruction.data)
316                                .into_vec()
317                                .map_err(|_| {
318                                    RpcError::CustomError(
319                                        "Failed to decode instruction data".to_string(),
320                                    )
321                                })?;
322                            vec.push(data);
323                            program_ids.push(light_compressed_account::Pubkey::new_from_array(
324                                account_keys[ui_compiled_instruction.program_id_index as usize]
325                                    .to_bytes(),
326                            ));
327                            vec_accounts.push(
328                                accounts
329                                    .iter()
330                                    .map(|x| {
331                                        light_compressed_account::Pubkey::new_from_array(
332                                            account_keys[(*x) as usize].to_bytes(),
333                                        )
334                                    })
335                                    .collect(),
336                            );
337                        }
338                        UiInstruction::Parsed(_) => {
339                            println!("Parsed instructions are not implemented yet");
340                        }
341                    }
342                }
343            }
344        }
345        let parsed_event =
346            event_from_light_transaction(program_ids.as_slice(), vec.as_slice(), vec_accounts)
347                .map_err(|e| RpcError::CustomError(format!("Failed to parse event: {e:?}")))?;
348        let event = parsed_event.map(|e| (e, signature, slot));
349        Ok(event)
350    }
351
352    async fn _create_and_send_transaction_with_event<T>(
353        &mut self,
354        instructions: &[Instruction],
355        payer: &Pubkey,
356        signers: &[&Keypair],
357    ) -> Result<Option<(T, Signature, u64)>, RpcError>
358    where
359        T: BorshDeserialize + Send + Debug,
360    {
361        let latest_blockhash = self.client.get_latest_blockhash()?;
362
363        let mut instructions_vec = vec![
364            solana_compute_budget_interface::ComputeBudgetInstruction::set_compute_unit_limit(
365                1_000_000,
366            ),
367        ];
368        instructions_vec.extend_from_slice(instructions);
369
370        let transaction = Transaction::new_signed_with_payer(
371            instructions_vec.as_slice(),
372            Some(payer),
373            signers,
374            latest_blockhash,
375        );
376
377        let (signature, slot) = self
378            .process_transaction_with_context(transaction.clone())
379            .await?;
380
381        let mut parsed_event = None;
382        for instruction in &transaction.message.instructions {
383            let ix_data = instruction.data.clone();
384            match T::deserialize(&mut &instruction.data[..]) {
385                Ok(e) => {
386                    parsed_event = Some(e);
387                    break;
388                }
389                Err(e) => {
390                    warn!(
391                        "Failed to parse event: {:?}, type: {:?}, ix data: {:?}",
392                        e,
393                        std::any::type_name::<T>(),
394                        ix_data
395                    );
396                }
397            }
398        }
399
400        if parsed_event.is_none() {
401            parsed_event = self.parse_inner_instructions::<T>(signature).ok();
402        }
403
404        let result = parsed_event.map(|e| (e, signature, slot));
405        Ok(result)
406    }
407}
408
409impl LightClient {
410    #[allow(clippy::result_large_err)]
411    fn parse_inner_instructions<T: BorshDeserialize>(
412        &self,
413        signature: Signature,
414    ) -> Result<T, RpcError> {
415        let rpc_transaction_config = RpcTransactionConfig {
416            encoding: Some(UiTransactionEncoding::Base64),
417            commitment: Some(self.client.commitment()),
418            ..Default::default()
419        };
420        let transaction = self
421            .client
422            .get_transaction_with_config(&signature, rpc_transaction_config)
423            .map_err(|e| RpcError::CustomError(e.to_string()))?;
424        let meta = transaction.transaction.meta.as_ref().ok_or_else(|| {
425            RpcError::CustomError("Transaction missing metadata information".to_string())
426        })?;
427        if meta.status.is_err() {
428            return Err(RpcError::CustomError(
429                "Transaction status indicates an error".to_string(),
430            ));
431        }
432
433        let inner_instructions = match &meta.inner_instructions {
434            OptionSerializer::Some(i) => i,
435            OptionSerializer::None => {
436                return Err(RpcError::CustomError(
437                    "No inner instructions found".to_string(),
438                ));
439            }
440            OptionSerializer::Skip => {
441                return Err(RpcError::CustomError(
442                    "No inner instructions found".to_string(),
443                ));
444            }
445        };
446
447        for ix in inner_instructions.iter() {
448            for ui_instruction in ix.instructions.iter() {
449                match ui_instruction {
450                    UiInstruction::Compiled(ui_compiled_instruction) => {
451                        let data = bs58::decode(&ui_compiled_instruction.data)
452                            .into_vec()
453                            .map_err(|_| {
454                                RpcError::CustomError(
455                                    "Failed to decode instruction data".to_string(),
456                                )
457                            })?;
458
459                        match T::try_from_slice(data.as_slice()) {
460                            Ok(parsed_data) => return Ok(parsed_data),
461                            Err(e) => {
462                                warn!("Failed to parse inner instruction: {:?}", e);
463                            }
464                        }
465                    }
466                    UiInstruction::Parsed(_) => {
467                        println!("Parsed instructions are not implemented yet");
468                    }
469                }
470            }
471        }
472        Err(RpcError::CustomError(
473            "Failed to find any parseable inner instructions".to_string(),
474        ))
475    }
476
477    /// Instantly advances the validator to the given slot using surfpool's
478    /// `surfnet_timeTravel` RPC method. This is much faster than polling
479    /// `get_slot` in a loop and is intended for testing against surfpool.
480    ///
481    /// Returns the `EpochInfo` after the time travel, or an error if the
482    /// RPC call fails (e.g. when not running against surfpool).
483    pub async fn warp_to_slot(&self, slot: Slot) -> Result<serde_json::Value, RpcError> {
484        let url = self.client.url();
485        let body = serde_json::json!({
486            "jsonrpc": "2.0",
487            "id": 1,
488            "method": "surfnet_timeTravel",
489            "params": [{ "absoluteSlot": slot }]
490        });
491        let response = reqwest::Client::new()
492            .post(url)
493            .json(&body)
494            .send()
495            .await
496            .map_err(|e| RpcError::CustomError(format!("warp_to_slot failed: {e}")))?;
497        let result: serde_json::Value = response
498            .json()
499            .await
500            .map_err(|e| RpcError::CustomError(format!("warp_to_slot response error: {e}")))?;
501        Ok(result)
502    }
503}
504
505// Conversion helpers from indexer types to interface types
506
507use crate::indexer::ColdContext as IndexerColdContext;
508
509fn cold_context_to_compressed_account(
510    cold: &IndexerColdContext,
511    lamports: u64,
512    owner: Pubkey,
513) -> crate::indexer::CompressedAccount {
514    use light_compressed_account::compressed_account::CompressedAccountData;
515
516    crate::indexer::CompressedAccount {
517        address: cold.address,
518        data: Some(CompressedAccountData {
519            discriminator: cold.data.discriminator,
520            data: cold.data.data.clone(),
521            data_hash: cold.data.data_hash,
522        }),
523        hash: cold.hash,
524        lamports,
525        leaf_index: cold.leaf_index as u32,
526        owner,
527        prove_by_index: cold.prove_by_index,
528        seq: cold.tree_info.seq,
529        slot_created: cold.tree_info.slot_created,
530        tree_info: TreeInfo {
531            tree: cold.tree_info.tree,
532            queue: cold.tree_info.queue,
533            cpi_context: None,
534            next_tree_info: None,
535            tree_type: cold.tree_info.tree_type,
536        },
537    }
538}
539
540fn convert_account_interface(
541    indexer_ai: IndexerAccountInterface,
542) -> Result<AccountInterface, RpcError> {
543    let account = Account {
544        lamports: indexer_ai.account.lamports,
545        data: indexer_ai.account.data,
546        owner: indexer_ai.account.owner,
547        executable: indexer_ai.account.executable,
548        rent_epoch: indexer_ai.account.rent_epoch,
549    };
550
551    match indexer_ai.cold {
552        None => Ok(AccountInterface::hot(indexer_ai.key, account)),
553        Some(cold) => {
554            let compressed = cold_context_to_compressed_account(
555                &cold,
556                indexer_ai.account.lamports,
557                indexer_ai.account.owner,
558            );
559            Ok(AccountInterface::cold(
560                indexer_ai.key,
561                compressed,
562                indexer_ai.account.owner,
563            ))
564        }
565    }
566}
567
568fn convert_token_account_interface(
569    indexer_tai: IndexerTokenAccountInterface,
570) -> Result<TokenAccountInterface, RpcError> {
571    use crate::indexer::CompressedTokenAccount;
572
573    let account = Account {
574        lamports: indexer_tai.account.account.lamports,
575        data: indexer_tai.account.account.data.clone(),
576        owner: indexer_tai.account.account.owner,
577        executable: indexer_tai.account.account.executable,
578        rent_epoch: indexer_tai.account.account.rent_epoch,
579    };
580
581    match indexer_tai.account.cold {
582        None => TokenAccountInterface::hot(indexer_tai.account.key, account)
583            .map_err(|e| RpcError::CustomError(format!("parse error: {}", e))),
584        Some(cold) => {
585            let compressed_account = cold_context_to_compressed_account(
586                &cold,
587                indexer_tai.account.account.lamports,
588                indexer_tai.account.account.owner,
589            );
590            let token_owner = indexer_tai.token.owner;
591            let compressed_token = CompressedTokenAccount {
592                token: indexer_tai.token,
593                account: compressed_account,
594            };
595            Ok(TokenAccountInterface::cold(
596                indexer_tai.account.key,
597                compressed_token,
598                token_owner,
599                indexer_tai.account.account.owner,
600            ))
601        }
602    }
603}
604
605#[async_trait]
606impl Rpc for LightClient {
607    async fn new(config: LightClientConfig) -> Result<Self, RpcError>
608    where
609        Self: Sized,
610    {
611        Self::new_with_retry(config, None).await
612    }
613
614    fn get_payer(&self) -> &Keypair {
615        &self.payer
616    }
617
618    fn get_url(&self) -> String {
619        self.client.url()
620    }
621
622    async fn health(&self) -> Result<(), RpcError> {
623        self.retry(|| async { self.client.get_health().map_err(RpcError::from) })
624            .await
625    }
626
627    async fn get_program_accounts(
628        &self,
629        program_id: &Pubkey,
630    ) -> Result<Vec<(Pubkey, Account)>, RpcError> {
631        self.retry(|| async {
632            self.client
633                .get_program_accounts(program_id)
634                .map_err(RpcError::from)
635        })
636        .await
637    }
638
639    async fn get_program_accounts_with_discriminator(
640        &self,
641        program_id: &Pubkey,
642        discriminator: &[u8],
643    ) -> Result<Vec<(Pubkey, Account)>, RpcError> {
644        use solana_rpc_client_api::{
645            config::{RpcAccountInfoConfig, RpcProgramAccountsConfig},
646            filter::{Memcmp, RpcFilterType},
647        };
648
649        let discriminator = discriminator.to_vec();
650        self.retry(|| async {
651            let config = RpcProgramAccountsConfig {
652                filters: Some(vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
653                    0,
654                    &discriminator,
655                ))]),
656                account_config: RpcAccountInfoConfig {
657                    encoding: Some(solana_account_decoder_client_types::UiAccountEncoding::Base64),
658                    commitment: Some(self.client.commitment()),
659                    ..Default::default()
660                },
661                ..Default::default()
662            };
663            self.client
664                .get_program_accounts_with_config(program_id, config)
665                .map_err(RpcError::from)
666        })
667        .await
668    }
669
670    async fn process_transaction(
671        &mut self,
672        transaction: Transaction,
673    ) -> Result<Signature, RpcError> {
674        self.retry(|| async {
675            self.client
676                .send_and_confirm_transaction(&transaction)
677                .map_err(RpcError::from)
678        })
679        .await
680    }
681
682    async fn process_transaction_with_context(
683        &mut self,
684        transaction: Transaction,
685    ) -> Result<(Signature, Slot), RpcError> {
686        self.retry(|| async {
687            let signature = self.client.send_and_confirm_transaction(&transaction)?;
688            let sig_info = self.client.get_signature_statuses(&[signature])?;
689            let slot = sig_info
690                .value
691                .first()
692                .and_then(|s| s.as_ref())
693                .map(|s| s.slot)
694                .ok_or_else(|| RpcError::CustomError("Failed to get slot".into()))?;
695            Ok((signature, slot))
696        })
697        .await
698    }
699
700    async fn confirm_transaction(&self, signature: Signature) -> Result<bool, RpcError> {
701        self.retry(|| async {
702            self.client
703                .confirm_transaction(&signature)
704                .map_err(RpcError::from)
705        })
706        .await
707    }
708
709    async fn get_account(&self, address: Pubkey) -> Result<Option<Account>, RpcError> {
710        self.retry(|| async {
711            self.client
712                .get_account_with_commitment(&address, self.client.commitment())
713                .map(|response| response.value)
714                .map_err(RpcError::from)
715        })
716        .await
717    }
718
719    async fn get_multiple_accounts(
720        &self,
721        addresses: &[Pubkey],
722    ) -> Result<Vec<Option<Account>>, RpcError> {
723        self.retry(|| async {
724            self.client
725                .get_multiple_accounts(addresses)
726                .map_err(RpcError::from)
727        })
728        .await
729    }
730
731    async fn get_minimum_balance_for_rent_exemption(
732        &self,
733        data_len: usize,
734    ) -> Result<u64, RpcError> {
735        self.retry(|| async {
736            self.client
737                .get_minimum_balance_for_rent_exemption(data_len)
738                .map_err(RpcError::from)
739        })
740        .await
741    }
742
743    async fn airdrop_lamports(
744        &mut self,
745        to: &Pubkey,
746        lamports: u64,
747    ) -> Result<Signature, RpcError> {
748        self.retry(|| async {
749            let signature = self
750                .client
751                .request_airdrop(to, lamports)
752                .map_err(RpcError::ClientError)?;
753            self.retry(|| async {
754                if self
755                    .client
756                    .confirm_transaction_with_commitment(&signature, self.client.commitment())?
757                    .value
758                {
759                    Ok(())
760                } else {
761                    Err(RpcError::CustomError("Airdrop not confirmed".into()))
762                }
763            })
764            .await?;
765
766            Ok(signature)
767        })
768        .await
769    }
770
771    async fn get_balance(&self, pubkey: &Pubkey) -> Result<u64, RpcError> {
772        self.retry(|| async { self.client.get_balance(pubkey).map_err(RpcError::from) })
773            .await
774    }
775
776    async fn get_latest_blockhash(&mut self) -> Result<(Hash, u64), RpcError> {
777        self.retry(|| async {
778            self.client
779                // Confirmed commitments land more reliably than finalized
780                // https://www.helius.dev/blog/how-to-deal-with-blockhash-errors-on-solana#how-to-deal-with-blockhash-errors
781                .get_latest_blockhash_with_commitment(CommitmentConfig::confirmed())
782                .map_err(RpcError::from)
783        })
784        .await
785    }
786
787    async fn get_slot(&self) -> Result<u64, RpcError> {
788        self.retry(|| async { self.client.get_slot().map_err(RpcError::from) })
789            .await
790    }
791
792    async fn send_transaction(&self, transaction: &Transaction) -> Result<Signature, RpcError> {
793        self.retry(|| async {
794            self.client
795                .send_transaction_with_config(
796                    transaction,
797                    RpcSendTransactionConfig {
798                        skip_preflight: true,
799                        max_retries: Some(self.retry_config.max_retries as usize),
800                        ..Default::default()
801                    },
802                )
803                .map_err(RpcError::from)
804        })
805        .await
806    }
807
808    async fn send_transaction_with_config(
809        &self,
810        transaction: &Transaction,
811        config: RpcSendTransactionConfig,
812    ) -> Result<Signature, RpcError> {
813        self.retry(|| async {
814            self.client
815                .send_transaction_with_config(transaction, config)
816                .map_err(RpcError::from)
817        })
818        .await
819    }
820
821    async fn get_transaction_slot(&self, signature: &Signature) -> Result<u64, RpcError> {
822        self.retry(|| async {
823            Ok(self
824                .client
825                .get_transaction_with_config(
826                    signature,
827                    RpcTransactionConfig {
828                        encoding: Some(UiTransactionEncoding::Base64),
829                        commitment: Some(self.client.commitment()),
830                        ..Default::default()
831                    },
832                )
833                .map_err(RpcError::from)?
834                .slot)
835        })
836        .await
837    }
838
839    async fn get_signature_statuses(
840        &self,
841        signatures: &[Signature],
842    ) -> Result<Vec<Option<TransactionStatus>>, RpcError> {
843        self.client
844            .get_signature_statuses(signatures)
845            .map(|response| response.value)
846            .map_err(RpcError::from)
847    }
848
849    async fn create_and_send_transaction_with_event<T>(
850        &mut self,
851        instructions: &[Instruction],
852        payer: &Pubkey,
853        signers: &[&Keypair],
854    ) -> Result<Option<(T, Signature, u64)>, RpcError>
855    where
856        T: BorshDeserialize + Send + Debug,
857    {
858        self._create_and_send_transaction_with_event::<T>(instructions, payer, signers)
859            .await
860    }
861
862    async fn create_and_send_transaction_with_public_event(
863        &mut self,
864        instructions: &[Instruction],
865        payer: &Pubkey,
866        signers: &[&Keypair],
867    ) -> Result<Option<(PublicTransactionEvent, Signature, Slot)>, RpcError> {
868        let parsed_event = self
869            ._create_and_send_transaction_with_batched_event(instructions, payer, signers)
870            .await?;
871
872        let event = parsed_event.map(|(e, signature, slot)| (e[0].event.clone(), signature, slot));
873        Ok(event)
874    }
875
876    async fn create_and_send_transaction_with_batched_event(
877        &mut self,
878        instructions: &[Instruction],
879        payer: &Pubkey,
880        signers: &[&Keypair],
881    ) -> Result<Option<(Vec<BatchPublicTransactionEvent>, Signature, Slot)>, RpcError> {
882        self._create_and_send_transaction_with_batched_event(instructions, payer, signers)
883            .await
884    }
885
886    /// Creates and sends a versioned transaction with address lookup tables.
887    ///
888    /// `address_lookup_tables` must contain pre-fetched `AddressLookupTableAccount` values
889    /// loaded from the chain. Callers are responsible for resolving these accounts before
890    /// calling this method. Unresolved or missing lookup tables will cause compilation to fail.
891    ///
892    /// Returns `RpcError::CustomError` on message compilation failure,
893    /// `RpcError::SigningError` on signing failure.
894    async fn create_and_send_versioned_transaction<'a>(
895        &'a mut self,
896        instructions: &'a [Instruction],
897        payer: &'a Pubkey,
898        signers: &'a [&'a Keypair],
899        address_lookup_tables: &'a [AddressLookupTableAccount],
900    ) -> Result<Signature, RpcError> {
901        let blockhash = self.get_latest_blockhash().await?.0;
902
903        let message =
904            v0::Message::try_compile(payer, instructions, address_lookup_tables, blockhash)
905                .map_err(|e| {
906                    RpcError::CustomError(format!("Failed to compile v0 message: {}", e))
907                })?;
908
909        let versioned_message = VersionedMessage::V0(message);
910
911        let transaction = VersionedTransaction::try_new(versioned_message, signers)
912            .map_err(|e| RpcError::SigningError(e.to_string()))?;
913
914        self.retry(|| async {
915            self.client
916                .send_and_confirm_transaction(&transaction)
917                .map_err(RpcError::from)
918        })
919        .await
920    }
921
922    fn indexer(&self) -> Result<&impl Indexer, RpcError> {
923        self.indexer.as_ref().ok_or(RpcError::IndexerNotInitialized)
924    }
925
926    fn indexer_mut(&mut self) -> Result<&mut impl Indexer, RpcError> {
927        self.indexer.as_mut().ok_or(RpcError::IndexerNotInitialized)
928    }
929
930    /// Fetch the latest state tree addresses from the cluster.
931    ///
932    /// When the `v2` feature is enabled, returns the default V2
933    /// batched state trees.
934    /// When `v2` is disabled, uses V1 lookup-table resolution or
935    /// localnet defaults.
936    async fn get_latest_active_state_trees(&mut self) -> Result<Vec<TreeInfo>, RpcError> {
937        // V2: the default batched state trees are the same on every network.
938        #[cfg(feature = "v2")]
939        {
940            let trees = default_v2_state_trees().to_vec();
941            self.state_merkle_trees = trees.clone();
942            return Ok(trees);
943        }
944
945        // V1 path: network-dependent resolution.
946        #[cfg(not(feature = "v2"))]
947        {
948            let network = self.detect_network();
949
950            if matches!(network, RpcUrl::Localnet) {
951                let default_trees = vec![TreeInfo {
952                    tree: pubkey!("smt1NamzXdq4AMqS2fS2F1i5KTYPZRhoHgWx38d8WsT"),
953                    queue: pubkey!("nfq1NvQDJ2GEgnS8zt9prAe8rjjpAW1zFkrvZoBR148"),
954                    cpi_context: Some(pubkey!("cpi1uHzrEhBG733DoEJNgHCyRS3XmmyVNZx5fonubE4")),
955                    next_tree_info: None,
956                    tree_type: TreeType::StateV1,
957                }];
958                self.state_merkle_trees = default_trees.clone();
959                return Ok(default_trees);
960            }
961
962            let (mainnet_tables, devnet_tables) = default_state_tree_lookup_tables();
963
964            let lookup_tables = match network {
965                RpcUrl::Devnet | RpcUrl::Testnet | RpcUrl::ZKTestnet => &devnet_tables,
966                _ => &mainnet_tables,
967            };
968
969            let res = get_light_state_tree_infos(
970                self,
971                &lookup_tables[0].state_tree_lookup_table,
972                &lookup_tables[0].nullify_table,
973            )
974            .await?;
975            self.state_merkle_trees = res.clone();
976            Ok(res)
977        }
978    }
979
980    /// Returns list of state tree infos.
981    fn get_state_tree_infos(&self) -> Vec<TreeInfo> {
982        #[cfg(feature = "v2")]
983        {
984            default_v2_state_trees().to_vec()
985        }
986        #[cfg(not(feature = "v2"))]
987        {
988            self.state_merkle_trees.to_vec()
989        }
990    }
991
992    /// Gets a random active state tree.
993    fn get_random_state_tree_info(&self) -> Result<TreeInfo, RpcError> {
994        #[cfg(feature = "v2")]
995        {
996            use rand::Rng;
997            let mut rng = rand::thread_rng();
998            let trees = default_v2_state_trees();
999            Ok(trees[rng.gen_range(0..trees.len())])
1000        }
1001
1002        #[cfg(not(feature = "v2"))]
1003        {
1004            let mut rng = rand::thread_rng();
1005            let filtered_trees: Vec<TreeInfo> = self
1006                .state_merkle_trees
1007                .iter()
1008                .filter(|tree| tree.tree_type == TreeType::StateV1)
1009                .copied()
1010                .collect();
1011            select_state_tree_info(&mut rng, &filtered_trees)
1012        }
1013    }
1014
1015    /// Gets a random v1 state tree.
1016    /// State trees are cached and have to be fetched or set.
1017    fn get_random_state_tree_info_v1(&self) -> Result<TreeInfo, RpcError> {
1018        let mut rng = rand::thread_rng();
1019        let v1_trees: Vec<TreeInfo> = self
1020            .state_merkle_trees
1021            .iter()
1022            .filter(|tree| tree.tree_type == TreeType::StateV1)
1023            .copied()
1024            .collect();
1025        select_state_tree_info(&mut rng, &v1_trees)
1026    }
1027
1028    fn get_address_tree_v1(&self) -> TreeInfo {
1029        TreeInfo {
1030            tree: pubkey!("amt1Ayt45jfbdw5YSo7iz6WZxUmnZsQTYXy82hVwyC2"),
1031            queue: pubkey!("aq1S9z4reTSQAdgWHGD2zDaS39sjGrAxbR31vxJ2F4F"),
1032            cpi_context: None,
1033            next_tree_info: None,
1034            tree_type: TreeType::AddressV1,
1035        }
1036    }
1037
1038    fn get_address_tree_v2(&self) -> TreeInfo {
1039        TreeInfo {
1040            tree: pubkey!("amt2kaJA14v3urZbZvnc5v2np8jqvc4Z8zDep5wbtzx"),
1041            queue: pubkey!("amt2kaJA14v3urZbZvnc5v2np8jqvc4Z8zDep5wbtzx"),
1042            cpi_context: None,
1043            next_tree_info: None,
1044            tree_type: TreeType::AddressV2,
1045        }
1046    }
1047
1048    async fn get_account_interface(
1049        &self,
1050        address: &Pubkey,
1051        config: Option<IndexerRpcConfig>,
1052    ) -> Result<Response<Option<AccountInterface>>, RpcError> {
1053        let indexer = self
1054            .indexer
1055            .as_ref()
1056            .ok_or(RpcError::IndexerNotInitialized)?;
1057        let resp = indexer
1058            .get_account_interface(address, config)
1059            .await
1060            .map_err(|e| RpcError::CustomError(format!("Indexer error: {e}")))?;
1061
1062        let value = resp.value.map(convert_account_interface).transpose()?;
1063        Ok(Response {
1064            context: resp.context,
1065            value,
1066        })
1067    }
1068
1069    async fn get_token_account_interface(
1070        &self,
1071        address: &Pubkey,
1072        config: Option<IndexerRpcConfig>,
1073    ) -> Result<Response<Option<TokenAccountInterface>>, RpcError> {
1074        let indexer = self
1075            .indexer
1076            .as_ref()
1077            .ok_or(RpcError::IndexerNotInitialized)?;
1078        let resp = indexer
1079            .get_token_account_interface(address, config)
1080            .await
1081            .map_err(|e| RpcError::CustomError(format!("Indexer error: {e}")))?;
1082
1083        let value = match resp.value {
1084            Some(tai) => Some(convert_token_account_interface(tai)?),
1085            None => None,
1086        };
1087
1088        Ok(Response {
1089            context: resp.context,
1090            value,
1091        })
1092    }
1093
1094    async fn get_associated_token_account_interface(
1095        &self,
1096        owner: &Pubkey,
1097        mint: &Pubkey,
1098        config: Option<IndexerRpcConfig>,
1099    ) -> Result<Response<Option<TokenAccountInterface>>, RpcError> {
1100        let indexer = self
1101            .indexer
1102            .as_ref()
1103            .ok_or(RpcError::IndexerNotInitialized)?;
1104        let resp = indexer
1105            .get_associated_token_account_interface(owner, mint, config)
1106            .await
1107            .map_err(|e| RpcError::CustomError(format!("Indexer error: {e}")))?;
1108
1109        let value = match resp.value {
1110            Some(tai) => {
1111                let mut iface = convert_token_account_interface(tai)?;
1112                // For cold ATAs, the compressed token stores token.owner =
1113                // ATA pubkey (for hash verification). Override parsed.owner
1114                // with the wallet owner so ata_bump() derivation succeeds.
1115                if iface.is_cold() {
1116                    iface.parsed.owner = *owner;
1117                }
1118                Some(iface)
1119            }
1120            None => None,
1121        };
1122
1123        Ok(Response {
1124            context: resp.context,
1125            value,
1126        })
1127    }
1128
1129    async fn get_multiple_account_interfaces(
1130        &self,
1131        addresses: Vec<&Pubkey>,
1132        config: Option<IndexerRpcConfig>,
1133    ) -> Result<Response<Vec<Option<AccountInterface>>>, RpcError> {
1134        let indexer = self
1135            .indexer
1136            .as_ref()
1137            .ok_or(RpcError::IndexerNotInitialized)?;
1138        let resp = indexer
1139            .get_multiple_account_interfaces(addresses, config)
1140            .await
1141            .map_err(|e| RpcError::CustomError(format!("Indexer error: {e}")))?;
1142
1143        let value: Result<Vec<Option<AccountInterface>>, RpcError> = resp
1144            .value
1145            .into_iter()
1146            .map(|opt| opt.map(convert_account_interface).transpose())
1147            .collect();
1148
1149        Ok(Response {
1150            context: resp.context,
1151            value: value?,
1152        })
1153    }
1154
1155    async fn get_mint_interface(
1156        &self,
1157        address: &Pubkey,
1158        config: Option<IndexerRpcConfig>,
1159    ) -> Result<Response<Option<MintInterface>>, RpcError> {
1160        use light_compressed_account::address::derive_address;
1161        use light_token_interface::{state::Mint, MINT_ADDRESS_TREE};
1162
1163        let address_tree = Pubkey::new_from_array(MINT_ADDRESS_TREE);
1164        let compressed_address = derive_address(
1165            &address.to_bytes(),
1166            &address_tree.to_bytes(),
1167            &light_token_interface::LIGHT_TOKEN_PROGRAM_ID,
1168        );
1169
1170        let indexer = self
1171            .indexer
1172            .as_ref()
1173            .ok_or(RpcError::IndexerNotInitialized)?;
1174
1175        // Use get_account_interface to check hot/cold (Photon handles derived address fallback)
1176        let resp = indexer
1177            .get_account_interface(address, config.clone())
1178            .await
1179            .map_err(|e| RpcError::CustomError(format!("Indexer error: {e}")))?;
1180
1181        let value = match resp.value {
1182            Some(ai) => {
1183                let state = if ai.is_cold() {
1184                    let cold = ai.cold.as_ref().ok_or_else(|| {
1185                        RpcError::CustomError("Cold mint missing cold context".into())
1186                    })?;
1187
1188                    // Build CompressedAccount from indexer ColdContext
1189                    let mut compressed = cold_context_to_compressed_account(
1190                        cold,
1191                        ai.account.lamports,
1192                        ai.account.owner,
1193                    );
1194
1195                    if compressed.address.is_none() {
1196                        compressed.address = Some(compressed_address);
1197                    }
1198
1199                    // Parse mint data from cold data bytes
1200                    let mint_data = if cold.data.data.is_empty() {
1201                        None
1202                    } else {
1203                        Mint::try_from_slice(&cold.data.data).ok()
1204                    }
1205                    .ok_or_else(|| {
1206                        RpcError::CustomError(
1207                            "Missing or invalid mint data in compressed account".into(),
1208                        )
1209                    })?;
1210
1211                    MintState::Cold {
1212                        compressed,
1213                        mint_data,
1214                    }
1215                } else {
1216                    let expected_owner =
1217                        Pubkey::new_from_array(light_token_interface::LIGHT_TOKEN_PROGRAM_ID);
1218                    if ai.account.owner != expected_owner {
1219                        return Err(RpcError::CustomError(format!(
1220                            "Invalid mint account owner: expected {}, got {}",
1221                            expected_owner, ai.account.owner,
1222                        )));
1223                    }
1224                    Mint::try_from_slice(&ai.account.data).map_err(|e| {
1225                        RpcError::CustomError(format!(
1226                            "Failed to deserialize hot mint account: {e}"
1227                        ))
1228                    })?;
1229                    MintState::Hot {
1230                        account: ai.account,
1231                    }
1232                };
1233
1234                Some(MintInterface {
1235                    mint: *address,
1236                    address_tree,
1237                    compressed_address,
1238                    state,
1239                })
1240            }
1241            None => None,
1242        };
1243
1244        Ok(Response {
1245            context: resp.context,
1246            value,
1247        })
1248    }
1249}
1250
1251impl MerkleTreeExt for LightClient {}
1252
1253/// Selects a random state tree from the provided list.
1254///
1255/// This function should be used together with `get_state_tree_infos()` to first
1256/// retrieve the list of state trees, then select one randomly.
1257///
1258/// # Arguments
1259/// * `rng` - A mutable reference to a random number generator
1260/// * `state_trees` - A slice of `TreeInfo` representing state trees
1261///
1262/// # Returns
1263/// A randomly selected `TreeInfo` from the provided list, or an error if the list is empty
1264///
1265/// # Errors
1266/// Returns `RpcError::NoStateTreesAvailable` if the provided slice is empty
1267///
1268/// # Example
1269/// ```ignore
1270/// use rand::thread_rng;
1271/// let tree_infos = client.get_state_tree_infos();
1272/// let mut rng = thread_rng();
1273/// let selected_tree = select_state_tree_info(&mut rng, &tree_infos)?;
1274/// ```
1275pub fn select_state_tree_info<R: rand::Rng>(
1276    rng: &mut R,
1277    state_trees: &[TreeInfo],
1278) -> Result<TreeInfo, RpcError> {
1279    if state_trees.is_empty() {
1280        return Err(RpcError::NoStateTreesAvailable);
1281    }
1282
1283    Ok(state_trees[rng.gen_range(0..state_trees.len())])
1284}