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            // Extract token owner before moving token into CompressedTokenAccount
591            let token_owner = indexer_tai.token.owner;
592            let compressed_token = CompressedTokenAccount {
593                token: indexer_tai.token,
594                account: compressed_account,
595            };
596            Ok(TokenAccountInterface::cold(
597                indexer_tai.account.key,
598                compressed_token,
599                token_owner, // owner_override: use token owner, not account key
600                indexer_tai.account.account.owner,
601            ))
602        }
603    }
604}
605
606#[async_trait]
607impl Rpc for LightClient {
608    async fn new(config: LightClientConfig) -> Result<Self, RpcError>
609    where
610        Self: Sized,
611    {
612        Self::new_with_retry(config, None).await
613    }
614
615    fn get_payer(&self) -> &Keypair {
616        &self.payer
617    }
618
619    fn get_url(&self) -> String {
620        self.client.url()
621    }
622
623    async fn health(&self) -> Result<(), RpcError> {
624        self.retry(|| async { self.client.get_health().map_err(RpcError::from) })
625            .await
626    }
627
628    async fn get_program_accounts(
629        &self,
630        program_id: &Pubkey,
631    ) -> Result<Vec<(Pubkey, Account)>, RpcError> {
632        self.retry(|| async {
633            self.client
634                .get_program_accounts(program_id)
635                .map_err(RpcError::from)
636        })
637        .await
638    }
639
640    async fn get_program_accounts_with_discriminator(
641        &self,
642        program_id: &Pubkey,
643        discriminator: &[u8],
644    ) -> Result<Vec<(Pubkey, Account)>, RpcError> {
645        use solana_rpc_client_api::{
646            config::{RpcAccountInfoConfig, RpcProgramAccountsConfig},
647            filter::{Memcmp, RpcFilterType},
648        };
649
650        let discriminator = discriminator.to_vec();
651        self.retry(|| async {
652            let config = RpcProgramAccountsConfig {
653                filters: Some(vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
654                    0,
655                    &discriminator,
656                ))]),
657                account_config: RpcAccountInfoConfig {
658                    encoding: Some(solana_account_decoder_client_types::UiAccountEncoding::Base64),
659                    commitment: Some(self.client.commitment()),
660                    ..Default::default()
661                },
662                ..Default::default()
663            };
664            self.client
665                .get_program_accounts_with_config(program_id, config)
666                .map_err(RpcError::from)
667        })
668        .await
669    }
670
671    async fn process_transaction(
672        &mut self,
673        transaction: Transaction,
674    ) -> Result<Signature, RpcError> {
675        self.retry(|| async {
676            self.client
677                .send_and_confirm_transaction(&transaction)
678                .map_err(RpcError::from)
679        })
680        .await
681    }
682
683    async fn process_transaction_with_context(
684        &mut self,
685        transaction: Transaction,
686    ) -> Result<(Signature, Slot), RpcError> {
687        self.retry(|| async {
688            let signature = self.client.send_and_confirm_transaction(&transaction)?;
689            let sig_info = self.client.get_signature_statuses(&[signature])?;
690            let slot = sig_info
691                .value
692                .first()
693                .and_then(|s| s.as_ref())
694                .map(|s| s.slot)
695                .ok_or_else(|| RpcError::CustomError("Failed to get slot".into()))?;
696            Ok((signature, slot))
697        })
698        .await
699    }
700
701    async fn confirm_transaction(&self, signature: Signature) -> Result<bool, RpcError> {
702        self.retry(|| async {
703            self.client
704                .confirm_transaction(&signature)
705                .map_err(RpcError::from)
706        })
707        .await
708    }
709
710    async fn get_account(&self, address: Pubkey) -> Result<Option<Account>, RpcError> {
711        self.retry(|| async {
712            self.client
713                .get_account_with_commitment(&address, self.client.commitment())
714                .map(|response| response.value)
715                .map_err(RpcError::from)
716        })
717        .await
718    }
719
720    async fn get_multiple_accounts(
721        &self,
722        addresses: &[Pubkey],
723    ) -> Result<Vec<Option<Account>>, RpcError> {
724        self.retry(|| async {
725            self.client
726                .get_multiple_accounts(addresses)
727                .map_err(RpcError::from)
728        })
729        .await
730    }
731
732    async fn get_minimum_balance_for_rent_exemption(
733        &self,
734        data_len: usize,
735    ) -> Result<u64, RpcError> {
736        self.retry(|| async {
737            self.client
738                .get_minimum_balance_for_rent_exemption(data_len)
739                .map_err(RpcError::from)
740        })
741        .await
742    }
743
744    async fn airdrop_lamports(
745        &mut self,
746        to: &Pubkey,
747        lamports: u64,
748    ) -> Result<Signature, RpcError> {
749        self.retry(|| async {
750            let signature = self
751                .client
752                .request_airdrop(to, lamports)
753                .map_err(RpcError::ClientError)?;
754            self.retry(|| async {
755                if self
756                    .client
757                    .confirm_transaction_with_commitment(&signature, self.client.commitment())?
758                    .value
759                {
760                    Ok(())
761                } else {
762                    Err(RpcError::CustomError("Airdrop not confirmed".into()))
763                }
764            })
765            .await?;
766
767            Ok(signature)
768        })
769        .await
770    }
771
772    async fn get_balance(&self, pubkey: &Pubkey) -> Result<u64, RpcError> {
773        self.retry(|| async { self.client.get_balance(pubkey).map_err(RpcError::from) })
774            .await
775    }
776
777    async fn get_latest_blockhash(&mut self) -> Result<(Hash, u64), RpcError> {
778        self.retry(|| async {
779            self.client
780                // Confirmed commitments land more reliably than finalized
781                // https://www.helius.dev/blog/how-to-deal-with-blockhash-errors-on-solana#how-to-deal-with-blockhash-errors
782                .get_latest_blockhash_with_commitment(CommitmentConfig::confirmed())
783                .map_err(RpcError::from)
784        })
785        .await
786    }
787
788    async fn get_slot(&self) -> Result<u64, RpcError> {
789        self.retry(|| async { self.client.get_slot().map_err(RpcError::from) })
790            .await
791    }
792
793    async fn send_transaction(&self, transaction: &Transaction) -> Result<Signature, RpcError> {
794        self.retry(|| async {
795            self.client
796                .send_transaction_with_config(
797                    transaction,
798                    RpcSendTransactionConfig {
799                        skip_preflight: true,
800                        max_retries: Some(self.retry_config.max_retries as usize),
801                        ..Default::default()
802                    },
803                )
804                .map_err(RpcError::from)
805        })
806        .await
807    }
808
809    async fn send_transaction_with_config(
810        &self,
811        transaction: &Transaction,
812        config: RpcSendTransactionConfig,
813    ) -> Result<Signature, RpcError> {
814        self.retry(|| async {
815            self.client
816                .send_transaction_with_config(transaction, config)
817                .map_err(RpcError::from)
818        })
819        .await
820    }
821
822    async fn get_transaction_slot(&self, signature: &Signature) -> Result<u64, RpcError> {
823        self.retry(|| async {
824            Ok(self
825                .client
826                .get_transaction_with_config(
827                    signature,
828                    RpcTransactionConfig {
829                        encoding: Some(UiTransactionEncoding::Base64),
830                        commitment: Some(self.client.commitment()),
831                        ..Default::default()
832                    },
833                )
834                .map_err(RpcError::from)?
835                .slot)
836        })
837        .await
838    }
839
840    async fn get_signature_statuses(
841        &self,
842        signatures: &[Signature],
843    ) -> Result<Vec<Option<TransactionStatus>>, RpcError> {
844        self.client
845            .get_signature_statuses(signatures)
846            .map(|response| response.value)
847            .map_err(RpcError::from)
848    }
849
850    async fn create_and_send_transaction_with_event<T>(
851        &mut self,
852        instructions: &[Instruction],
853        payer: &Pubkey,
854        signers: &[&Keypair],
855    ) -> Result<Option<(T, Signature, u64)>, RpcError>
856    where
857        T: BorshDeserialize + Send + Debug,
858    {
859        self._create_and_send_transaction_with_event::<T>(instructions, payer, signers)
860            .await
861    }
862
863    async fn create_and_send_transaction_with_public_event(
864        &mut self,
865        instructions: &[Instruction],
866        payer: &Pubkey,
867        signers: &[&Keypair],
868    ) -> Result<Option<(PublicTransactionEvent, Signature, Slot)>, RpcError> {
869        let parsed_event = self
870            ._create_and_send_transaction_with_batched_event(instructions, payer, signers)
871            .await?;
872
873        let event = parsed_event.map(|(e, signature, slot)| (e[0].event.clone(), signature, slot));
874        Ok(event)
875    }
876
877    async fn create_and_send_transaction_with_batched_event(
878        &mut self,
879        instructions: &[Instruction],
880        payer: &Pubkey,
881        signers: &[&Keypair],
882    ) -> Result<Option<(Vec<BatchPublicTransactionEvent>, Signature, Slot)>, RpcError> {
883        self._create_and_send_transaction_with_batched_event(instructions, payer, signers)
884            .await
885    }
886
887    /// Creates and sends a versioned transaction with address lookup tables.
888    ///
889    /// `address_lookup_tables` must contain pre-fetched `AddressLookupTableAccount` values
890    /// loaded from the chain. Callers are responsible for resolving these accounts before
891    /// calling this method. Unresolved or missing lookup tables will cause compilation to fail.
892    ///
893    /// Returns `RpcError::CustomError` on message compilation failure,
894    /// `RpcError::SigningError` on signing failure.
895    async fn create_and_send_versioned_transaction<'a>(
896        &'a mut self,
897        instructions: &'a [Instruction],
898        payer: &'a Pubkey,
899        signers: &'a [&'a Keypair],
900        address_lookup_tables: &'a [AddressLookupTableAccount],
901    ) -> Result<Signature, RpcError> {
902        let blockhash = self.get_latest_blockhash().await?.0;
903
904        let message =
905            v0::Message::try_compile(payer, instructions, address_lookup_tables, blockhash)
906                .map_err(|e| {
907                    RpcError::CustomError(format!("Failed to compile v0 message: {}", e))
908                })?;
909
910        let versioned_message = VersionedMessage::V0(message);
911
912        let transaction = VersionedTransaction::try_new(versioned_message, signers)
913            .map_err(|e| RpcError::SigningError(e.to_string()))?;
914
915        self.retry(|| async {
916            self.client
917                .send_and_confirm_transaction(&transaction)
918                .map_err(RpcError::from)
919        })
920        .await
921    }
922
923    fn indexer(&self) -> Result<&impl Indexer, RpcError> {
924        self.indexer.as_ref().ok_or(RpcError::IndexerNotInitialized)
925    }
926
927    fn indexer_mut(&mut self) -> Result<&mut impl Indexer, RpcError> {
928        self.indexer.as_mut().ok_or(RpcError::IndexerNotInitialized)
929    }
930
931    /// Fetch the latest state tree addresses from the cluster.
932    ///
933    /// When the `v2` feature is enabled, returns the default V2
934    /// batched state trees.
935    /// When `v2` is disabled, uses V1 lookup-table resolution or
936    /// localnet defaults.
937    async fn get_latest_active_state_trees(&mut self) -> Result<Vec<TreeInfo>, RpcError> {
938        // V2: the default batched state trees are the same on every network.
939        #[cfg(feature = "v2")]
940        {
941            let trees = default_v2_state_trees().to_vec();
942            self.state_merkle_trees = trees.clone();
943            return Ok(trees);
944        }
945
946        // V1 path: network-dependent resolution.
947        #[cfg(not(feature = "v2"))]
948        {
949            let network = self.detect_network();
950
951            if matches!(network, RpcUrl::Localnet) {
952                let default_trees = vec![TreeInfo {
953                    tree: pubkey!("smt1NamzXdq4AMqS2fS2F1i5KTYPZRhoHgWx38d8WsT"),
954                    queue: pubkey!("nfq1NvQDJ2GEgnS8zt9prAe8rjjpAW1zFkrvZoBR148"),
955                    cpi_context: Some(pubkey!("cpi1uHzrEhBG733DoEJNgHCyRS3XmmyVNZx5fonubE4")),
956                    next_tree_info: None,
957                    tree_type: TreeType::StateV1,
958                }];
959                self.state_merkle_trees = default_trees.clone();
960                return Ok(default_trees);
961            }
962
963            let (mainnet_tables, devnet_tables) = default_state_tree_lookup_tables();
964
965            let lookup_tables = match network {
966                RpcUrl::Devnet | RpcUrl::Testnet | RpcUrl::ZKTestnet => &devnet_tables,
967                _ => &mainnet_tables,
968            };
969
970            let res = get_light_state_tree_infos(
971                self,
972                &lookup_tables[0].state_tree_lookup_table,
973                &lookup_tables[0].nullify_table,
974            )
975            .await?;
976            self.state_merkle_trees = res.clone();
977            Ok(res)
978        }
979    }
980
981    /// Returns list of state tree infos.
982    fn get_state_tree_infos(&self) -> Vec<TreeInfo> {
983        #[cfg(feature = "v2")]
984        {
985            default_v2_state_trees().to_vec()
986        }
987        #[cfg(not(feature = "v2"))]
988        {
989            self.state_merkle_trees.to_vec()
990        }
991    }
992
993    /// Gets a random active state tree.
994    fn get_random_state_tree_info(&self) -> Result<TreeInfo, RpcError> {
995        #[cfg(feature = "v2")]
996        {
997            use rand::Rng;
998            let mut rng = rand::thread_rng();
999            let trees = default_v2_state_trees();
1000            Ok(trees[rng.gen_range(0..trees.len())])
1001        }
1002
1003        #[cfg(not(feature = "v2"))]
1004        {
1005            let mut rng = rand::thread_rng();
1006            let filtered_trees: Vec<TreeInfo> = self
1007                .state_merkle_trees
1008                .iter()
1009                .filter(|tree| tree.tree_type == TreeType::StateV1)
1010                .copied()
1011                .collect();
1012            select_state_tree_info(&mut rng, &filtered_trees)
1013        }
1014    }
1015
1016    /// Gets a random v1 state tree.
1017    /// State trees are cached and have to be fetched or set.
1018    fn get_random_state_tree_info_v1(&self) -> Result<TreeInfo, RpcError> {
1019        let mut rng = rand::thread_rng();
1020        let v1_trees: Vec<TreeInfo> = self
1021            .state_merkle_trees
1022            .iter()
1023            .filter(|tree| tree.tree_type == TreeType::StateV1)
1024            .copied()
1025            .collect();
1026        select_state_tree_info(&mut rng, &v1_trees)
1027    }
1028
1029    fn get_address_tree_v1(&self) -> TreeInfo {
1030        TreeInfo {
1031            tree: pubkey!("amt1Ayt45jfbdw5YSo7iz6WZxUmnZsQTYXy82hVwyC2"),
1032            queue: pubkey!("aq1S9z4reTSQAdgWHGD2zDaS39sjGrAxbR31vxJ2F4F"),
1033            cpi_context: None,
1034            next_tree_info: None,
1035            tree_type: TreeType::AddressV1,
1036        }
1037    }
1038
1039    fn get_address_tree_v2(&self) -> TreeInfo {
1040        TreeInfo {
1041            tree: pubkey!("amt2kaJA14v3urZbZvnc5v2np8jqvc4Z8zDep5wbtzx"),
1042            queue: pubkey!("amt2kaJA14v3urZbZvnc5v2np8jqvc4Z8zDep5wbtzx"),
1043            cpi_context: None,
1044            next_tree_info: None,
1045            tree_type: TreeType::AddressV2,
1046        }
1047    }
1048
1049    async fn get_account_interface(
1050        &self,
1051        address: &Pubkey,
1052        config: Option<IndexerRpcConfig>,
1053    ) -> Result<Response<Option<AccountInterface>>, RpcError> {
1054        let indexer = self
1055            .indexer
1056            .as_ref()
1057            .ok_or(RpcError::IndexerNotInitialized)?;
1058        let resp = indexer
1059            .get_account_interface(address, config)
1060            .await
1061            .map_err(|e| RpcError::CustomError(format!("Indexer error: {e}")))?;
1062
1063        let value = resp.value.map(convert_account_interface).transpose()?;
1064        Ok(Response {
1065            context: resp.context,
1066            value,
1067        })
1068    }
1069
1070    async fn get_token_account_interface(
1071        &self,
1072        address: &Pubkey,
1073        config: Option<IndexerRpcConfig>,
1074    ) -> Result<Response<Option<TokenAccountInterface>>, RpcError> {
1075        let indexer = self
1076            .indexer
1077            .as_ref()
1078            .ok_or(RpcError::IndexerNotInitialized)?;
1079        let resp = indexer
1080            .get_token_account_interface(address, config)
1081            .await
1082            .map_err(|e| RpcError::CustomError(format!("Indexer error: {e}")))?;
1083
1084        let value = match resp.value {
1085            Some(tai) => Some(convert_token_account_interface(tai)?),
1086            None => None,
1087        };
1088
1089        Ok(Response {
1090            context: resp.context,
1091            value,
1092        })
1093    }
1094
1095    async fn get_associated_token_account_interface(
1096        &self,
1097        owner: &Pubkey,
1098        mint: &Pubkey,
1099        config: Option<IndexerRpcConfig>,
1100    ) -> Result<Response<Option<TokenAccountInterface>>, RpcError> {
1101        let indexer = self
1102            .indexer
1103            .as_ref()
1104            .ok_or(RpcError::IndexerNotInitialized)?;
1105        let resp = indexer
1106            .get_associated_token_account_interface(owner, mint, config)
1107            .await
1108            .map_err(|e| RpcError::CustomError(format!("Indexer error: {e}")))?;
1109
1110        let value = match resp.value {
1111            Some(tai) => Some(convert_token_account_interface(tai)?),
1112            None => None,
1113        };
1114
1115        Ok(Response {
1116            context: resp.context,
1117            value,
1118        })
1119    }
1120
1121    async fn get_multiple_account_interfaces(
1122        &self,
1123        addresses: Vec<&Pubkey>,
1124        config: Option<IndexerRpcConfig>,
1125    ) -> Result<Response<Vec<Option<AccountInterface>>>, RpcError> {
1126        let indexer = self
1127            .indexer
1128            .as_ref()
1129            .ok_or(RpcError::IndexerNotInitialized)?;
1130        let resp = indexer
1131            .get_multiple_account_interfaces(addresses, config)
1132            .await
1133            .map_err(|e| RpcError::CustomError(format!("Indexer error: {e}")))?;
1134
1135        let value: Result<Vec<Option<AccountInterface>>, RpcError> = resp
1136            .value
1137            .into_iter()
1138            .map(|opt| opt.map(convert_account_interface).transpose())
1139            .collect();
1140
1141        Ok(Response {
1142            context: resp.context,
1143            value: value?,
1144        })
1145    }
1146
1147    async fn get_mint_interface(
1148        &self,
1149        address: &Pubkey,
1150        config: Option<IndexerRpcConfig>,
1151    ) -> Result<Response<Option<MintInterface>>, RpcError> {
1152        use light_compressed_account::address::derive_address;
1153        use light_token_interface::{state::Mint, MINT_ADDRESS_TREE};
1154
1155        let address_tree = Pubkey::new_from_array(MINT_ADDRESS_TREE);
1156        let compressed_address = derive_address(
1157            &address.to_bytes(),
1158            &address_tree.to_bytes(),
1159            &light_token_interface::LIGHT_TOKEN_PROGRAM_ID,
1160        );
1161
1162        let indexer = self
1163            .indexer
1164            .as_ref()
1165            .ok_or(RpcError::IndexerNotInitialized)?;
1166
1167        // Use get_account_interface to check hot/cold (Photon handles derived address fallback)
1168        let resp = indexer
1169            .get_account_interface(address, config.clone())
1170            .await
1171            .map_err(|e| RpcError::CustomError(format!("Indexer error: {e}")))?;
1172
1173        let value = match resp.value {
1174            Some(ai) => {
1175                let state = if ai.is_cold() {
1176                    let cold = ai.cold.as_ref().ok_or_else(|| {
1177                        RpcError::CustomError("Cold mint missing cold context".into())
1178                    })?;
1179
1180                    // Build CompressedAccount from indexer ColdContext
1181                    let mut compressed = cold_context_to_compressed_account(
1182                        cold,
1183                        ai.account.lamports,
1184                        ai.account.owner,
1185                    );
1186
1187                    if compressed.address.is_none() {
1188                        compressed.address = Some(compressed_address);
1189                    }
1190
1191                    // Parse mint data from cold data bytes
1192                    let mint_data = if cold.data.data.is_empty() {
1193                        None
1194                    } else {
1195                        Mint::try_from_slice(&cold.data.data).ok()
1196                    }
1197                    .ok_or_else(|| {
1198                        RpcError::CustomError(
1199                            "Missing or invalid mint data in compressed account".into(),
1200                        )
1201                    })?;
1202
1203                    MintState::Cold {
1204                        compressed,
1205                        mint_data,
1206                    }
1207                } else {
1208                    let expected_owner =
1209                        Pubkey::new_from_array(light_token_interface::LIGHT_TOKEN_PROGRAM_ID);
1210                    if ai.account.owner != expected_owner {
1211                        return Err(RpcError::CustomError(format!(
1212                            "Invalid mint account owner: expected {}, got {}",
1213                            expected_owner, ai.account.owner,
1214                        )));
1215                    }
1216                    Mint::try_from_slice(&ai.account.data).map_err(|e| {
1217                        RpcError::CustomError(format!(
1218                            "Failed to deserialize hot mint account: {e}"
1219                        ))
1220                    })?;
1221                    MintState::Hot {
1222                        account: ai.account,
1223                    }
1224                };
1225
1226                Some(MintInterface {
1227                    mint: *address,
1228                    address_tree,
1229                    compressed_address,
1230                    state,
1231                })
1232            }
1233            None => None,
1234        };
1235
1236        Ok(Response {
1237            context: resp.context,
1238            value,
1239        })
1240    }
1241}
1242
1243impl MerkleTreeExt for LightClient {}
1244
1245/// Selects a random state tree from the provided list.
1246///
1247/// This function should be used together with `get_state_tree_infos()` to first
1248/// retrieve the list of state trees, then select one randomly.
1249///
1250/// # Arguments
1251/// * `rng` - A mutable reference to a random number generator
1252/// * `state_trees` - A slice of `TreeInfo` representing state trees
1253///
1254/// # Returns
1255/// A randomly selected `TreeInfo` from the provided list, or an error if the list is empty
1256///
1257/// # Errors
1258/// Returns `RpcError::NoStateTreesAvailable` if the provided slice is empty
1259///
1260/// # Example
1261/// ```ignore
1262/// use rand::thread_rng;
1263/// let tree_infos = client.get_state_tree_infos();
1264/// let mut rng = thread_rng();
1265/// let selected_tree = select_state_tree_info(&mut rng, &tree_infos)?;
1266/// ```
1267pub fn select_state_tree_info<R: rand::Rng>(
1268    rng: &mut R,
1269    state_trees: &[TreeInfo],
1270) -> Result<TreeInfo, RpcError> {
1271    if state_trees.is_empty() {
1272        return Err(RpcError::NoStateTreesAvailable);
1273    }
1274
1275    Ok(state_trees[rng.gen_range(0..state_trees.len())])
1276}