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