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;
33use crate::{
34    indexer::{photon_indexer::PhotonIndexer, Indexer, TreeInfo},
35    rpc::{
36        errors::RpcError,
37        get_light_state_tree_infos::{
38            default_state_tree_lookup_tables, get_light_state_tree_infos,
39        },
40        merkle_tree::MerkleTreeExt,
41        Rpc,
42    },
43};
44
45pub enum RpcUrl {
46    Testnet,
47    Devnet,
48    Localnet,
49    ZKTestnet,
50    Custom(String),
51}
52
53impl Display for RpcUrl {
54    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
55        let str = match self {
56            RpcUrl::Testnet => "https://api.testnet.solana.com".to_string(),
57            RpcUrl::Devnet => "https://api.devnet.solana.com".to_string(),
58            RpcUrl::Localnet => "http://localhost:8899".to_string(),
59            RpcUrl::ZKTestnet => "https://zk-testnet.helius.dev:8899".to_string(),
60            RpcUrl::Custom(url) => url.clone(),
61        };
62        write!(f, "{}", str)
63    }
64}
65
66#[derive(Clone, Debug, Copy)]
67pub struct RetryConfig {
68    pub max_retries: u32,
69    pub retry_delay: Duration,
70    /// Max Light slot timeout in time based on solana slot length and light
71    /// slot length.
72    pub timeout: Duration,
73}
74
75impl Default for RetryConfig {
76    fn default() -> Self {
77        RetryConfig {
78            max_retries: 10,
79            retry_delay: Duration::from_secs(1),
80            timeout: Duration::from_secs(60),
81        }
82    }
83}
84
85#[allow(dead_code)]
86pub struct LightClient {
87    pub client: RpcClient,
88    pub payer: Keypair,
89    pub retry_config: RetryConfig,
90    pub indexer: Option<PhotonIndexer>,
91    pub state_merkle_trees: Vec<TreeInfo>,
92}
93
94impl Debug for LightClient {
95    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
96        write!(f, "LightClient {{ client: {:?} }}", self.client.url())
97    }
98}
99
100impl LightClient {
101    pub async fn new_with_retry(
102        config: LightClientConfig,
103        retry_config: Option<RetryConfig>,
104    ) -> Result<Self, RpcError> {
105        let payer = Keypair::new();
106        let commitment_config = config
107            .commitment_config
108            .unwrap_or(CommitmentConfig::confirmed());
109        let client = RpcClient::new_with_commitment(config.url.to_string(), commitment_config);
110        let retry_config = retry_config.unwrap_or_default();
111
112        let indexer = config
113            .photon_url
114            .map(|path| PhotonIndexer::new(path, config.api_key));
115
116        let mut new = Self {
117            client,
118            payer,
119            retry_config,
120            indexer,
121            state_merkle_trees: Vec::new(),
122        };
123        if config.fetch_active_tree {
124            new.get_latest_active_state_trees().await?;
125        }
126        Ok(new)
127    }
128
129    pub fn add_indexer(&mut self, path: String, api_key: Option<String>) {
130        self.indexer = Some(PhotonIndexer::new(path, api_key));
131    }
132
133    /// Detects the network type based on the RPC URL
134    fn detect_network(&self) -> RpcUrl {
135        let url = self.client.url();
136
137        if url.contains("devnet") {
138            RpcUrl::Devnet
139        } else if url.contains("testnet") {
140            RpcUrl::Testnet
141        } else if url.contains("localhost") || url.contains("127.0.0.1") {
142            RpcUrl::Localnet
143        } else if url.contains("zk-testnet") {
144            RpcUrl::ZKTestnet
145        } else {
146            // Default to mainnet for production URLs and custom URLs
147            RpcUrl::Custom(url.to_string())
148        }
149    }
150
151    async fn retry<F, Fut, T>(&self, operation: F) -> Result<T, RpcError>
152    where
153        F: Fn() -> Fut,
154        Fut: std::future::Future<Output = Result<T, RpcError>>,
155    {
156        let mut attempts = 0;
157        let start_time = Instant::now();
158        loop {
159            match operation().await {
160                Ok(result) => return Ok(result),
161                Err(e) => {
162                    let retry = self.should_retry(&e);
163                    if retry {
164                        attempts += 1;
165                        if attempts >= self.retry_config.max_retries
166                            || start_time.elapsed() >= self.retry_config.timeout
167                        {
168                            return Err(e);
169                        }
170                        warn!(
171                            "Operation failed, retrying in {:?} (attempt {}/{}): {:?}",
172                            self.retry_config.retry_delay,
173                            attempts,
174                            self.retry_config.max_retries,
175                            e
176                        );
177                        sleep(self.retry_config.retry_delay).await;
178                    } else {
179                        return Err(e);
180                    }
181                }
182            }
183        }
184    }
185
186    async fn _create_and_send_transaction_with_batched_event(
187        &mut self,
188        instructions: &[Instruction],
189        payer: &Pubkey,
190        signers: &[&Keypair],
191    ) -> Result<Option<(Vec<BatchPublicTransactionEvent>, Signature, Slot)>, RpcError> {
192        let latest_blockhash = self.client.get_latest_blockhash()?;
193
194        let mut instructions_vec = vec![
195            solana_compute_budget_interface::ComputeBudgetInstruction::set_compute_unit_limit(
196                1_000_000,
197            ),
198        ];
199        instructions_vec.extend_from_slice(instructions);
200
201        let transaction = Transaction::new_signed_with_payer(
202            instructions_vec.as_slice(),
203            Some(payer),
204            signers,
205            latest_blockhash,
206        );
207
208        let (signature, slot) = self
209            .process_transaction_with_context(transaction.clone())
210            .await?;
211
212        let mut vec = Vec::new();
213        let mut vec_accounts = Vec::new();
214        let mut program_ids = Vec::new();
215        instructions_vec.iter().for_each(|x| {
216            program_ids.push(light_compressed_account::Pubkey::new_from_array(
217                x.program_id.to_bytes(),
218            ));
219            vec.push(x.data.clone());
220            vec_accounts.push(
221                x.accounts
222                    .iter()
223                    .map(|x| light_compressed_account::Pubkey::new_from_array(x.pubkey.to_bytes()))
224                    .collect(),
225            );
226        });
227        {
228            let rpc_transaction_config = RpcTransactionConfig {
229                encoding: Some(UiTransactionEncoding::Base64),
230                commitment: Some(self.client.commitment()),
231                ..Default::default()
232            };
233            let transaction = self
234                .client
235                .get_transaction_with_config(&signature, rpc_transaction_config)
236                .map_err(|e| RpcError::CustomError(e.to_string()))?;
237            let decoded_transaction = transaction
238                .transaction
239                .transaction
240                .decode()
241                .clone()
242                .unwrap();
243            let account_keys = decoded_transaction.message.static_account_keys();
244            let meta = transaction.transaction.meta.as_ref().ok_or_else(|| {
245                RpcError::CustomError("Transaction missing metadata information".to_string())
246            })?;
247            if meta.status.is_err() {
248                return Err(RpcError::CustomError(
249                    "Transaction status indicates an error".to_string(),
250                ));
251            }
252
253            let inner_instructions = match &meta.inner_instructions {
254                OptionSerializer::Some(i) => i,
255                OptionSerializer::None => {
256                    return Err(RpcError::CustomError(
257                        "No inner instructions found".to_string(),
258                    ));
259                }
260                OptionSerializer::Skip => {
261                    return Err(RpcError::CustomError(
262                        "No inner instructions found".to_string(),
263                    ));
264                }
265            };
266
267            for ix in inner_instructions.iter() {
268                for ui_instruction in ix.instructions.iter() {
269                    match ui_instruction {
270                        UiInstruction::Compiled(ui_compiled_instruction) => {
271                            let accounts = &ui_compiled_instruction.accounts;
272                            let data = bs58::decode(&ui_compiled_instruction.data)
273                                .into_vec()
274                                .map_err(|_| {
275                                    RpcError::CustomError(
276                                        "Failed to decode instruction data".to_string(),
277                                    )
278                                })?;
279                            vec.push(data);
280                            program_ids.push(light_compressed_account::Pubkey::new_from_array(
281                                account_keys[ui_compiled_instruction.program_id_index as usize]
282                                    .to_bytes(),
283                            ));
284                            vec_accounts.push(
285                                accounts
286                                    .iter()
287                                    .map(|x| {
288                                        light_compressed_account::Pubkey::new_from_array(
289                                            account_keys[(*x) as usize].to_bytes(),
290                                        )
291                                    })
292                                    .collect(),
293                            );
294                        }
295                        UiInstruction::Parsed(_) => {
296                            println!("Parsed instructions are not implemented yet");
297                        }
298                    }
299                }
300            }
301        }
302        let parsed_event =
303            event_from_light_transaction(program_ids.as_slice(), vec.as_slice(), vec_accounts)
304                .map_err(|e| RpcError::CustomError(format!("Failed to parse event: {e:?}")))?;
305        let event = parsed_event.map(|e| (e, signature, slot));
306        Ok(event)
307    }
308
309    async fn _create_and_send_transaction_with_event<T>(
310        &mut self,
311        instructions: &[Instruction],
312        payer: &Pubkey,
313        signers: &[&Keypair],
314    ) -> Result<Option<(T, Signature, u64)>, RpcError>
315    where
316        T: BorshDeserialize + Send + Debug,
317    {
318        let latest_blockhash = self.client.get_latest_blockhash()?;
319
320        let mut instructions_vec = vec![
321            solana_compute_budget_interface::ComputeBudgetInstruction::set_compute_unit_limit(
322                1_000_000,
323            ),
324        ];
325        instructions_vec.extend_from_slice(instructions);
326
327        let transaction = Transaction::new_signed_with_payer(
328            instructions_vec.as_slice(),
329            Some(payer),
330            signers,
331            latest_blockhash,
332        );
333
334        let (signature, slot) = self
335            .process_transaction_with_context(transaction.clone())
336            .await?;
337
338        let mut parsed_event = None;
339        for instruction in &transaction.message.instructions {
340            let ix_data = instruction.data.clone();
341            match T::deserialize(&mut &instruction.data[..]) {
342                Ok(e) => {
343                    parsed_event = Some(e);
344                    break;
345                }
346                Err(e) => {
347                    warn!(
348                        "Failed to parse event: {:?}, type: {:?}, ix data: {:?}",
349                        e,
350                        std::any::type_name::<T>(),
351                        ix_data
352                    );
353                }
354            }
355        }
356
357        if parsed_event.is_none() {
358            parsed_event = self.parse_inner_instructions::<T>(signature).ok();
359        }
360
361        let result = parsed_event.map(|e| (e, signature, slot));
362        Ok(result)
363    }
364}
365
366impl LightClient {
367    #[allow(clippy::result_large_err)]
368    fn parse_inner_instructions<T: BorshDeserialize>(
369        &self,
370        signature: Signature,
371    ) -> Result<T, RpcError> {
372        let rpc_transaction_config = RpcTransactionConfig {
373            encoding: Some(UiTransactionEncoding::Base64),
374            commitment: Some(self.client.commitment()),
375            ..Default::default()
376        };
377        let transaction = self
378            .client
379            .get_transaction_with_config(&signature, rpc_transaction_config)
380            .map_err(|e| RpcError::CustomError(e.to_string()))?;
381        let meta = transaction.transaction.meta.as_ref().ok_or_else(|| {
382            RpcError::CustomError("Transaction missing metadata information".to_string())
383        })?;
384        if meta.status.is_err() {
385            return Err(RpcError::CustomError(
386                "Transaction status indicates an error".to_string(),
387            ));
388        }
389
390        let inner_instructions = match &meta.inner_instructions {
391            OptionSerializer::Some(i) => i,
392            OptionSerializer::None => {
393                return Err(RpcError::CustomError(
394                    "No inner instructions found".to_string(),
395                ));
396            }
397            OptionSerializer::Skip => {
398                return Err(RpcError::CustomError(
399                    "No inner instructions found".to_string(),
400                ));
401            }
402        };
403
404        for ix in inner_instructions.iter() {
405            for ui_instruction in ix.instructions.iter() {
406                match ui_instruction {
407                    UiInstruction::Compiled(ui_compiled_instruction) => {
408                        let data = bs58::decode(&ui_compiled_instruction.data)
409                            .into_vec()
410                            .map_err(|_| {
411                                RpcError::CustomError(
412                                    "Failed to decode instruction data".to_string(),
413                                )
414                            })?;
415
416                        match T::try_from_slice(data.as_slice()) {
417                            Ok(parsed_data) => return Ok(parsed_data),
418                            Err(e) => {
419                                warn!("Failed to parse inner instruction: {:?}", e);
420                            }
421                        }
422                    }
423                    UiInstruction::Parsed(_) => {
424                        println!("Parsed instructions are not implemented yet");
425                    }
426                }
427            }
428        }
429        Err(RpcError::CustomError(
430            "Failed to find any parseable inner instructions".to_string(),
431        ))
432    }
433
434    /// Instantly advances the validator to the given slot using surfpool's
435    /// `surfnet_timeTravel` RPC method. This is much faster than polling
436    /// `get_slot` in a loop and is intended for testing against surfpool.
437    ///
438    /// Returns the `EpochInfo` after the time travel, or an error if the
439    /// RPC call fails (e.g. when not running against surfpool).
440    pub async fn warp_to_slot(&self, slot: Slot) -> Result<serde_json::Value, RpcError> {
441        let url = self.client.url();
442        let body = serde_json::json!({
443            "jsonrpc": "2.0",
444            "id": 1,
445            "method": "surfnet_timeTravel",
446            "params": [{ "absoluteSlot": slot }]
447        });
448        let response = reqwest::Client::new()
449            .post(url)
450            .json(&body)
451            .send()
452            .await
453            .map_err(|e| RpcError::CustomError(format!("warp_to_slot failed: {e}")))?;
454        let result: serde_json::Value = response
455            .json()
456            .await
457            .map_err(|e| RpcError::CustomError(format!("warp_to_slot response error: {e}")))?;
458        Ok(result)
459    }
460}
461
462#[async_trait]
463impl Rpc for LightClient {
464    async fn new(config: LightClientConfig) -> Result<Self, RpcError>
465    where
466        Self: Sized,
467    {
468        Self::new_with_retry(config, None).await
469    }
470
471    fn get_payer(&self) -> &Keypair {
472        &self.payer
473    }
474
475    fn get_url(&self) -> String {
476        self.client.url()
477    }
478
479    async fn health(&self) -> Result<(), RpcError> {
480        self.retry(|| async { self.client.get_health().map_err(RpcError::from) })
481            .await
482    }
483
484    async fn get_program_accounts(
485        &self,
486        program_id: &Pubkey,
487    ) -> Result<Vec<(Pubkey, Account)>, RpcError> {
488        self.retry(|| async {
489            self.client
490                .get_program_accounts(program_id)
491                .map_err(RpcError::from)
492        })
493        .await
494    }
495
496    async fn get_program_accounts_with_discriminator(
497        &self,
498        program_id: &Pubkey,
499        discriminator: &[u8],
500    ) -> Result<Vec<(Pubkey, Account)>, RpcError> {
501        use solana_rpc_client_api::{
502            config::{RpcAccountInfoConfig, RpcProgramAccountsConfig},
503            filter::{Memcmp, RpcFilterType},
504        };
505
506        let discriminator = discriminator.to_vec();
507        self.retry(|| async {
508            let config = RpcProgramAccountsConfig {
509                filters: Some(vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
510                    0,
511                    &discriminator,
512                ))]),
513                account_config: RpcAccountInfoConfig {
514                    encoding: Some(solana_account_decoder_client_types::UiAccountEncoding::Base64),
515                    commitment: Some(self.client.commitment()),
516                    ..Default::default()
517                },
518                ..Default::default()
519            };
520            self.client
521                .get_program_accounts_with_config(program_id, config)
522                .map_err(RpcError::from)
523        })
524        .await
525    }
526
527    async fn process_transaction(
528        &mut self,
529        transaction: Transaction,
530    ) -> Result<Signature, RpcError> {
531        self.retry(|| async {
532            self.client
533                .send_and_confirm_transaction(&transaction)
534                .map_err(RpcError::from)
535        })
536        .await
537    }
538
539    async fn process_transaction_with_context(
540        &mut self,
541        transaction: Transaction,
542    ) -> Result<(Signature, Slot), RpcError> {
543        self.retry(|| async {
544            let signature = self.client.send_and_confirm_transaction(&transaction)?;
545            let sig_info = self.client.get_signature_statuses(&[signature])?;
546            let slot = sig_info
547                .value
548                .first()
549                .and_then(|s| s.as_ref())
550                .map(|s| s.slot)
551                .ok_or_else(|| RpcError::CustomError("Failed to get slot".into()))?;
552            Ok((signature, slot))
553        })
554        .await
555    }
556
557    async fn confirm_transaction(&self, signature: Signature) -> Result<bool, RpcError> {
558        self.retry(|| async {
559            self.client
560                .confirm_transaction(&signature)
561                .map_err(RpcError::from)
562        })
563        .await
564    }
565
566    async fn get_account(&self, address: Pubkey) -> Result<Option<Account>, RpcError> {
567        self.retry(|| async {
568            self.client
569                .get_account_with_commitment(&address, self.client.commitment())
570                .map(|response| response.value)
571                .map_err(RpcError::from)
572        })
573        .await
574    }
575
576    async fn get_multiple_accounts(
577        &self,
578        addresses: &[Pubkey],
579    ) -> Result<Vec<Option<Account>>, RpcError> {
580        self.retry(|| async {
581            self.client
582                .get_multiple_accounts(addresses)
583                .map_err(RpcError::from)
584        })
585        .await
586    }
587
588    async fn get_minimum_balance_for_rent_exemption(
589        &self,
590        data_len: usize,
591    ) -> Result<u64, RpcError> {
592        self.retry(|| async {
593            self.client
594                .get_minimum_balance_for_rent_exemption(data_len)
595                .map_err(RpcError::from)
596        })
597        .await
598    }
599
600    async fn airdrop_lamports(
601        &mut self,
602        to: &Pubkey,
603        lamports: u64,
604    ) -> Result<Signature, RpcError> {
605        self.retry(|| async {
606            let signature = self
607                .client
608                .request_airdrop(to, lamports)
609                .map_err(RpcError::ClientError)?;
610            self.retry(|| async {
611                if self
612                    .client
613                    .confirm_transaction_with_commitment(&signature, self.client.commitment())?
614                    .value
615                {
616                    Ok(())
617                } else {
618                    Err(RpcError::CustomError("Airdrop not confirmed".into()))
619                }
620            })
621            .await?;
622
623            Ok(signature)
624        })
625        .await
626    }
627
628    async fn get_balance(&self, pubkey: &Pubkey) -> Result<u64, RpcError> {
629        self.retry(|| async { self.client.get_balance(pubkey).map_err(RpcError::from) })
630            .await
631    }
632
633    async fn get_latest_blockhash(&mut self) -> Result<(Hash, u64), RpcError> {
634        self.retry(|| async {
635            self.client
636                // Confirmed commitments land more reliably than finalized
637                // https://www.helius.dev/blog/how-to-deal-with-blockhash-errors-on-solana#how-to-deal-with-blockhash-errors
638                .get_latest_blockhash_with_commitment(CommitmentConfig::confirmed())
639                .map_err(RpcError::from)
640        })
641        .await
642    }
643
644    async fn get_slot(&self) -> Result<u64, RpcError> {
645        self.retry(|| async { self.client.get_slot().map_err(RpcError::from) })
646            .await
647    }
648
649    async fn send_transaction(&self, transaction: &Transaction) -> Result<Signature, RpcError> {
650        self.retry(|| async {
651            self.client
652                .send_transaction_with_config(
653                    transaction,
654                    RpcSendTransactionConfig {
655                        skip_preflight: true,
656                        max_retries: Some(self.retry_config.max_retries as usize),
657                        ..Default::default()
658                    },
659                )
660                .map_err(RpcError::from)
661        })
662        .await
663    }
664
665    async fn send_transaction_with_config(
666        &self,
667        transaction: &Transaction,
668        config: RpcSendTransactionConfig,
669    ) -> Result<Signature, RpcError> {
670        self.retry(|| async {
671            self.client
672                .send_transaction_with_config(transaction, config)
673                .map_err(RpcError::from)
674        })
675        .await
676    }
677
678    async fn get_transaction_slot(&self, signature: &Signature) -> Result<u64, RpcError> {
679        self.retry(|| async {
680            Ok(self
681                .client
682                .get_transaction_with_config(
683                    signature,
684                    RpcTransactionConfig {
685                        encoding: Some(UiTransactionEncoding::Base64),
686                        commitment: Some(self.client.commitment()),
687                        ..Default::default()
688                    },
689                )
690                .map_err(RpcError::from)?
691                .slot)
692        })
693        .await
694    }
695
696    async fn get_signature_statuses(
697        &self,
698        signatures: &[Signature],
699    ) -> Result<Vec<Option<TransactionStatus>>, RpcError> {
700        self.client
701            .get_signature_statuses(signatures)
702            .map(|response| response.value)
703            .map_err(RpcError::from)
704    }
705
706    async fn create_and_send_transaction_with_event<T>(
707        &mut self,
708        instructions: &[Instruction],
709        payer: &Pubkey,
710        signers: &[&Keypair],
711    ) -> Result<Option<(T, Signature, u64)>, RpcError>
712    where
713        T: BorshDeserialize + Send + Debug,
714    {
715        self._create_and_send_transaction_with_event::<T>(instructions, payer, signers)
716            .await
717    }
718
719    async fn create_and_send_transaction_with_public_event(
720        &mut self,
721        instructions: &[Instruction],
722        payer: &Pubkey,
723        signers: &[&Keypair],
724    ) -> Result<Option<(PublicTransactionEvent, Signature, Slot)>, RpcError> {
725        let parsed_event = self
726            ._create_and_send_transaction_with_batched_event(instructions, payer, signers)
727            .await?;
728
729        let event = parsed_event.map(|(e, signature, slot)| (e[0].event.clone(), signature, slot));
730        Ok(event)
731    }
732
733    async fn create_and_send_transaction_with_batched_event(
734        &mut self,
735        instructions: &[Instruction],
736        payer: &Pubkey,
737        signers: &[&Keypair],
738    ) -> Result<Option<(Vec<BatchPublicTransactionEvent>, Signature, Slot)>, RpcError> {
739        self._create_and_send_transaction_with_batched_event(instructions, payer, signers)
740            .await
741    }
742
743    /// Creates and sends a versioned transaction with address lookup tables.
744    ///
745    /// `address_lookup_tables` must contain pre-fetched `AddressLookupTableAccount` values
746    /// loaded from the chain. Callers are responsible for resolving these accounts before
747    /// calling this method. Unresolved or missing lookup tables will cause compilation to fail.
748    ///
749    /// Returns `RpcError::CustomError` on message compilation failure,
750    /// `RpcError::SigningError` on signing failure.
751    async fn create_and_send_versioned_transaction<'a>(
752        &'a mut self,
753        instructions: &'a [Instruction],
754        payer: &'a Pubkey,
755        signers: &'a [&'a Keypair],
756        address_lookup_tables: &'a [AddressLookupTableAccount],
757    ) -> Result<Signature, RpcError> {
758        let blockhash = self.get_latest_blockhash().await?.0;
759
760        let message =
761            v0::Message::try_compile(payer, instructions, address_lookup_tables, blockhash)
762                .map_err(|e| {
763                    RpcError::CustomError(format!("Failed to compile v0 message: {}", e))
764                })?;
765
766        let versioned_message = VersionedMessage::V0(message);
767
768        let transaction = VersionedTransaction::try_new(versioned_message, signers)
769            .map_err(|e| RpcError::SigningError(e.to_string()))?;
770
771        self.retry(|| async {
772            self.client
773                .send_and_confirm_transaction(&transaction)
774                .map_err(RpcError::from)
775        })
776        .await
777    }
778
779    fn indexer(&self) -> Result<&impl Indexer, RpcError> {
780        self.indexer.as_ref().ok_or(RpcError::IndexerNotInitialized)
781    }
782
783    fn indexer_mut(&mut self) -> Result<&mut impl Indexer, RpcError> {
784        self.indexer.as_mut().ok_or(RpcError::IndexerNotInitialized)
785    }
786
787    /// Fetch the latest state tree addresses from the cluster.
788    async fn get_latest_active_state_trees(&mut self) -> Result<Vec<TreeInfo>, RpcError> {
789        let network = self.detect_network();
790
791        // Return default test values for localnet
792        if matches!(network, RpcUrl::Localnet) {
793            use light_compressed_account::TreeType;
794            use solana_pubkey::pubkey;
795
796            use crate::indexer::TreeInfo;
797
798            #[cfg(feature = "v2")]
799            let default_trees = vec![
800                TreeInfo {
801                    tree: pubkey!("bmt1LryLZUMmF7ZtqESaw7wifBXLfXHQYoE4GAmrahU"),
802                    queue: pubkey!("oq1na8gojfdUhsfCpyjNt6h4JaDWtHf1yQj4koBWfto"),
803                    cpi_context: Some(pubkey!("cpi15BoVPKgEPw5o8wc2T816GE7b378nMXnhH3Xbq4y")),
804                    next_tree_info: None,
805                    tree_type: TreeType::StateV2,
806                },
807                TreeInfo {
808                    tree: pubkey!("bmt2UxoBxB9xWev4BkLvkGdapsz6sZGkzViPNph7VFi"),
809                    queue: pubkey!("oq2UkeMsJLfXt2QHzim242SUi3nvjJs8Pn7Eac9H9vg"),
810                    cpi_context: Some(pubkey!("cpi2yGapXUR3As5SjnHBAVvmApNiLsbeZpF3euWnW6B")),
811                    next_tree_info: None,
812                    tree_type: TreeType::StateV2,
813                },
814                TreeInfo {
815                    tree: pubkey!("bmt3ccLd4bqSVZVeCJnH1F6C8jNygAhaDfxDwePyyGb"),
816                    queue: pubkey!("oq3AxjekBWgo64gpauB6QtuZNesuv19xrhaC1ZM1THQ"),
817                    cpi_context: Some(pubkey!("cpi3mbwMpSX8FAGMZVP85AwxqCaQMfEk9Em1v8QK9Rf")),
818                    next_tree_info: None,
819                    tree_type: TreeType::StateV2,
820                },
821                TreeInfo {
822                    tree: pubkey!("bmt4d3p1a4YQgk9PeZv5s4DBUmbF5NxqYpk9HGjQsd8"),
823                    queue: pubkey!("oq4ypwvVGzCUMoiKKHWh4S1SgZJ9vCvKpcz6RT6A8dq"),
824                    cpi_context: Some(pubkey!("cpi4yyPDc4bCgHAnsenunGA8Y77j3XEDyjgfyCKgcoc")),
825                    next_tree_info: None,
826                    tree_type: TreeType::StateV2,
827                },
828                TreeInfo {
829                    tree: pubkey!("bmt5yU97jC88YXTuSukYHa8Z5Bi2ZDUtmzfkDTA2mG2"),
830                    queue: pubkey!("oq5oh5ZR3yGomuQgFduNDzjtGvVWfDRGLuDVjv9a96P"),
831                    cpi_context: Some(pubkey!("cpi5ZTjdgYpZ1Xr7B1cMLLUE81oTtJbNNAyKary2nV6")),
832                    next_tree_info: None,
833                    tree_type: TreeType::StateV2,
834                },
835            ];
836
837            #[cfg(not(feature = "v2"))]
838            let default_trees = vec![TreeInfo {
839                tree: pubkey!("smt1NamzXdq4AMqS2fS2F1i5KTYPZRhoHgWx38d8WsT"),
840                queue: pubkey!("nfq1NvQDJ2GEgnS8zt9prAe8rjjpAW1zFkrvZoBR148"),
841                cpi_context: Some(pubkey!("cpi1uHzrEhBG733DoEJNgHCyRS3XmmyVNZx5fonubE4")),
842                next_tree_info: None,
843                tree_type: TreeType::StateV1,
844            }];
845
846            self.state_merkle_trees = default_trees.clone();
847            return Ok(default_trees);
848        }
849
850        let (mainnet_tables, devnet_tables) = default_state_tree_lookup_tables();
851
852        let lookup_tables = match network {
853            RpcUrl::Devnet | RpcUrl::Testnet | RpcUrl::ZKTestnet => &devnet_tables,
854            _ => &mainnet_tables, // Default to mainnet for production and custom URLs
855        };
856
857        let res = get_light_state_tree_infos(
858            self,
859            &lookup_tables[0].state_tree_lookup_table,
860            &lookup_tables[0].nullify_table,
861        )
862        .await?;
863        self.state_merkle_trees = res.clone();
864        Ok(res)
865    }
866
867    /// Fetch the latest state tree addresses from the cluster.
868    fn get_state_tree_infos(&self) -> Vec<TreeInfo> {
869        self.state_merkle_trees.to_vec()
870    }
871
872    /// Gets a random active state tree.
873    /// State trees are cached and have to be fetched or set.
874    /// Returns v1 state trees by default, v2 state trees when v2 feature is enabled.
875    fn get_random_state_tree_info(&self) -> Result<TreeInfo, RpcError> {
876        let mut rng = rand::thread_rng();
877
878        #[cfg(feature = "v2")]
879        let filtered_trees: Vec<TreeInfo> = self
880            .state_merkle_trees
881            .iter()
882            .filter(|tree| tree.tree_type == TreeType::StateV2)
883            .copied()
884            .collect();
885
886        #[cfg(not(feature = "v2"))]
887        let filtered_trees: Vec<TreeInfo> = self
888            .state_merkle_trees
889            .iter()
890            .filter(|tree| tree.tree_type == TreeType::StateV1)
891            .copied()
892            .collect();
893
894        select_state_tree_info(&mut rng, &filtered_trees)
895    }
896
897    /// Gets a random v1 state tree.
898    /// State trees are cached and have to be fetched or set.
899    fn get_random_state_tree_info_v1(&self) -> Result<TreeInfo, RpcError> {
900        let mut rng = rand::thread_rng();
901        let v1_trees: Vec<TreeInfo> = self
902            .state_merkle_trees
903            .iter()
904            .filter(|tree| tree.tree_type == TreeType::StateV1)
905            .copied()
906            .collect();
907        select_state_tree_info(&mut rng, &v1_trees)
908    }
909
910    fn get_address_tree_v1(&self) -> TreeInfo {
911        TreeInfo {
912            tree: pubkey!("amt1Ayt45jfbdw5YSo7iz6WZxUmnZsQTYXy82hVwyC2"),
913            queue: pubkey!("aq1S9z4reTSQAdgWHGD2zDaS39sjGrAxbR31vxJ2F4F"),
914            cpi_context: None,
915            next_tree_info: None,
916            tree_type: TreeType::AddressV1,
917        }
918    }
919
920    fn get_address_tree_v2(&self) -> TreeInfo {
921        TreeInfo {
922            tree: pubkey!("amt2kaJA14v3urZbZvnc5v2np8jqvc4Z8zDep5wbtzx"),
923            queue: pubkey!("amt2kaJA14v3urZbZvnc5v2np8jqvc4Z8zDep5wbtzx"),
924            cpi_context: None,
925            next_tree_info: None,
926            tree_type: TreeType::AddressV2,
927        }
928    }
929}
930
931impl MerkleTreeExt for LightClient {}
932
933/// Selects a random state tree from the provided list.
934///
935/// This function should be used together with `get_state_tree_infos()` to first
936/// retrieve the list of state trees, then select one randomly.
937///
938/// # Arguments
939/// * `rng` - A mutable reference to a random number generator
940/// * `state_trees` - A slice of `TreeInfo` representing state trees
941///
942/// # Returns
943/// A randomly selected `TreeInfo` from the provided list, or an error if the list is empty
944///
945/// # Errors
946/// Returns `RpcError::NoStateTreesAvailable` if the provided slice is empty
947///
948/// # Example
949/// ```ignore
950/// use rand::thread_rng;
951/// let tree_infos = client.get_state_tree_infos();
952/// let mut rng = thread_rng();
953/// let selected_tree = select_state_tree_info(&mut rng, &tree_infos)?;
954/// ```
955pub fn select_state_tree_info<R: rand::Rng>(
956    rng: &mut R,
957    state_trees: &[TreeInfo],
958) -> Result<TreeInfo, RpcError> {
959    if state_trees.is_empty() {
960        return Err(RpcError::NoStateTreesAvailable);
961    }
962
963    Ok(state_trees[rng.gen_range(0..state_trees.len())])
964}