Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
213 changes: 114 additions & 99 deletions sdk-libs/client/src/rpc/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,23 +30,62 @@ 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,
get_light_state_tree_infos::{
default_state_tree_lookup_tables, get_light_state_tree_infos,
},
merkle_tree::MerkleTreeExt,
Rpc,
},
rpc::{errors::RpcError, merkle_tree::MerkleTreeExt, Rpc},
};

/// V2 batched state trees.
#[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,
Expand Down Expand Up @@ -135,7 +174,8 @@ impl LightClient {
self.indexer = Some(PhotonIndexer::new(path, api_key));
}

/// Detects the network type based on the RPC URL
/// Detects the network type based on the RPC URL. V1 only.
#[cfg(not(feature = "v2"))]
fn detect_network(&self) -> RpcUrl {
let url = self.client.url();

Expand Down Expand Up @@ -963,113 +1003,88 @@ impl Rpc for LightClient {
}

/// Fetch the latest state tree addresses from the cluster.
///
/// When the `v2` feature is enabled, returns the default V2
/// batched state trees.
/// When `v2` is disabled, uses V1 lookup-table resolution or
/// localnet defaults.
async fn get_latest_active_state_trees(&mut self) -> Result<Vec<TreeInfo>, RpcError> {
let network = self.detect_network();

// Return default test values for localnet
if matches!(network, RpcUrl::Localnet) {
use light_compressed_account::TreeType;
use solana_pubkey::pubkey;
// V2: the default batched state trees are the same on every network.
#[cfg(feature = "v2")]
{
let trees = default_v2_state_trees().to_vec();
self.state_merkle_trees = trees.clone();
return Ok(trees);
}
Comment on lines +1012 to +1018
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

V2 cache write is never read — pick one source of truth.

Line 1016 stores the V2 trees in self.state_merkle_trees, but the other V2 code paths (get_state_tree_infos at line 1059, get_random_state_tree_info at line 1073) call default_v2_state_trees() directly, bypassing the cache entirely. This means the field write here is dead code in V2 mode.

I'd recommend one of two approaches:

  • Option A (simplest): Remove the cache write in V2 mode — keeps things honest about where the data lives.
  • Option B (consistent): Have all V2 paths read from self.state_merkle_trees, populated once here. This also avoids reconstructing the array on every call.
Option A: remove redundant cache write
         #[cfg(feature = "v2")]
         {
-            let trees = default_v2_state_trees().to_vec();
-            self.state_merkle_trees = trees.clone();
-            return Ok(trees);
+            return Ok(default_v2_state_trees().to_vec());
         }

Then get_state_tree_infos and get_random_state_tree_info V2 paths stay as-is.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// V2: the default batched state trees are the same on every network.
#[cfg(feature = "v2")]
{
let trees = default_v2_state_trees().to_vec();
self.state_merkle_trees = trees.clone();
return Ok(trees);
}
// V2: the default batched state trees are the same on every network.
#[cfg(feature = "v2")]
{
return Ok(default_v2_state_trees().to_vec());
}
🤖 Prompt for AI Agents
In `@sdk-libs/client/src/rpc/client.rs` around lines 1012 - 1018, The V2 code
currently writes to self.state_merkle_trees inside the V2 branch but other V2
paths (get_state_tree_infos and get_random_state_tree_info) call
default_v2_state_trees() directly, making that write dead; to fix, remove the
redundant cache write in the V2 branch (delete the assignment to
self.state_merkle_trees = trees.clone()) so the function simply returns
default_v2_state_trees().to_vec(), keeping default_v2_state_trees() as the
single source of truth; alternatively, if you prefer caching, update
get_state_tree_infos and get_random_state_tree_info to read from
self.state_merkle_trees (ensuring it's initialized here) — pick one approach and
make all V2 code paths consistent with it.


use crate::indexer::TreeInfo;
// V1 path: network-dependent resolution.
#[cfg(not(feature = "v2"))]
{
let network = self.detect_network();

#[cfg(feature = "v2")]
let default_trees = vec![
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")),
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::StateV2,
},
TreeInfo {
tree: pubkey!("bmt5yU97jC88YXTuSukYHa8Z5Bi2ZDUtmzfkDTA2mG2"),
queue: pubkey!("oq5oh5ZR3yGomuQgFduNDzjtGvVWfDRGLuDVjv9a96P"),
cpi_context: Some(pubkey!("cpi5ZTjdgYpZ1Xr7B1cMLLUE81oTtJbNNAyKary2nV6")),
next_tree_info: None,
tree_type: TreeType::StateV2,
},
];

#[cfg(not(feature = "v2"))]
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);
}
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 (mainnet_tables, devnet_tables) = default_state_tree_lookup_tables();

let lookup_tables = match network {
RpcUrl::Devnet | RpcUrl::Testnet | RpcUrl::ZKTestnet => &devnet_tables,
_ => &mainnet_tables, // Default to mainnet for production and custom URLs
};
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)
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)
}
}

/// Fetch the latest state tree addresses from the cluster.
/// Returns list of state tree infos.
fn get_state_tree_infos(&self) -> Vec<TreeInfo> {
self.state_merkle_trees.to_vec()
#[cfg(feature = "v2")]
{
default_v2_state_trees().to_vec()
}
#[cfg(not(feature = "v2"))]
{
self.state_merkle_trees.to_vec()
}
}

/// Gets a random active state tree.
/// State trees are cached and have to be fetched or set.
/// Returns v1 state trees by default, v2 state trees when v2 feature is enabled.
fn get_random_state_tree_info(&self) -> Result<TreeInfo, RpcError> {
let mut rng = rand::thread_rng();

#[cfg(feature = "v2")]
let filtered_trees: Vec<TreeInfo> = self
.state_merkle_trees
.iter()
.filter(|tree| tree.tree_type == TreeType::StateV2)
.copied()
.collect();
{
use rand::Rng;
let mut rng = rand::thread_rng();
let trees = default_v2_state_trees();
Ok(trees[rng.gen_range(0..trees.len())])
}
Comment on lines 1069 to +1075
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

V2 path duplicates select_state_tree_info — reuse the existing helper.

select_state_tree_info (line 1341) already handles the "pick a random tree from a slice" logic, including the empty-slice guard. The V2 block here re-implements it inline. This is a DRY violation, and you lose the NoStateTreesAvailable guard (currently safe with 5 hardcoded entries, but fragile if the function signature ever changes to accept a dynamic set).

♻️ Proposed fix
         #[cfg(feature = "v2")]
         {
-            use rand::Rng;
-            let mut rng = rand::thread_rng();
+            use rand::thread_rng;
             let trees = default_v2_state_trees();
-            Ok(trees[rng.gen_range(0..trees.len())])
+            select_state_tree_info(&mut thread_rng(), &trees)
         }
🤖 Prompt for AI Agents
In `@sdk-libs/client/src/rpc/client.rs` around lines 1069 - 1075, The V2 branch
duplicates the random-selection logic and omits the empty-slice guard; replace
the inline selection with a call to the existing helper select_state_tree_info
so you reuse its empty-slice check and error variant (NoStateTreesAvailable).
Locate the V2 block that currently calls default_v2_state_trees() and randomly
indexes into the vector, and instead call
select_state_tree_info(&default_v2_state_trees()) (or adapt the helper's
signature if needed) and return its Result so the same guard and error handling
are preserved.


#[cfg(not(feature = "v2"))]
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)
{
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)
}
}

/// Gets a random v1 state tree.
Expand Down
Loading