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#[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 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 #[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 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 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
505use crate::indexer::ColdContext as IndexerColdContext;
508
509fn cold_context_to_compressed_account(
510 cold: &IndexerColdContext,
511 lamports: u64,
512 owner: Pubkey,
513) -> crate::indexer::CompressedAccount {
514 use light_compressed_account::compressed_account::CompressedAccountData;
515
516 crate::indexer::CompressedAccount {
517 address: cold.address,
518 data: Some(CompressedAccountData {
519 discriminator: cold.data.discriminator,
520 data: cold.data.data.clone(),
521 data_hash: cold.data.data_hash,
522 }),
523 hash: cold.hash,
524 lamports,
525 leaf_index: cold.leaf_index as u32,
526 owner,
527 prove_by_index: cold.prove_by_index,
528 seq: cold.tree_info.seq,
529 slot_created: cold.tree_info.slot_created,
530 tree_info: TreeInfo {
531 tree: cold.tree_info.tree,
532 queue: cold.tree_info.queue,
533 cpi_context: None,
534 next_tree_info: None,
535 tree_type: cold.tree_info.tree_type,
536 },
537 }
538}
539
540fn convert_account_interface(
541 indexer_ai: IndexerAccountInterface,
542) -> Result<AccountInterface, RpcError> {
543 let account = Account {
544 lamports: indexer_ai.account.lamports,
545 data: indexer_ai.account.data,
546 owner: indexer_ai.account.owner,
547 executable: indexer_ai.account.executable,
548 rent_epoch: indexer_ai.account.rent_epoch,
549 };
550
551 match indexer_ai.cold {
552 None => Ok(AccountInterface::hot(indexer_ai.key, account)),
553 Some(cold) => {
554 let compressed = cold_context_to_compressed_account(
555 &cold,
556 indexer_ai.account.lamports,
557 indexer_ai.account.owner,
558 );
559 Ok(AccountInterface::cold(
560 indexer_ai.key,
561 compressed,
562 indexer_ai.account.owner,
563 ))
564 }
565 }
566}
567
568fn convert_token_account_interface(
569 indexer_tai: IndexerTokenAccountInterface,
570) -> Result<TokenAccountInterface, RpcError> {
571 use crate::indexer::CompressedTokenAccount;
572
573 let account = Account {
574 lamports: indexer_tai.account.account.lamports,
575 data: indexer_tai.account.account.data.clone(),
576 owner: indexer_tai.account.account.owner,
577 executable: indexer_tai.account.account.executable,
578 rent_epoch: indexer_tai.account.account.rent_epoch,
579 };
580
581 match indexer_tai.account.cold {
582 None => TokenAccountInterface::hot(indexer_tai.account.key, account)
583 .map_err(|e| RpcError::CustomError(format!("parse error: {}", e))),
584 Some(cold) => {
585 let compressed_account = cold_context_to_compressed_account(
586 &cold,
587 indexer_tai.account.account.lamports,
588 indexer_tai.account.account.owner,
589 );
590 let token_owner = indexer_tai.token.owner;
591 let compressed_token = CompressedTokenAccount {
592 token: indexer_tai.token,
593 account: compressed_account,
594 };
595 Ok(TokenAccountInterface::cold(
596 indexer_tai.account.key,
597 compressed_token,
598 token_owner,
599 indexer_tai.account.account.owner,
600 ))
601 }
602 }
603}
604
605#[async_trait]
606impl Rpc for LightClient {
607 async fn new(config: LightClientConfig) -> Result<Self, RpcError>
608 where
609 Self: Sized,
610 {
611 Self::new_with_retry(config, None).await
612 }
613
614 fn get_payer(&self) -> &Keypair {
615 &self.payer
616 }
617
618 fn get_url(&self) -> String {
619 self.client.url()
620 }
621
622 async fn health(&self) -> Result<(), RpcError> {
623 self.retry(|| async { self.client.get_health().map_err(RpcError::from) })
624 .await
625 }
626
627 async fn get_program_accounts(
628 &self,
629 program_id: &Pubkey,
630 ) -> Result<Vec<(Pubkey, Account)>, RpcError> {
631 self.retry(|| async {
632 self.client
633 .get_program_accounts(program_id)
634 .map_err(RpcError::from)
635 })
636 .await
637 }
638
639 async fn get_program_accounts_with_discriminator(
640 &self,
641 program_id: &Pubkey,
642 discriminator: &[u8],
643 ) -> Result<Vec<(Pubkey, Account)>, RpcError> {
644 use solana_rpc_client_api::{
645 config::{RpcAccountInfoConfig, RpcProgramAccountsConfig},
646 filter::{Memcmp, RpcFilterType},
647 };
648
649 let discriminator = discriminator.to_vec();
650 self.retry(|| async {
651 let config = RpcProgramAccountsConfig {
652 filters: Some(vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
653 0,
654 &discriminator,
655 ))]),
656 account_config: RpcAccountInfoConfig {
657 encoding: Some(solana_account_decoder_client_types::UiAccountEncoding::Base64),
658 commitment: Some(self.client.commitment()),
659 ..Default::default()
660 },
661 ..Default::default()
662 };
663 self.client
664 .get_program_accounts_with_config(program_id, config)
665 .map_err(RpcError::from)
666 })
667 .await
668 }
669
670 async fn process_transaction(
671 &mut self,
672 transaction: Transaction,
673 ) -> Result<Signature, RpcError> {
674 self.retry(|| async {
675 self.client
676 .send_and_confirm_transaction(&transaction)
677 .map_err(RpcError::from)
678 })
679 .await
680 }
681
682 async fn process_transaction_with_context(
683 &mut self,
684 transaction: Transaction,
685 ) -> Result<(Signature, Slot), RpcError> {
686 self.retry(|| async {
687 let signature = self.client.send_and_confirm_transaction(&transaction)?;
688 let sig_info = self.client.get_signature_statuses(&[signature])?;
689 let slot = sig_info
690 .value
691 .first()
692 .and_then(|s| s.as_ref())
693 .map(|s| s.slot)
694 .ok_or_else(|| RpcError::CustomError("Failed to get slot".into()))?;
695 Ok((signature, slot))
696 })
697 .await
698 }
699
700 async fn confirm_transaction(&self, signature: Signature) -> Result<bool, RpcError> {
701 self.retry(|| async {
702 self.client
703 .confirm_transaction(&signature)
704 .map_err(RpcError::from)
705 })
706 .await
707 }
708
709 async fn get_account(&self, address: Pubkey) -> Result<Option<Account>, RpcError> {
710 self.retry(|| async {
711 self.client
712 .get_account_with_commitment(&address, self.client.commitment())
713 .map(|response| response.value)
714 .map_err(RpcError::from)
715 })
716 .await
717 }
718
719 async fn get_multiple_accounts(
720 &self,
721 addresses: &[Pubkey],
722 ) -> Result<Vec<Option<Account>>, RpcError> {
723 self.retry(|| async {
724 self.client
725 .get_multiple_accounts(addresses)
726 .map_err(RpcError::from)
727 })
728 .await
729 }
730
731 async fn get_minimum_balance_for_rent_exemption(
732 &self,
733 data_len: usize,
734 ) -> Result<u64, RpcError> {
735 self.retry(|| async {
736 self.client
737 .get_minimum_balance_for_rent_exemption(data_len)
738 .map_err(RpcError::from)
739 })
740 .await
741 }
742
743 async fn airdrop_lamports(
744 &mut self,
745 to: &Pubkey,
746 lamports: u64,
747 ) -> Result<Signature, RpcError> {
748 self.retry(|| async {
749 let signature = self
750 .client
751 .request_airdrop(to, lamports)
752 .map_err(RpcError::ClientError)?;
753 self.retry(|| async {
754 if self
755 .client
756 .confirm_transaction_with_commitment(&signature, self.client.commitment())?
757 .value
758 {
759 Ok(())
760 } else {
761 Err(RpcError::CustomError("Airdrop not confirmed".into()))
762 }
763 })
764 .await?;
765
766 Ok(signature)
767 })
768 .await
769 }
770
771 async fn get_balance(&self, pubkey: &Pubkey) -> Result<u64, RpcError> {
772 self.retry(|| async { self.client.get_balance(pubkey).map_err(RpcError::from) })
773 .await
774 }
775
776 async fn get_latest_blockhash(&mut self) -> Result<(Hash, u64), RpcError> {
777 self.retry(|| async {
778 self.client
779 .get_latest_blockhash_with_commitment(CommitmentConfig::confirmed())
782 .map_err(RpcError::from)
783 })
784 .await
785 }
786
787 async fn get_slot(&self) -> Result<u64, RpcError> {
788 self.retry(|| async { self.client.get_slot().map_err(RpcError::from) })
789 .await
790 }
791
792 async fn send_transaction(&self, transaction: &Transaction) -> Result<Signature, RpcError> {
793 self.retry(|| async {
794 self.client
795 .send_transaction_with_config(
796 transaction,
797 RpcSendTransactionConfig {
798 skip_preflight: true,
799 max_retries: Some(self.retry_config.max_retries as usize),
800 ..Default::default()
801 },
802 )
803 .map_err(RpcError::from)
804 })
805 .await
806 }
807
808 async fn send_transaction_with_config(
809 &self,
810 transaction: &Transaction,
811 config: RpcSendTransactionConfig,
812 ) -> Result<Signature, RpcError> {
813 self.retry(|| async {
814 self.client
815 .send_transaction_with_config(transaction, config)
816 .map_err(RpcError::from)
817 })
818 .await
819 }
820
821 async fn get_transaction_slot(&self, signature: &Signature) -> Result<u64, RpcError> {
822 self.retry(|| async {
823 Ok(self
824 .client
825 .get_transaction_with_config(
826 signature,
827 RpcTransactionConfig {
828 encoding: Some(UiTransactionEncoding::Base64),
829 commitment: Some(self.client.commitment()),
830 ..Default::default()
831 },
832 )
833 .map_err(RpcError::from)?
834 .slot)
835 })
836 .await
837 }
838
839 async fn get_signature_statuses(
840 &self,
841 signatures: &[Signature],
842 ) -> Result<Vec<Option<TransactionStatus>>, RpcError> {
843 self.client
844 .get_signature_statuses(signatures)
845 .map(|response| response.value)
846 .map_err(RpcError::from)
847 }
848
849 async fn create_and_send_transaction_with_event<T>(
850 &mut self,
851 instructions: &[Instruction],
852 payer: &Pubkey,
853 signers: &[&Keypair],
854 ) -> Result<Option<(T, Signature, u64)>, RpcError>
855 where
856 T: BorshDeserialize + Send + Debug,
857 {
858 self._create_and_send_transaction_with_event::<T>(instructions, payer, signers)
859 .await
860 }
861
862 async fn create_and_send_transaction_with_public_event(
863 &mut self,
864 instructions: &[Instruction],
865 payer: &Pubkey,
866 signers: &[&Keypair],
867 ) -> Result<Option<(PublicTransactionEvent, Signature, Slot)>, RpcError> {
868 let parsed_event = self
869 ._create_and_send_transaction_with_batched_event(instructions, payer, signers)
870 .await?;
871
872 let event = parsed_event.map(|(e, signature, slot)| (e[0].event.clone(), signature, slot));
873 Ok(event)
874 }
875
876 async fn create_and_send_transaction_with_batched_event(
877 &mut self,
878 instructions: &[Instruction],
879 payer: &Pubkey,
880 signers: &[&Keypair],
881 ) -> Result<Option<(Vec<BatchPublicTransactionEvent>, Signature, Slot)>, RpcError> {
882 self._create_and_send_transaction_with_batched_event(instructions, payer, signers)
883 .await
884 }
885
886 async fn create_and_send_versioned_transaction<'a>(
895 &'a mut self,
896 instructions: &'a [Instruction],
897 payer: &'a Pubkey,
898 signers: &'a [&'a Keypair],
899 address_lookup_tables: &'a [AddressLookupTableAccount],
900 ) -> Result<Signature, RpcError> {
901 let blockhash = self.get_latest_blockhash().await?.0;
902
903 let message =
904 v0::Message::try_compile(payer, instructions, address_lookup_tables, blockhash)
905 .map_err(|e| {
906 RpcError::CustomError(format!("Failed to compile v0 message: {}", e))
907 })?;
908
909 let versioned_message = VersionedMessage::V0(message);
910
911 let transaction = VersionedTransaction::try_new(versioned_message, signers)
912 .map_err(|e| RpcError::SigningError(e.to_string()))?;
913
914 self.retry(|| async {
915 self.client
916 .send_and_confirm_transaction(&transaction)
917 .map_err(RpcError::from)
918 })
919 .await
920 }
921
922 fn indexer(&self) -> Result<&impl Indexer, RpcError> {
923 self.indexer.as_ref().ok_or(RpcError::IndexerNotInitialized)
924 }
925
926 fn indexer_mut(&mut self) -> Result<&mut impl Indexer, RpcError> {
927 self.indexer.as_mut().ok_or(RpcError::IndexerNotInitialized)
928 }
929
930 async fn get_latest_active_state_trees(&mut self) -> Result<Vec<TreeInfo>, RpcError> {
937 #[cfg(feature = "v2")]
939 {
940 let trees = default_v2_state_trees().to_vec();
941 self.state_merkle_trees = trees.clone();
942 return Ok(trees);
943 }
944
945 #[cfg(not(feature = "v2"))]
947 {
948 let network = self.detect_network();
949
950 if matches!(network, RpcUrl::Localnet) {
951 let default_trees = vec![TreeInfo {
952 tree: pubkey!("smt1NamzXdq4AMqS2fS2F1i5KTYPZRhoHgWx38d8WsT"),
953 queue: pubkey!("nfq1NvQDJ2GEgnS8zt9prAe8rjjpAW1zFkrvZoBR148"),
954 cpi_context: Some(pubkey!("cpi1uHzrEhBG733DoEJNgHCyRS3XmmyVNZx5fonubE4")),
955 next_tree_info: None,
956 tree_type: TreeType::StateV1,
957 }];
958 self.state_merkle_trees = default_trees.clone();
959 return Ok(default_trees);
960 }
961
962 let (mainnet_tables, devnet_tables) = default_state_tree_lookup_tables();
963
964 let lookup_tables = match network {
965 RpcUrl::Devnet | RpcUrl::Testnet | RpcUrl::ZKTestnet => &devnet_tables,
966 _ => &mainnet_tables,
967 };
968
969 let res = get_light_state_tree_infos(
970 self,
971 &lookup_tables[0].state_tree_lookup_table,
972 &lookup_tables[0].nullify_table,
973 )
974 .await?;
975 self.state_merkle_trees = res.clone();
976 Ok(res)
977 }
978 }
979
980 fn get_state_tree_infos(&self) -> Vec<TreeInfo> {
982 #[cfg(feature = "v2")]
983 {
984 default_v2_state_trees().to_vec()
985 }
986 #[cfg(not(feature = "v2"))]
987 {
988 self.state_merkle_trees.to_vec()
989 }
990 }
991
992 fn get_random_state_tree_info(&self) -> Result<TreeInfo, RpcError> {
994 #[cfg(feature = "v2")]
995 {
996 use rand::Rng;
997 let mut rng = rand::thread_rng();
998 let trees = default_v2_state_trees();
999 Ok(trees[rng.gen_range(0..trees.len())])
1000 }
1001
1002 #[cfg(not(feature = "v2"))]
1003 {
1004 let mut rng = rand::thread_rng();
1005 let filtered_trees: Vec<TreeInfo> = self
1006 .state_merkle_trees
1007 .iter()
1008 .filter(|tree| tree.tree_type == TreeType::StateV1)
1009 .copied()
1010 .collect();
1011 select_state_tree_info(&mut rng, &filtered_trees)
1012 }
1013 }
1014
1015 fn get_random_state_tree_info_v1(&self) -> Result<TreeInfo, RpcError> {
1018 let mut rng = rand::thread_rng();
1019 let v1_trees: Vec<TreeInfo> = self
1020 .state_merkle_trees
1021 .iter()
1022 .filter(|tree| tree.tree_type == TreeType::StateV1)
1023 .copied()
1024 .collect();
1025 select_state_tree_info(&mut rng, &v1_trees)
1026 }
1027
1028 fn get_address_tree_v1(&self) -> TreeInfo {
1029 TreeInfo {
1030 tree: pubkey!("amt1Ayt45jfbdw5YSo7iz6WZxUmnZsQTYXy82hVwyC2"),
1031 queue: pubkey!("aq1S9z4reTSQAdgWHGD2zDaS39sjGrAxbR31vxJ2F4F"),
1032 cpi_context: None,
1033 next_tree_info: None,
1034 tree_type: TreeType::AddressV1,
1035 }
1036 }
1037
1038 fn get_address_tree_v2(&self) -> TreeInfo {
1039 TreeInfo {
1040 tree: pubkey!("amt2kaJA14v3urZbZvnc5v2np8jqvc4Z8zDep5wbtzx"),
1041 queue: pubkey!("amt2kaJA14v3urZbZvnc5v2np8jqvc4Z8zDep5wbtzx"),
1042 cpi_context: None,
1043 next_tree_info: None,
1044 tree_type: TreeType::AddressV2,
1045 }
1046 }
1047
1048 async fn get_account_interface(
1049 &self,
1050 address: &Pubkey,
1051 config: Option<IndexerRpcConfig>,
1052 ) -> Result<Response<Option<AccountInterface>>, RpcError> {
1053 let indexer = self
1054 .indexer
1055 .as_ref()
1056 .ok_or(RpcError::IndexerNotInitialized)?;
1057 let resp = indexer
1058 .get_account_interface(address, config)
1059 .await
1060 .map_err(|e| RpcError::CustomError(format!("Indexer error: {e}")))?;
1061
1062 let value = resp.value.map(convert_account_interface).transpose()?;
1063 Ok(Response {
1064 context: resp.context,
1065 value,
1066 })
1067 }
1068
1069 async fn get_token_account_interface(
1070 &self,
1071 address: &Pubkey,
1072 config: Option<IndexerRpcConfig>,
1073 ) -> Result<Response<Option<TokenAccountInterface>>, RpcError> {
1074 let indexer = self
1075 .indexer
1076 .as_ref()
1077 .ok_or(RpcError::IndexerNotInitialized)?;
1078 let resp = indexer
1079 .get_token_account_interface(address, config)
1080 .await
1081 .map_err(|e| RpcError::CustomError(format!("Indexer error: {e}")))?;
1082
1083 let value = match resp.value {
1084 Some(tai) => Some(convert_token_account_interface(tai)?),
1085 None => None,
1086 };
1087
1088 Ok(Response {
1089 context: resp.context,
1090 value,
1091 })
1092 }
1093
1094 async fn get_associated_token_account_interface(
1095 &self,
1096 owner: &Pubkey,
1097 mint: &Pubkey,
1098 config: Option<IndexerRpcConfig>,
1099 ) -> Result<Response<Option<TokenAccountInterface>>, RpcError> {
1100 let indexer = self
1101 .indexer
1102 .as_ref()
1103 .ok_or(RpcError::IndexerNotInitialized)?;
1104 let resp = indexer
1105 .get_associated_token_account_interface(owner, mint, config)
1106 .await
1107 .map_err(|e| RpcError::CustomError(format!("Indexer error: {e}")))?;
1108
1109 let value = match resp.value {
1110 Some(tai) => {
1111 let mut iface = convert_token_account_interface(tai)?;
1112 if iface.is_cold() {
1116 iface.parsed.owner = *owner;
1117 }
1118 Some(iface)
1119 }
1120 None => None,
1121 };
1122
1123 Ok(Response {
1124 context: resp.context,
1125 value,
1126 })
1127 }
1128
1129 async fn get_multiple_account_interfaces(
1130 &self,
1131 addresses: Vec<&Pubkey>,
1132 config: Option<IndexerRpcConfig>,
1133 ) -> Result<Response<Vec<Option<AccountInterface>>>, RpcError> {
1134 let indexer = self
1135 .indexer
1136 .as_ref()
1137 .ok_or(RpcError::IndexerNotInitialized)?;
1138 let resp = indexer
1139 .get_multiple_account_interfaces(addresses, config)
1140 .await
1141 .map_err(|e| RpcError::CustomError(format!("Indexer error: {e}")))?;
1142
1143 let value: Result<Vec<Option<AccountInterface>>, RpcError> = resp
1144 .value
1145 .into_iter()
1146 .map(|opt| opt.map(convert_account_interface).transpose())
1147 .collect();
1148
1149 Ok(Response {
1150 context: resp.context,
1151 value: value?,
1152 })
1153 }
1154
1155 async fn get_mint_interface(
1156 &self,
1157 address: &Pubkey,
1158 config: Option<IndexerRpcConfig>,
1159 ) -> Result<Response<Option<MintInterface>>, RpcError> {
1160 use light_compressed_account::address::derive_address;
1161 use light_token_interface::{state::Mint, MINT_ADDRESS_TREE};
1162
1163 let address_tree = Pubkey::new_from_array(MINT_ADDRESS_TREE);
1164 let compressed_address = derive_address(
1165 &address.to_bytes(),
1166 &address_tree.to_bytes(),
1167 &light_token_interface::LIGHT_TOKEN_PROGRAM_ID,
1168 );
1169
1170 let indexer = self
1171 .indexer
1172 .as_ref()
1173 .ok_or(RpcError::IndexerNotInitialized)?;
1174
1175 let resp = indexer
1177 .get_account_interface(address, config.clone())
1178 .await
1179 .map_err(|e| RpcError::CustomError(format!("Indexer error: {e}")))?;
1180
1181 let value = match resp.value {
1182 Some(ai) => {
1183 let state = if ai.is_cold() {
1184 let cold = ai.cold.as_ref().ok_or_else(|| {
1185 RpcError::CustomError("Cold mint missing cold context".into())
1186 })?;
1187
1188 let mut compressed = cold_context_to_compressed_account(
1190 cold,
1191 ai.account.lamports,
1192 ai.account.owner,
1193 );
1194
1195 if compressed.address.is_none() {
1196 compressed.address = Some(compressed_address);
1197 }
1198
1199 let mint_data = if cold.data.data.is_empty() {
1201 None
1202 } else {
1203 Mint::try_from_slice(&cold.data.data).ok()
1204 }
1205 .ok_or_else(|| {
1206 RpcError::CustomError(
1207 "Missing or invalid mint data in compressed account".into(),
1208 )
1209 })?;
1210
1211 MintState::Cold {
1212 compressed,
1213 mint_data,
1214 }
1215 } else {
1216 let expected_owner =
1217 Pubkey::new_from_array(light_token_interface::LIGHT_TOKEN_PROGRAM_ID);
1218 if ai.account.owner != expected_owner {
1219 return Err(RpcError::CustomError(format!(
1220 "Invalid mint account owner: expected {}, got {}",
1221 expected_owner, ai.account.owner,
1222 )));
1223 }
1224 Mint::try_from_slice(&ai.account.data).map_err(|e| {
1225 RpcError::CustomError(format!(
1226 "Failed to deserialize hot mint account: {e}"
1227 ))
1228 })?;
1229 MintState::Hot {
1230 account: ai.account,
1231 }
1232 };
1233
1234 Some(MintInterface {
1235 mint: *address,
1236 address_tree,
1237 compressed_address,
1238 state,
1239 })
1240 }
1241 None => None,
1242 };
1243
1244 Ok(Response {
1245 context: resp.context,
1246 value,
1247 })
1248 }
1249}
1250
1251impl MerkleTreeExt for LightClient {}
1252
1253pub fn select_state_tree_info<R: rand::Rng>(
1276 rng: &mut R,
1277 state_trees: &[TreeInfo],
1278) -> Result<TreeInfo, RpcError> {
1279 if state_trees.is_empty() {
1280 return Err(RpcError::NoStateTreesAvailable);
1281 }
1282
1283 Ok(state_trees[rng.gen_range(0..state_trees.len())])
1284}