use std::{
fmt::{Debug, Display, Formatter},
time::Duration,
};
use async_trait::async_trait;
use borsh::BorshDeserialize;
use bs58;
use light_compressed_account::TreeType;
use light_event::{
event::{BatchPublicTransactionEvent, PublicTransactionEvent},
parse::event_from_light_transaction,
};
use solana_account::Account;
use solana_clock::Slot;
use solana_commitment_config::CommitmentConfig;
use solana_hash::Hash;
use solana_instruction::Instruction;
use solana_keypair::Keypair;
use solana_message::{v0, AddressLookupTableAccount, VersionedMessage};
use solana_pubkey::{pubkey, Pubkey};
use solana_rpc_client::rpc_client::RpcClient;
use solana_rpc_client_api::config::{RpcSendTransactionConfig, RpcTransactionConfig};
use solana_signature::Signature;
use solana_transaction::{versioned::VersionedTransaction, Transaction};
use solana_transaction_status_client_types::{
option_serializer::OptionSerializer, TransactionStatus, UiInstruction, UiTransactionEncoding,
};
use tokio::time::{sleep, Instant};
use tracing::warn;
use super::LightClientConfig;
#[cfg(not(feature = "v2"))]
use crate::rpc::get_light_state_tree_infos::{
default_state_tree_lookup_tables, get_light_state_tree_infos,
};
use crate::{
indexer::{
photon_indexer::PhotonIndexer, AccountInterface as IndexerAccountInterface, Indexer,
IndexerRpcConfig, Response, TokenAccountInterface as IndexerTokenAccountInterface,
TreeInfo,
},
interface::{AccountInterface, MintInterface, MintState, TokenAccountInterface},
rpc::{errors::RpcError, merkle_tree::MerkleTreeExt, Rpc},
};
#[cfg(feature = "v2")]
pub(crate) fn default_v2_state_trees() -> [TreeInfo; 5] {
[
TreeInfo {
tree: pubkey!("bmt1LryLZUMmF7ZtqESaw7wifBXLfXHQYoE4GAmrahU"),
queue: pubkey!("oq1na8gojfdUhsfCpyjNt6h4JaDWtHf1yQj4koBWfto"),
cpi_context: Some(pubkey!("cpi15BoVPKgEPw5o8wc2T816GE7b378nMXnhH3Xbq4y")),
next_tree_info: None,
tree_type: TreeType::StateV2,
},
TreeInfo {
tree: pubkey!("bmt2UxoBxB9xWev4BkLvkGdapsz6sZGkzViPNph7VFi"),
queue: pubkey!("oq2UkeMsJLfXt2QHzim242SUi3nvjJs8Pn7Eac9H9vg"),
cpi_context: Some(pubkey!("cpi2yGapXUR3As5SjnHBAVvmApNiLsbeZpF3euWnW6B")),
next_tree_info: None,
tree_type: TreeType::StateV2,
},
TreeInfo {
tree: pubkey!("bmt3ccLd4bqSVZVeCJnH1F6C8jNygAhaDfxDwePyyGb"),
queue: pubkey!("oq3AxjekBWgo64gpauB6QtuZNesuv19xrhaC1ZM1THQ"),
cpi_context: Some(pubkey!("cpi3mbwMpSX8FAGMZVP85AwxqCaQMfEk9Em1v8QK9Rf")),
next_tree_info: None,
tree_type: TreeType::StateV2,
},
TreeInfo {
tree: pubkey!("bmt4d3p1a4YQgk9PeZv5s4DBUmbF5NxqYpk9HGjQsd8"),
queue: pubkey!("oq4ypwvVGzCUMoiKKHWh4S1SgZJ9vCvKpcz6RT6A8dq"),
cpi_context: Some(pubkey!("cpi4yyPDc4bCgHAnsenunGA8Y77j3XEDyjgfyCKgcoc")),
next_tree_info: None,
tree_type: TreeType::StateV2,
},
TreeInfo {
tree: pubkey!("bmt5yU97jC88YXTuSukYHa8Z5Bi2ZDUtmzfkDTA2mG2"),
queue: pubkey!("oq5oh5ZR3yGomuQgFduNDzjtGvVWfDRGLuDVjv9a96P"),
cpi_context: Some(pubkey!("cpi5ZTjdgYpZ1Xr7B1cMLLUE81oTtJbNNAyKary2nV6")),
next_tree_info: None,
tree_type: TreeType::StateV2,
},
]
}
pub enum RpcUrl {
Testnet,
Devnet,
Localnet,
ZKTestnet,
Custom(String),
}
impl Display for RpcUrl {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let str = match self {
RpcUrl::Testnet => "https://api.testnet.solana.com".to_string(),
RpcUrl::Devnet => "https://api.devnet.solana.com".to_string(),
RpcUrl::Localnet => "http://localhost:8899".to_string(),
RpcUrl::ZKTestnet => "https://zk-testnet.helius.dev:8899".to_string(),
RpcUrl::Custom(url) => url.clone(),
};
write!(f, "{}", str)
}
}
#[derive(Clone, Debug, Copy)]
pub struct RetryConfig {
pub max_retries: u32,
pub retry_delay: Duration,
pub timeout: Duration,
}
impl Default for RetryConfig {
fn default() -> Self {
RetryConfig {
max_retries: 10,
retry_delay: Duration::from_secs(1),
timeout: Duration::from_secs(60),
}
}
}
#[allow(dead_code)]
pub struct LightClient {
pub client: RpcClient,
pub payer: Keypair,
pub retry_config: RetryConfig,
pub indexer: Option<PhotonIndexer>,
pub state_merkle_trees: Vec<TreeInfo>,
}
impl Debug for LightClient {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "LightClient {{ client: {:?} }}", self.client.url())
}
}
impl LightClient {
pub async fn new_with_retry(
config: LightClientConfig,
retry_config: Option<RetryConfig>,
) -> Result<Self, RpcError> {
let payer = Keypair::new();
let commitment_config = config
.commitment_config
.unwrap_or(CommitmentConfig::confirmed());
let client = RpcClient::new_with_commitment(config.url.to_string(), commitment_config);
let retry_config = retry_config.unwrap_or_default();
let indexer = config.photon_url.map(PhotonIndexer::new);
let mut new = Self {
client,
payer,
retry_config,
indexer,
state_merkle_trees: Vec::new(),
};
if config.fetch_active_tree {
new.get_latest_active_state_trees().await?;
}
Ok(new)
}
pub fn add_indexer(&mut self, url: String) {
self.indexer = Some(PhotonIndexer::new(url));
}
#[cfg(not(feature = "v2"))]
fn detect_network(&self) -> RpcUrl {
let url = self.client.url();
if url.contains("devnet") {
RpcUrl::Devnet
} else if url.contains("testnet") {
RpcUrl::Testnet
} else if url.contains("localhost") || url.contains("127.0.0.1") {
RpcUrl::Localnet
} else if url.contains("zk-testnet") {
RpcUrl::ZKTestnet
} else {
RpcUrl::Custom(url.to_string())
}
}
async fn retry<F, Fut, T>(&self, operation: F) -> Result<T, RpcError>
where
F: Fn() -> Fut,
Fut: std::future::Future<Output = Result<T, RpcError>>,
{
let mut attempts = 0;
let start_time = Instant::now();
loop {
match operation().await {
Ok(result) => return Ok(result),
Err(e) => {
let retry = self.should_retry(&e);
if retry {
attempts += 1;
if attempts >= self.retry_config.max_retries
|| start_time.elapsed() >= self.retry_config.timeout
{
return Err(e);
}
warn!(
"Operation failed, retrying in {:?} (attempt {}/{}): {:?}",
self.retry_config.retry_delay,
attempts,
self.retry_config.max_retries,
e
);
sleep(self.retry_config.retry_delay).await;
} else {
return Err(e);
}
}
}
}
}
async fn _create_and_send_transaction_with_batched_event(
&mut self,
instructions: &[Instruction],
payer: &Pubkey,
signers: &[&Keypair],
) -> Result<Option<(Vec<BatchPublicTransactionEvent>, Signature, Slot)>, RpcError> {
let latest_blockhash = self.client.get_latest_blockhash()?;
let mut instructions_vec = vec![
solana_compute_budget_interface::ComputeBudgetInstruction::set_compute_unit_limit(
1_000_000,
),
];
instructions_vec.extend_from_slice(instructions);
let transaction = Transaction::new_signed_with_payer(
instructions_vec.as_slice(),
Some(payer),
signers,
latest_blockhash,
);
let (signature, slot) = self
.process_transaction_with_context(transaction.clone())
.await?;
let mut vec = Vec::new();
let mut vec_accounts = Vec::new();
let mut program_ids = Vec::new();
instructions_vec.iter().for_each(|x| {
program_ids.push(light_compressed_account::Pubkey::new_from_array(
x.program_id.to_bytes(),
));
vec.push(x.data.clone());
vec_accounts.push(
x.accounts
.iter()
.map(|x| light_compressed_account::Pubkey::new_from_array(x.pubkey.to_bytes()))
.collect(),
);
});
{
let rpc_transaction_config = RpcTransactionConfig {
encoding: Some(UiTransactionEncoding::Base64),
commitment: Some(self.client.commitment()),
..Default::default()
};
let transaction = self
.client
.get_transaction_with_config(&signature, rpc_transaction_config)
.map_err(|e| RpcError::CustomError(e.to_string()))?;
let decoded_transaction = transaction
.transaction
.transaction
.decode()
.clone()
.unwrap();
let account_keys = decoded_transaction.message.static_account_keys();
let meta = transaction.transaction.meta.as_ref().ok_or_else(|| {
RpcError::CustomError("Transaction missing metadata information".to_string())
})?;
if meta.status.is_err() {
return Err(RpcError::CustomError(
"Transaction status indicates an error".to_string(),
));
}
let inner_instructions = match &meta.inner_instructions {
OptionSerializer::Some(i) => i,
OptionSerializer::None => {
return Err(RpcError::CustomError(
"No inner instructions found".to_string(),
));
}
OptionSerializer::Skip => {
return Err(RpcError::CustomError(
"No inner instructions found".to_string(),
));
}
};
for ix in inner_instructions.iter() {
for ui_instruction in ix.instructions.iter() {
match ui_instruction {
UiInstruction::Compiled(ui_compiled_instruction) => {
let accounts = &ui_compiled_instruction.accounts;
let data = bs58::decode(&ui_compiled_instruction.data)
.into_vec()
.map_err(|_| {
RpcError::CustomError(
"Failed to decode instruction data".to_string(),
)
})?;
vec.push(data);
program_ids.push(light_compressed_account::Pubkey::new_from_array(
account_keys[ui_compiled_instruction.program_id_index as usize]
.to_bytes(),
));
vec_accounts.push(
accounts
.iter()
.map(|x| {
light_compressed_account::Pubkey::new_from_array(
account_keys[(*x) as usize].to_bytes(),
)
})
.collect(),
);
}
UiInstruction::Parsed(_) => {
println!("Parsed instructions are not implemented yet");
}
}
}
}
}
let parsed_event =
event_from_light_transaction(program_ids.as_slice(), vec.as_slice(), vec_accounts)
.map_err(|e| RpcError::CustomError(format!("Failed to parse event: {e:?}")))?;
let event = parsed_event.map(|e| (e, signature, slot));
Ok(event)
}
async fn _create_and_send_transaction_with_event<T>(
&mut self,
instructions: &[Instruction],
payer: &Pubkey,
signers: &[&Keypair],
) -> Result<Option<(T, Signature, u64)>, RpcError>
where
T: BorshDeserialize + Send + Debug,
{
let latest_blockhash = self.client.get_latest_blockhash()?;
let mut instructions_vec = vec![
solana_compute_budget_interface::ComputeBudgetInstruction::set_compute_unit_limit(
1_000_000,
),
];
instructions_vec.extend_from_slice(instructions);
let transaction = Transaction::new_signed_with_payer(
instructions_vec.as_slice(),
Some(payer),
signers,
latest_blockhash,
);
let (signature, slot) = self
.process_transaction_with_context(transaction.clone())
.await?;
let mut parsed_event = None;
for instruction in &transaction.message.instructions {
let ix_data = instruction.data.clone();
match T::deserialize(&mut &instruction.data[..]) {
Ok(e) => {
parsed_event = Some(e);
break;
}
Err(e) => {
warn!(
"Failed to parse event: {:?}, type: {:?}, ix data: {:?}",
e,
std::any::type_name::<T>(),
ix_data
);
}
}
}
if parsed_event.is_none() {
parsed_event = self.parse_inner_instructions::<T>(signature).ok();
}
let result = parsed_event.map(|e| (e, signature, slot));
Ok(result)
}
}
impl LightClient {
#[allow(clippy::result_large_err)]
fn parse_inner_instructions<T: BorshDeserialize>(
&self,
signature: Signature,
) -> Result<T, RpcError> {
let rpc_transaction_config = RpcTransactionConfig {
encoding: Some(UiTransactionEncoding::Base64),
commitment: Some(self.client.commitment()),
..Default::default()
};
let transaction = self
.client
.get_transaction_with_config(&signature, rpc_transaction_config)
.map_err(|e| RpcError::CustomError(e.to_string()))?;
let meta = transaction.transaction.meta.as_ref().ok_or_else(|| {
RpcError::CustomError("Transaction missing metadata information".to_string())
})?;
if meta.status.is_err() {
return Err(RpcError::CustomError(
"Transaction status indicates an error".to_string(),
));
}
let inner_instructions = match &meta.inner_instructions {
OptionSerializer::Some(i) => i,
OptionSerializer::None => {
return Err(RpcError::CustomError(
"No inner instructions found".to_string(),
));
}
OptionSerializer::Skip => {
return Err(RpcError::CustomError(
"No inner instructions found".to_string(),
));
}
};
for ix in inner_instructions.iter() {
for ui_instruction in ix.instructions.iter() {
match ui_instruction {
UiInstruction::Compiled(ui_compiled_instruction) => {
let data = bs58::decode(&ui_compiled_instruction.data)
.into_vec()
.map_err(|_| {
RpcError::CustomError(
"Failed to decode instruction data".to_string(),
)
})?;
match T::try_from_slice(data.as_slice()) {
Ok(parsed_data) => return Ok(parsed_data),
Err(e) => {
warn!("Failed to parse inner instruction: {:?}", e);
}
}
}
UiInstruction::Parsed(_) => {
println!("Parsed instructions are not implemented yet");
}
}
}
}
Err(RpcError::CustomError(
"Failed to find any parseable inner instructions".to_string(),
))
}
pub async fn warp_to_slot(&self, slot: Slot) -> Result<serde_json::Value, RpcError> {
let url = self.client.url();
let body = serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": "surfnet_timeTravel",
"params": [{ "absoluteSlot": slot }]
});
let response = reqwest::Client::new()
.post(url)
.json(&body)
.send()
.await
.map_err(|e| RpcError::CustomError(format!("warp_to_slot failed: {e}")))?;
let result: serde_json::Value = response
.json()
.await
.map_err(|e| RpcError::CustomError(format!("warp_to_slot response error: {e}")))?;
Ok(result)
}
}
use crate::indexer::ColdContext as IndexerColdContext;
fn cold_context_to_compressed_account(
cold: &IndexerColdContext,
lamports: u64,
owner: Pubkey,
) -> crate::indexer::CompressedAccount {
use light_compressed_account::compressed_account::CompressedAccountData;
crate::indexer::CompressedAccount {
address: cold.address,
data: Some(CompressedAccountData {
discriminator: cold.data.discriminator,
data: cold.data.data.clone(),
data_hash: cold.data.data_hash,
}),
hash: cold.hash,
lamports,
leaf_index: cold.leaf_index as u32,
owner,
prove_by_index: cold.prove_by_index,
seq: cold.tree_info.seq,
slot_created: cold.tree_info.slot_created,
tree_info: TreeInfo {
tree: cold.tree_info.tree,
queue: cold.tree_info.queue,
cpi_context: None,
next_tree_info: None,
tree_type: cold.tree_info.tree_type,
},
}
}
fn convert_account_interface(
indexer_ai: IndexerAccountInterface,
) -> Result<AccountInterface, RpcError> {
let account = Account {
lamports: indexer_ai.account.lamports,
data: indexer_ai.account.data,
owner: indexer_ai.account.owner,
executable: indexer_ai.account.executable,
rent_epoch: indexer_ai.account.rent_epoch,
};
match indexer_ai.cold {
None => Ok(AccountInterface::hot(indexer_ai.key, account)),
Some(cold) => {
let compressed = cold_context_to_compressed_account(
&cold,
indexer_ai.account.lamports,
indexer_ai.account.owner,
);
Ok(AccountInterface::cold(
indexer_ai.key,
compressed,
indexer_ai.account.owner,
))
}
}
}
fn convert_token_account_interface(
indexer_tai: IndexerTokenAccountInterface,
) -> Result<TokenAccountInterface, RpcError> {
use crate::indexer::CompressedTokenAccount;
let account = Account {
lamports: indexer_tai.account.account.lamports,
data: indexer_tai.account.account.data.clone(),
owner: indexer_tai.account.account.owner,
executable: indexer_tai.account.account.executable,
rent_epoch: indexer_tai.account.account.rent_epoch,
};
match indexer_tai.account.cold {
None => TokenAccountInterface::hot(indexer_tai.account.key, account)
.map_err(|e| RpcError::CustomError(format!("parse error: {}", e))),
Some(cold) => {
let compressed_account = cold_context_to_compressed_account(
&cold,
indexer_tai.account.account.lamports,
indexer_tai.account.account.owner,
);
let token_owner = indexer_tai.token.owner;
let compressed_token = CompressedTokenAccount {
token: indexer_tai.token,
account: compressed_account,
};
Ok(TokenAccountInterface::cold(
indexer_tai.account.key,
compressed_token,
token_owner,
indexer_tai.account.account.owner,
))
}
}
}
#[async_trait]
impl Rpc for LightClient {
async fn new(config: LightClientConfig) -> Result<Self, RpcError>
where
Self: Sized,
{
Self::new_with_retry(config, None).await
}
fn get_payer(&self) -> &Keypair {
&self.payer
}
fn get_url(&self) -> String {
self.client.url()
}
async fn health(&self) -> Result<(), RpcError> {
self.retry(|| async { self.client.get_health().map_err(RpcError::from) })
.await
}
async fn get_program_accounts(
&self,
program_id: &Pubkey,
) -> Result<Vec<(Pubkey, Account)>, RpcError> {
self.retry(|| async {
self.client
.get_program_accounts(program_id)
.map_err(RpcError::from)
})
.await
}
async fn get_program_accounts_with_discriminator(
&self,
program_id: &Pubkey,
discriminator: &[u8],
) -> Result<Vec<(Pubkey, Account)>, RpcError> {
use solana_rpc_client_api::{
config::{RpcAccountInfoConfig, RpcProgramAccountsConfig},
filter::{Memcmp, RpcFilterType},
};
let discriminator = discriminator.to_vec();
self.retry(|| async {
let config = RpcProgramAccountsConfig {
filters: Some(vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
0,
&discriminator,
))]),
account_config: RpcAccountInfoConfig {
encoding: Some(solana_account_decoder_client_types::UiAccountEncoding::Base64),
commitment: Some(self.client.commitment()),
..Default::default()
},
..Default::default()
};
self.client
.get_program_accounts_with_config(program_id, config)
.map_err(RpcError::from)
})
.await
}
async fn process_transaction(
&mut self,
transaction: Transaction,
) -> Result<Signature, RpcError> {
self.retry(|| async {
self.client
.send_and_confirm_transaction(&transaction)
.map_err(RpcError::from)
})
.await
}
async fn process_transaction_with_context(
&mut self,
transaction: Transaction,
) -> Result<(Signature, Slot), RpcError> {
self.retry(|| async {
let signature = self.client.send_and_confirm_transaction(&transaction)?;
let sig_info = self.client.get_signature_statuses(&[signature])?;
let slot = sig_info
.value
.first()
.and_then(|s| s.as_ref())
.map(|s| s.slot)
.ok_or_else(|| RpcError::CustomError("Failed to get slot".into()))?;
Ok((signature, slot))
})
.await
}
async fn confirm_transaction(&self, signature: Signature) -> Result<bool, RpcError> {
self.retry(|| async {
self.client
.confirm_transaction(&signature)
.map_err(RpcError::from)
})
.await
}
async fn get_account(&self, address: Pubkey) -> Result<Option<Account>, RpcError> {
self.retry(|| async {
self.client
.get_account_with_commitment(&address, self.client.commitment())
.map(|response| response.value)
.map_err(RpcError::from)
})
.await
}
async fn get_multiple_accounts(
&self,
addresses: &[Pubkey],
) -> Result<Vec<Option<Account>>, RpcError> {
self.retry(|| async {
self.client
.get_multiple_accounts(addresses)
.map_err(RpcError::from)
})
.await
}
async fn get_minimum_balance_for_rent_exemption(
&self,
data_len: usize,
) -> Result<u64, RpcError> {
self.retry(|| async {
self.client
.get_minimum_balance_for_rent_exemption(data_len)
.map_err(RpcError::from)
})
.await
}
async fn airdrop_lamports(
&mut self,
to: &Pubkey,
lamports: u64,
) -> Result<Signature, RpcError> {
self.retry(|| async {
let signature = self
.client
.request_airdrop(to, lamports)
.map_err(RpcError::ClientError)?;
self.retry(|| async {
if self
.client
.confirm_transaction_with_commitment(&signature, self.client.commitment())?
.value
{
Ok(())
} else {
Err(RpcError::CustomError("Airdrop not confirmed".into()))
}
})
.await?;
Ok(signature)
})
.await
}
async fn get_balance(&self, pubkey: &Pubkey) -> Result<u64, RpcError> {
self.retry(|| async { self.client.get_balance(pubkey).map_err(RpcError::from) })
.await
}
async fn get_latest_blockhash(&mut self) -> Result<(Hash, u64), RpcError> {
self.retry(|| async {
self.client
.get_latest_blockhash_with_commitment(CommitmentConfig::confirmed())
.map_err(RpcError::from)
})
.await
}
async fn get_slot(&self) -> Result<u64, RpcError> {
self.retry(|| async { self.client.get_slot().map_err(RpcError::from) })
.await
}
async fn send_transaction(&self, transaction: &Transaction) -> Result<Signature, RpcError> {
self.retry(|| async {
self.client
.send_transaction_with_config(
transaction,
RpcSendTransactionConfig {
skip_preflight: true,
max_retries: Some(self.retry_config.max_retries as usize),
..Default::default()
},
)
.map_err(RpcError::from)
})
.await
}
async fn send_transaction_with_config(
&self,
transaction: &Transaction,
config: RpcSendTransactionConfig,
) -> Result<Signature, RpcError> {
self.retry(|| async {
self.client
.send_transaction_with_config(transaction, config)
.map_err(RpcError::from)
})
.await
}
async fn get_transaction_slot(&self, signature: &Signature) -> Result<u64, RpcError> {
self.retry(|| async {
Ok(self
.client
.get_transaction_with_config(
signature,
RpcTransactionConfig {
encoding: Some(UiTransactionEncoding::Base64),
commitment: Some(self.client.commitment()),
..Default::default()
},
)
.map_err(RpcError::from)?
.slot)
})
.await
}
async fn get_signature_statuses(
&self,
signatures: &[Signature],
) -> Result<Vec<Option<TransactionStatus>>, RpcError> {
self.client
.get_signature_statuses(signatures)
.map(|response| response.value)
.map_err(RpcError::from)
}
async fn create_and_send_transaction_with_event<T>(
&mut self,
instructions: &[Instruction],
payer: &Pubkey,
signers: &[&Keypair],
) -> Result<Option<(T, Signature, u64)>, RpcError>
where
T: BorshDeserialize + Send + Debug,
{
self._create_and_send_transaction_with_event::<T>(instructions, payer, signers)
.await
}
async fn create_and_send_transaction_with_public_event(
&mut self,
instructions: &[Instruction],
payer: &Pubkey,
signers: &[&Keypair],
) -> Result<Option<(PublicTransactionEvent, Signature, Slot)>, RpcError> {
let parsed_event = self
._create_and_send_transaction_with_batched_event(instructions, payer, signers)
.await?;
let event = parsed_event.map(|(e, signature, slot)| (e[0].event.clone(), signature, slot));
Ok(event)
}
async fn create_and_send_transaction_with_batched_event(
&mut self,
instructions: &[Instruction],
payer: &Pubkey,
signers: &[&Keypair],
) -> Result<Option<(Vec<BatchPublicTransactionEvent>, Signature, Slot)>, RpcError> {
self._create_and_send_transaction_with_batched_event(instructions, payer, signers)
.await
}
async fn create_and_send_versioned_transaction<'a>(
&'a mut self,
instructions: &'a [Instruction],
payer: &'a Pubkey,
signers: &'a [&'a Keypair],
address_lookup_tables: &'a [AddressLookupTableAccount],
) -> Result<Signature, RpcError> {
let blockhash = self.get_latest_blockhash().await?.0;
let message =
v0::Message::try_compile(payer, instructions, address_lookup_tables, blockhash)
.map_err(|e| {
RpcError::CustomError(format!("Failed to compile v0 message: {}", e))
})?;
let versioned_message = VersionedMessage::V0(message);
let transaction = VersionedTransaction::try_new(versioned_message, signers)
.map_err(|e| RpcError::SigningError(e.to_string()))?;
self.retry(|| async {
self.client
.send_and_confirm_transaction(&transaction)
.map_err(RpcError::from)
})
.await
}
fn indexer(&self) -> Result<&impl Indexer, RpcError> {
self.indexer.as_ref().ok_or(RpcError::IndexerNotInitialized)
}
fn indexer_mut(&mut self) -> Result<&mut impl Indexer, RpcError> {
self.indexer.as_mut().ok_or(RpcError::IndexerNotInitialized)
}
async fn get_latest_active_state_trees(&mut self) -> Result<Vec<TreeInfo>, RpcError> {
#[cfg(feature = "v2")]
{
let trees = default_v2_state_trees().to_vec();
self.state_merkle_trees = trees.clone();
return Ok(trees);
}
#[cfg(not(feature = "v2"))]
{
let network = self.detect_network();
if matches!(network, RpcUrl::Localnet) {
let default_trees = vec![TreeInfo {
tree: pubkey!("smt1NamzXdq4AMqS2fS2F1i5KTYPZRhoHgWx38d8WsT"),
queue: pubkey!("nfq1NvQDJ2GEgnS8zt9prAe8rjjpAW1zFkrvZoBR148"),
cpi_context: Some(pubkey!("cpi1uHzrEhBG733DoEJNgHCyRS3XmmyVNZx5fonubE4")),
next_tree_info: None,
tree_type: TreeType::StateV1,
}];
self.state_merkle_trees = default_trees.clone();
return Ok(default_trees);
}
let (mainnet_tables, devnet_tables) = default_state_tree_lookup_tables();
let lookup_tables = match network {
RpcUrl::Devnet | RpcUrl::Testnet | RpcUrl::ZKTestnet => &devnet_tables,
_ => &mainnet_tables,
};
let res = get_light_state_tree_infos(
self,
&lookup_tables[0].state_tree_lookup_table,
&lookup_tables[0].nullify_table,
)
.await?;
self.state_merkle_trees = res.clone();
Ok(res)
}
}
fn get_state_tree_infos(&self) -> Vec<TreeInfo> {
#[cfg(feature = "v2")]
{
default_v2_state_trees().to_vec()
}
#[cfg(not(feature = "v2"))]
{
self.state_merkle_trees.to_vec()
}
}
fn get_random_state_tree_info(&self) -> Result<TreeInfo, RpcError> {
#[cfg(feature = "v2")]
{
use rand::Rng;
let mut rng = rand::thread_rng();
let trees = default_v2_state_trees();
Ok(trees[rng.gen_range(0..trees.len())])
}
#[cfg(not(feature = "v2"))]
{
let mut rng = rand::thread_rng();
let filtered_trees: Vec<TreeInfo> = self
.state_merkle_trees
.iter()
.filter(|tree| tree.tree_type == TreeType::StateV1)
.copied()
.collect();
select_state_tree_info(&mut rng, &filtered_trees)
}
}
fn get_random_state_tree_info_v1(&self) -> Result<TreeInfo, RpcError> {
let mut rng = rand::thread_rng();
let v1_trees: Vec<TreeInfo> = self
.state_merkle_trees
.iter()
.filter(|tree| tree.tree_type == TreeType::StateV1)
.copied()
.collect();
select_state_tree_info(&mut rng, &v1_trees)
}
fn get_address_tree_v1(&self) -> TreeInfo {
TreeInfo {
tree: pubkey!("amt1Ayt45jfbdw5YSo7iz6WZxUmnZsQTYXy82hVwyC2"),
queue: pubkey!("aq1S9z4reTSQAdgWHGD2zDaS39sjGrAxbR31vxJ2F4F"),
cpi_context: None,
next_tree_info: None,
tree_type: TreeType::AddressV1,
}
}
fn get_address_tree_v2(&self) -> TreeInfo {
TreeInfo {
tree: pubkey!("amt2kaJA14v3urZbZvnc5v2np8jqvc4Z8zDep5wbtzx"),
queue: pubkey!("amt2kaJA14v3urZbZvnc5v2np8jqvc4Z8zDep5wbtzx"),
cpi_context: None,
next_tree_info: None,
tree_type: TreeType::AddressV2,
}
}
async fn get_account_interface(
&self,
address: &Pubkey,
config: Option<IndexerRpcConfig>,
) -> Result<Response<Option<AccountInterface>>, RpcError> {
let indexer = self
.indexer
.as_ref()
.ok_or(RpcError::IndexerNotInitialized)?;
let resp = indexer
.get_account_interface(address, config)
.await
.map_err(|e| RpcError::CustomError(format!("Indexer error: {e}")))?;
let value = resp.value.map(convert_account_interface).transpose()?;
Ok(Response {
context: resp.context,
value,
})
}
async fn get_token_account_interface(
&self,
address: &Pubkey,
config: Option<IndexerRpcConfig>,
) -> Result<Response<Option<TokenAccountInterface>>, RpcError> {
let indexer = self
.indexer
.as_ref()
.ok_or(RpcError::IndexerNotInitialized)?;
let resp = indexer
.get_token_account_interface(address, config)
.await
.map_err(|e| RpcError::CustomError(format!("Indexer error: {e}")))?;
let value = match resp.value {
Some(tai) => Some(convert_token_account_interface(tai)?),
None => None,
};
Ok(Response {
context: resp.context,
value,
})
}
async fn get_associated_token_account_interface(
&self,
owner: &Pubkey,
mint: &Pubkey,
config: Option<IndexerRpcConfig>,
) -> Result<Response<Option<TokenAccountInterface>>, RpcError> {
let indexer = self
.indexer
.as_ref()
.ok_or(RpcError::IndexerNotInitialized)?;
let resp = indexer
.get_associated_token_account_interface(owner, mint, config)
.await
.map_err(|e| RpcError::CustomError(format!("Indexer error: {e}")))?;
let value = match resp.value {
Some(tai) => {
let mut iface = convert_token_account_interface(tai)?;
if iface.is_cold() {
iface.parsed.owner = *owner;
}
Some(iface)
}
None => None,
};
Ok(Response {
context: resp.context,
value,
})
}
async fn get_multiple_account_interfaces(
&self,
addresses: Vec<&Pubkey>,
config: Option<IndexerRpcConfig>,
) -> Result<Response<Vec<Option<AccountInterface>>>, RpcError> {
let indexer = self
.indexer
.as_ref()
.ok_or(RpcError::IndexerNotInitialized)?;
let resp = indexer
.get_multiple_account_interfaces(addresses, config)
.await
.map_err(|e| RpcError::CustomError(format!("Indexer error: {e}")))?;
let value: Result<Vec<Option<AccountInterface>>, RpcError> = resp
.value
.into_iter()
.map(|opt| opt.map(convert_account_interface).transpose())
.collect();
Ok(Response {
context: resp.context,
value: value?,
})
}
async fn get_mint_interface(
&self,
address: &Pubkey,
config: Option<IndexerRpcConfig>,
) -> Result<Response<Option<MintInterface>>, RpcError> {
use light_compressed_account::address::derive_address;
use light_token_interface::{state::Mint, MINT_ADDRESS_TREE};
let address_tree = Pubkey::new_from_array(MINT_ADDRESS_TREE);
let compressed_address = derive_address(
&address.to_bytes(),
&address_tree.to_bytes(),
&light_token_interface::LIGHT_TOKEN_PROGRAM_ID,
);
let indexer = self
.indexer
.as_ref()
.ok_or(RpcError::IndexerNotInitialized)?;
let resp = indexer
.get_account_interface(address, config.clone())
.await
.map_err(|e| RpcError::CustomError(format!("Indexer error: {e}")))?;
let value = match resp.value {
Some(ai) => {
let state = if ai.is_cold() {
let cold = ai.cold.as_ref().ok_or_else(|| {
RpcError::CustomError("Cold mint missing cold context".into())
})?;
let mut compressed = cold_context_to_compressed_account(
cold,
ai.account.lamports,
ai.account.owner,
);
if compressed.address.is_none() {
compressed.address = Some(compressed_address);
}
let mint_data = if cold.data.data.is_empty() {
None
} else {
Mint::try_from_slice(&cold.data.data).ok()
}
.ok_or_else(|| {
RpcError::CustomError(
"Missing or invalid mint data in compressed account".into(),
)
})?;
MintState::Cold {
compressed,
mint_data,
}
} else {
let expected_owner =
Pubkey::new_from_array(light_token_interface::LIGHT_TOKEN_PROGRAM_ID);
if ai.account.owner != expected_owner {
return Err(RpcError::CustomError(format!(
"Invalid mint account owner: expected {}, got {}",
expected_owner, ai.account.owner,
)));
}
Mint::try_from_slice(&ai.account.data).map_err(|e| {
RpcError::CustomError(format!(
"Failed to deserialize hot mint account: {e}"
))
})?;
MintState::Hot {
account: ai.account,
}
};
Some(MintInterface {
mint: *address,
address_tree,
compressed_address,
state,
})
}
None => None,
};
Ok(Response {
context: resp.context,
value,
})
}
}
impl MerkleTreeExt for LightClient {}
pub fn select_state_tree_info<R: rand::Rng>(
rng: &mut R,
state_trees: &[TreeInfo],
) -> Result<TreeInfo, RpcError> {
if state_trees.is_empty() {
return Err(RpcError::NoStateTreesAvailable);
}
Ok(state_trees[rng.gen_range(0..state_trees.len())])
}