use std::{
collections::HashMap,
str::FromStr,
sync::{
atomic::{AtomicU64, Ordering},
Arc, Mutex,
},
};
use anyhow::Result;
use async_trait::async_trait;
use base64::Engine;
use litesvm::LiteSVM;
use serde_json::{to_value, Value};
use solana_client::{
client_error::{ClientError, ClientErrorKind, Result as ClientResult},
nonblocking::rpc_client::RpcClient,
rpc_client::{RpcClientConfig, SerializableTransaction},
rpc_request::RpcRequest,
rpc_response::{
Response, RpcBlockhash, RpcResponseContext, RpcSimulateTransactionResult, RpcVersionInfo,
},
rpc_sender::{RpcSender, RpcTransportStats},
};
use solana_sdk::{
account::Account, clock::Clock, epoch_info::EpochInfo, pubkey::Pubkey, rent::Rent,
signature::Signature, transaction::VersionedTransaction,
};
use solana_transaction_status::{
EncodedConfirmedTransactionWithStatusMeta, EncodedTransaction,
EncodedTransactionWithStatusMeta, UiTransactionStatusMeta,
};
#[derive(Debug, Clone, Copy)]
enum UiAccountEncoding {
Base64,
JsonParsed,
}
fn encode_ui_account(
_address: &Pubkey,
account: &Account,
encoding: UiAccountEncoding,
_max_len: Option<usize>,
_data_slice_config: Option<()>,
) -> Value {
match encoding {
UiAccountEncoding::Base64 => {
serde_json::json!({
"data": [base64::engine::general_purpose::STANDARD.encode(&account.data), "base64"],
"executable": account.executable,
"lamports": account.lamports,
"owner": account.owner.to_string(),
"rentEpoch": account.rent_epoch,
})
}
UiAccountEncoding::JsonParsed => {
serde_json::json!({
"data": account.data,
"executable": account.executable,
"lamports": account.lamports,
"owner": account.owner.to_string(),
"rentEpoch": account.rent_epoch,
})
}
}
}
fn get_encoding(config: &Value) -> UiAccountEncoding {
config
.as_object()
.and_then(|x| x.get("encoding"))
.and_then(|x| x.as_str())
.and_then(|x| match x {
"base64" => Some(UiAccountEncoding::Base64),
"jsonParsed" => Some(UiAccountEncoding::JsonParsed),
_ => None,
})
.unwrap_or(UiAccountEncoding::Base64)
}
fn to_wire_account(
address: &Pubkey,
account: Option<Account>,
encoding: UiAccountEncoding,
) -> Result<Value> {
if let Some(account) = account {
let value = to_value(encode_ui_account(address, &account, encoding, None, None))?;
Ok(value)
} else {
Ok(Value::Null)
}
}
fn send(svm: &mut LiteSVM, method: &str, params: &[Value]) -> Result<Value> {
let clock = svm.get_sysvar::<Clock>();
let slot = clock.slot;
let response = match method {
"getAccountInfo" => {
let address_str = params[0].as_str().unwrap_or_default();
let address = Pubkey::from_str(address_str)?;
let account = svm.get_account(&address);
let encoding = get_encoding(¶ms[1]);
to_value(Response {
context: RpcResponseContext { slot, api_version: None },
value: to_wire_account(&address, account, encoding)?,
})?
}
"getMultipleAccounts" => {
let default_addresses = Vec::new();
let addresses = params[0].as_array().unwrap_or(&default_addresses);
let encoding = get_encoding(¶ms[1]);
let mut accounts: Vec<Value> = Vec::new();
for address_str in addresses {
let address_str = address_str.as_str().unwrap_or_default();
let address = Pubkey::from_str(address_str)?;
let account = svm.get_account(&address);
accounts.push(to_wire_account(&address, account, encoding)?);
}
to_value(Response {
context: RpcResponseContext { slot, api_version: None },
value: accounts,
})?
}
"getMinimumBalanceForRentExemption" => {
let data_len = params[0].as_u64().unwrap_or(0) as usize;
let rent = Rent::default();
to_value(rent.minimum_balance(data_len))?
}
"getLatestBlockhash" => {
let blockhash = svm.latest_blockhash();
to_value(Response {
context: RpcResponseContext { slot, api_version: None },
value: RpcBlockhash {
blockhash: blockhash.to_string(),
last_valid_block_height: slot + 150,
},
})?
}
"sendTransaction" => {
let transaction_base64 = params[0].as_str().unwrap_or_default();
let transaction_bytes =
base64::engine::general_purpose::STANDARD.decode(transaction_base64)?;
let transaction = bincode::serde::decode_from_slice::<VersionedTransaction, _>(
&transaction_bytes,
bincode::config::standard(),
)?
.0;
let result = svm.send_transaction(transaction.clone());
match result {
Ok(_) => {
let signature = transaction.get_signature();
let signature_base58 = bs58::encode(signature).into_string();
let response = to_value(signature_base58)?;
tracing::debug!(?response, "sendTransaction response");
let mut updated_clock = svm.get_sysvar::<Clock>();
updated_clock.unix_timestamp += 1;
svm.set_sysvar::<Clock>(&updated_clock);
response
}
Err(e) => {
return Err(anyhow::anyhow!("Transaction failed: {:?}", e));
}
}
}
"simulateTransaction" => {
let tx_base64 = params[0].as_str().unwrap_or_default();
let tx_bytes = base64::engine::general_purpose::STANDARD.decode(tx_base64)?;
let transaction = bincode::serde::decode_from_slice::<VersionedTransaction, _>(
&tx_bytes,
bincode::config::standard(),
)?
.0;
let result = svm.simulate_transaction(transaction);
let (err, logs, units_consumed) = match result {
Ok(info) => (None, Some(info.meta.logs), Some(info.meta.compute_units_consumed)),
Err(failed) => (
Some(failed.err.into()),
Some(failed.meta.logs),
Some(failed.meta.compute_units_consumed),
),
};
to_value(Response {
context: RpcResponseContext { slot, api_version: None },
value: RpcSimulateTransactionResult {
err,
logs,
accounts: None,
units_consumed,
loaded_accounts_data_size: None,
return_data: None,
inner_instructions: None,
replacement_blockhash: None,
fee: None,
pre_balances: None,
post_balances: None,
pre_token_balances: None,
post_token_balances: None,
loaded_addresses: None,
},
})?
}
"getTransaction" => {
let signature_str = params[0].as_str().unwrap_or_default();
let signature = Signature::from_str(signature_str)?;
let _config = params.get(1);
let _encoding =
_config.and_then(|c| c.get("encoding")).and_then(|e| e.as_str()).unwrap_or("json");
let tx_metadata = svm.get_transaction(&signature);
match tx_metadata {
Some(Ok(metadata)) => {
let log_messages: Vec<String> = metadata.logs.clone();
let transaction_json = serde_json::json!({
"signatures": [signature_str],
"message": serde_json::json!({
"accountKeys": [],
"header": serde_json::json!({
"numRequiredSignatures": 0u8,
"numReadonlySignedAccounts": 0u8,
"numReadonlyUnsignedAccounts": 0u8,
}),
"recentBlockhash": "",
"instructions": [],
}),
});
use solana_transaction_status::option_serializer::OptionSerializer;
let meta = UiTransactionStatusMeta {
err: None,
status: Ok(()),
fee: 0,
pre_balances: vec![],
post_balances: vec![],
inner_instructions: OptionSerializer::None,
log_messages: OptionSerializer::Some(log_messages),
pre_token_balances: OptionSerializer::None,
post_token_balances: OptionSerializer::None,
rewards: OptionSerializer::None,
loaded_addresses: OptionSerializer::Skip,
return_data: OptionSerializer::Skip,
compute_units_consumed: OptionSerializer::Skip,
cost_units: OptionSerializer::Skip,
};
let transaction =
EncodedTransaction::Json(serde_json::from_value(transaction_json)?);
let transaction_data = EncodedConfirmedTransactionWithStatusMeta {
slot,
transaction: EncodedTransactionWithStatusMeta {
transaction,
meta: Some(meta),
version: None,
},
block_time: None,
};
to_value(transaction_data)?
}
Some(Err(_)) => to_value(Response {
context: RpcResponseContext { slot, api_version: None },
value: Value::Null,
})?,
None => to_value(Response {
context: RpcResponseContext { slot, api_version: None },
value: Value::Null,
})?,
}
}
"getEpochInfo" => to_value(EpochInfo {
epoch: slot / 32,
slot_index: slot % 32,
slots_in_epoch: 32,
absolute_slot: slot,
block_height: slot,
transaction_count: Some(0),
})?,
"getVersion" => {
to_value(RpcVersionInfo { solana_core: "1.18.0".to_string(), feature_set: Some(0) })?
}
"getProgramAccounts" => {
use solana_sdk::account::ReadableAccount;
let program_id_str = params[0].as_str().unwrap_or_default();
let program_id = Pubkey::from_str(program_id_str)?;
let config = params.get(1).and_then(|c| c.as_object());
let encoding = params.get(1).map(get_encoding).unwrap_or(UiAccountEncoding::Base64);
let filters = config.and_then(|c| c.get("filters")).and_then(|f| f.as_array());
let mut results: Vec<Value> = Vec::new();
for (address, account_data) in &svm.accounts_db().inner {
let owner_bytes: [u8; 32] = account_data.owner().as_ref().try_into().unwrap();
let owner_pubkey = Pubkey::new_from_array(owner_bytes);
if owner_pubkey != program_id {
continue;
}
if let Some(filters) = filters {
let data = account_data.data();
let mut matches = true;
for filter in filters {
if let Some(obj) = filter.as_object() {
if let Some(memcmp) = obj.get("memcmp").and_then(|m| m.as_object()) {
let offset =
memcmp.get("offset").and_then(|o| o.as_u64()).unwrap_or(0)
as usize;
let bytes_str =
memcmp.get("bytes").and_then(|b| b.as_str()).unwrap_or("");
let expected =
bs58::decode(bytes_str).into_vec().unwrap_or_default();
if data.len() < offset + expected.len()
|| data[offset..offset + expected.len()] != expected[..]
{
matches = false;
break;
}
}
if let Some(size) = obj.get("dataSize").and_then(|s| s.as_u64()) {
if data.len() as u64 != size {
matches = false;
break;
}
}
}
}
if !matches {
continue;
}
}
let addr_bytes: [u8; 32] = address.as_ref().try_into().unwrap();
let addr = Pubkey::new_from_array(addr_bytes);
let account = Account {
lamports: account_data.lamports(),
data: account_data.data().to_vec(),
owner: owner_pubkey,
executable: account_data.executable(),
rent_epoch: account_data.rent_epoch(),
};
let encoded = encode_ui_account(&addr, &account, encoding, None, None);
results.push(serde_json::json!({
"pubkey": addr.to_string(),
"account": encoded,
}));
}
to_value(results)?
}
"getTokenAccountsByOwner" => {
use solana_sdk::account::ReadableAccount;
use spl_token::state::Account as TokenAccount;
let owner_str = params[0].as_str().unwrap_or_default();
let owner = Pubkey::from_str(owner_str)?;
let encoding = params.get(2).map(get_encoding).unwrap_or(UiAccountEncoding::Base64);
let filter = params.get(1).and_then(|f| f.as_object());
let filter_mint = filter
.and_then(|f| f.get("mint"))
.and_then(|m| m.as_str())
.and_then(|s| Pubkey::from_str(s).ok());
let filter_program_id = filter
.and_then(|f| f.get("programId"))
.and_then(|p| p.as_str())
.and_then(|s| Pubkey::from_str(s).ok());
let token_program = filter_program_id.unwrap_or(spl_token::ID);
let mut results: Vec<Value> = Vec::new();
for (address, account_data) in &svm.accounts_db().inner {
let owner_bytes: [u8; 32] = account_data.owner().as_ref().try_into().unwrap();
let account_owner = Pubkey::new_from_array(owner_bytes);
if account_owner != token_program {
continue;
}
use solana_sdk::program_pack::Pack;
if let Ok(token_account) = TokenAccount::unpack(account_data.data()) {
if token_account.owner != owner {
continue;
}
if let Some(mint) = filter_mint {
if token_account.mint != mint {
continue;
}
}
let addr_bytes: [u8; 32] = address.as_ref().try_into().unwrap();
let addr = Pubkey::new_from_array(addr_bytes);
let account = Account {
lamports: account_data.lamports(),
data: account_data.data().to_vec(),
owner: account_owner,
executable: account_data.executable(),
rent_epoch: account_data.rent_epoch(),
};
let encoded = encode_ui_account(&addr, &account, encoding, None, None);
results.push(serde_json::json!({
"pubkey": addr.to_string(),
"account": encoded,
}));
}
}
to_value(Response {
context: RpcResponseContext { slot, api_version: None },
value: results,
})?
}
"getTokenLargestAccounts" => {
use solana_sdk::{account::ReadableAccount, program_pack::Pack};
use spl_token::state::{Account as TokenAccount, Mint};
const MAX_RESULTS: usize = 20;
const TOKEN_ACCOUNT_BASE_LEN: usize = 165;
let mint_str = params[0].as_str().unwrap_or_default();
let mint = Pubkey::from_str(mint_str)?;
let decimals = match svm.get_account(&mint) {
Some(acct) if acct.data.len() >= Mint::LEN => Mint::unpack(&acct.data[..Mint::LEN])
.map(|m| m.decimals)
.map_err(|e| anyhow::anyhow!("unpack mint for decimals: {:?}", e))?,
_ => {
return Ok(to_value(Response {
context: RpcResponseContext { slot, api_version: None },
value: Vec::<Value>::new(),
})?);
}
};
let token_2022_id = spl_token_2022_interface::ID;
let mut holders: Vec<(Pubkey, u64)> = Vec::new();
for (address, account_data) in &svm.accounts_db().inner {
let data = account_data.data();
if data.len() < TOKEN_ACCOUNT_BASE_LEN {
continue;
}
let owner_bytes: [u8; 32] = account_data.owner().as_ref().try_into().unwrap();
let owner_program = Pubkey::new_from_array(owner_bytes);
if owner_program != spl_token::ID && owner_program != token_2022_id {
continue;
}
let Ok(token_account) = TokenAccount::unpack(&data[..TOKEN_ACCOUNT_BASE_LEN])
else {
continue;
};
if token_account.mint != mint {
continue;
}
let addr_bytes: [u8; 32] = address.as_ref().try_into().unwrap();
let addr = Pubkey::new_from_array(addr_bytes);
holders.push((addr, token_account.amount));
}
holders.sort_by(|a, b| b.1.cmp(&a.1));
holders.truncate(MAX_RESULTS);
let value: Vec<Value> = holders
.into_iter()
.map(|(addr, amount)| {
let ui_amount = if decimals == 0 {
amount as f64
} else {
amount as f64 / 10f64.powi(decimals as i32)
};
let ui_amount_string = if decimals == 0 {
amount.to_string()
} else {
format!("{:.*}", decimals as usize, ui_amount)
};
serde_json::json!({
"address": addr.to_string(),
"amount": amount.to_string(),
"decimals": decimals,
"uiAmount": ui_amount,
"uiAmountString": ui_amount_string,
})
})
.collect();
to_value(Response { context: RpcResponseContext { slot, api_version: None }, value })?
}
"getSignatureStatuses" => {
let signatures = params[0]
.as_array()
.ok_or_else(|| anyhow::anyhow!("Expected array of signatures"))?;
let config = params.get(1).and_then(|c| c.as_object());
let _search_transaction_history = config
.and_then(|c| c.get("searchTransactionHistory"))
.and_then(|v| v.as_bool())
.unwrap_or(false);
let mut statuses = Vec::new();
for sig_value in signatures {
let sig_str = sig_value
.as_str()
.ok_or_else(|| anyhow::anyhow!("Expected signature to be a string"))?;
match Signature::from_str(sig_str) {
Ok(sig) => match svm.get_transaction(&sig) {
Some(Ok(_)) => {
let status_obj = serde_json::json!({
"slot": slot,
"confirmations": None::<u64>,
"err": Value::Null,
"status": serde_json::json!({"Ok": null}),
"confirmationStatus": "finalized"
});
statuses.push(to_value(status_obj)?);
}
Some(Err(failed)) => {
let err_value =
serde_json::to_value(&failed.err).unwrap_or(Value::Null);
let status_obj = serde_json::json!({
"slot": slot,
"confirmations": None::<u64>,
"err": err_value,
"status": serde_json::json!({"Err": err_value}),
"confirmationStatus": "finalized"
});
statuses.push(to_value(status_obj)?);
}
None => {
statuses.push(Value::Null);
}
},
Err(_) => {
statuses.push(Value::Null);
}
}
}
to_value(Response {
context: RpcResponseContext { slot, api_version: None },
value: statuses,
})?
}
_ => return Err(anyhow::anyhow!("Method not implemented: {}", method)),
};
Ok(response)
}
pub struct MockRpcSender {
svm: Arc<Mutex<LiteSVM>>,
method_errors: HashMap<String, String>,
call_count: AtomicU64,
error_after: Option<ErrorAfterConfig>,
error_before: Option<ErrorBeforeConfig>,
}
struct ErrorAfterConfig {
threshold: u64,
message: String,
}
struct ErrorBeforeConfig {
threshold: u64,
message: String,
}
impl MockRpcSender {
pub fn new(svm: Arc<Mutex<LiteSVM>>) -> Self {
Self {
svm,
method_errors: HashMap::new(),
call_count: AtomicU64::new(0),
error_after: None,
error_before: None,
}
}
pub fn with_error_on(mut self, method: &str, error_msg: &str) -> Self {
self.method_errors.insert(method.to_string(), error_msg.to_string());
self
}
pub fn with_error_after(mut self, n: u64, error_msg: &str) -> Self {
self.error_after = Some(ErrorAfterConfig { threshold: n, message: error_msg.to_string() });
self
}
pub fn with_error_before(mut self, n: u64, error_msg: &str) -> Self {
self.error_before =
Some(ErrorBeforeConfig { threshold: n, message: error_msg.to_string() });
self
}
pub fn create_rpc_client(self) -> RpcClient {
RpcClient::new_sender(self, RpcClientConfig::default())
}
fn check_error_injection(&self, method: &str) -> Option<String> {
if let Some(msg) = self.method_errors.get(method) {
return Some(msg.clone());
}
if self.error_before.is_some() || self.error_after.is_some() {
let count = self.call_count.fetch_add(1, Ordering::SeqCst);
if let Some(ref config) = self.error_before {
if count < config.threshold {
return Some(config.message.clone());
}
}
if let Some(ref config) = self.error_after {
if count >= config.threshold {
return Some(config.message.clone());
}
}
}
None
}
}
#[async_trait]
impl RpcSender for MockRpcSender {
async fn send(&self, request: RpcRequest, params: Value) -> ClientResult<Value> {
let request_json = request.build_request_json(42, params.clone());
let method = request_json["method"].as_str().unwrap_or_default();
if let Some(error_msg) = self.check_error_injection(method) {
return Err(ClientError::new_with_request(ClientErrorKind::Custom(error_msg), request));
}
let default_params = Vec::new();
let params = request_json["params"].as_array().unwrap_or(&default_params);
let mut svm = self.svm.lock().expect("SVM mutex poisoned");
let response = send(&mut svm, method, params).map_err(|e| {
ClientError::new_with_request(ClientErrorKind::Custom(e.to_string()), request)
})?;
Ok(response)
}
fn get_transport_stats(&self) -> RpcTransportStats {
RpcTransportStats::default()
}
fn url(&self) -> String {
"MockRpcSender".to_string()
}
}
#[cfg(test)]
mod tests {
use solana_sdk::clock::Clock;
use super::*;
fn create_svm() -> Arc<Mutex<LiteSVM>> {
let mut svm = LiteSVM::new();
let mut clock = svm.get_sysvar::<Clock>();
clock.unix_timestamp = 1_000_000;
svm.set_sysvar::<Clock>(&clock);
Arc::new(Mutex::new(svm))
}
#[tokio::test]
async fn test_with_error_on_rejects_targeted_method() {
let svm = create_svm();
let sender = MockRpcSender::new(svm).with_error_on("getAccountInfo", "connection refused");
let result = sender
.send(
RpcRequest::GetAccountInfo,
serde_json::json!(["11111111111111111111111111111111", {"encoding": "base64"}]),
)
.await;
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("connection refused"),
"expected 'connection refused' in error, got: {err_msg}"
);
}
#[tokio::test]
async fn test_with_error_on_allows_other_methods() {
let svm = create_svm();
let sender = MockRpcSender::new(svm).with_error_on("getAccountInfo", "connection refused");
let result = sender.send(RpcRequest::GetVersion, serde_json::json!([])).await;
assert!(result.is_ok(), "expected getVersion to succeed: {result:?}");
}
#[tokio::test]
async fn test_with_error_on_multiple_methods() {
let svm = create_svm();
let sender = MockRpcSender::new(svm)
.with_error_on("getAccountInfo", "connection refused")
.with_error_on("getLatestBlockhash", "node behind");
let r1 = sender
.send(
RpcRequest::GetAccountInfo,
serde_json::json!(["11111111111111111111111111111111", {"encoding": "base64"}]),
)
.await;
assert!(r1.is_err());
assert!(r1.unwrap_err().to_string().contains("connection refused"));
let r2 = sender.send(RpcRequest::GetLatestBlockhash, serde_json::json!([])).await;
assert!(r2.is_err());
assert!(r2.unwrap_err().to_string().contains("node behind"));
}
#[tokio::test]
async fn test_with_error_after_allows_n_calls_then_fails() {
let svm = create_svm();
let sender = MockRpcSender::new(svm).with_error_after(2, "rate limited");
let r1 = sender.send(RpcRequest::GetVersion, serde_json::json!([])).await;
assert!(r1.is_ok(), "call 1 should succeed");
let r2 = sender.send(RpcRequest::GetVersion, serde_json::json!([])).await;
assert!(r2.is_ok(), "call 2 should succeed");
let r3 = sender.send(RpcRequest::GetVersion, serde_json::json!([])).await;
assert!(r3.is_err(), "call 3 should fail");
assert!(r3.unwrap_err().to_string().contains("rate limited"));
let r4 = sender.send(RpcRequest::GetVersion, serde_json::json!([])).await;
assert!(r4.is_err(), "call 4 should fail");
}
#[tokio::test]
async fn test_error_on_takes_precedence_over_error_after() {
let svm = create_svm();
let sender = MockRpcSender::new(svm)
.with_error_on("getAccountInfo", "method blocked")
.with_error_after(100, "rate limited");
let result = sender
.send(
RpcRequest::GetAccountInfo,
serde_json::json!(["11111111111111111111111111111111", {"encoding": "base64"}]),
)
.await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("method blocked"));
}
#[tokio::test]
async fn test_no_error_injection_passes_through() {
let svm = create_svm();
let sender = MockRpcSender::new(svm);
let result = sender.send(RpcRequest::GetVersion, serde_json::json!([])).await;
assert!(result.is_ok(), "default sender should succeed: {result:?}");
}
#[test]
fn test_get_signature_statuses_returns_null_for_unknown_signature() {
let mut svm = LiteSVM::new();
let mut clock = svm.get_sysvar::<Clock>();
clock.unix_timestamp = 1_000_000;
svm.set_sysvar::<Clock>(&clock);
let unknown_sig = Signature::new_unique();
let params = vec![
serde_json::json!([unknown_sig.to_string()]),
serde_json::json!({"searchTransactionHistory": true}),
];
let result = send(&mut svm, "getSignatureStatuses", ¶ms).unwrap();
let statuses = result["value"].as_array().expect("expected value to be an array");
assert_eq!(statuses.len(), 1);
assert!(statuses[0].is_null(), "expected null for unknown signature, got: {}", statuses[0]);
}
#[test]
fn test_get_signature_statuses_returns_null_for_invalid_signature() {
let mut svm = LiteSVM::new();
let mut clock = svm.get_sysvar::<Clock>();
clock.unix_timestamp = 1_000_000;
svm.set_sysvar::<Clock>(&clock);
let params = vec![serde_json::json!(["not-a-valid-signature"]), serde_json::json!({})];
let result = send(&mut svm, "getSignatureStatuses", ¶ms).unwrap();
let statuses = result["value"].as_array().expect("expected value to be an array");
assert_eq!(statuses.len(), 1);
assert!(statuses[0].is_null(), "expected null for invalid signature, got: {}", statuses[0]);
}
fn pack_mint_into_svm(svm: &mut LiteSVM, mint: Pubkey, decimals: u8, owner_program: Pubkey) {
use solana_sdk::program_pack::Pack;
use spl_token::state::Mint;
let mut data = vec![0u8; Mint::LEN];
let state = Mint {
mint_authority: solana_sdk::program_option::COption::None,
supply: 1_000_000_000,
decimals,
is_initialized: true,
freeze_authority: solana_sdk::program_option::COption::None,
};
state.pack_into_slice(&mut data);
svm.set_account(
mint,
Account {
lamports: 1_000_000_000,
data,
owner: owner_program,
executable: false,
rent_epoch: 0,
},
)
.expect("set mint account");
}
fn pack_token_account_into_svm(
svm: &mut LiteSVM,
token_account: Pubkey,
mint: Pubkey,
owner: Pubkey,
amount: u64,
owner_program: Pubkey,
) {
use solana_sdk::program_pack::Pack;
use spl_token::state::{Account as TokenAccount, AccountState};
let mut data = vec![0u8; TokenAccount::LEN];
let state = TokenAccount {
mint,
owner,
amount,
delegate: solana_sdk::program_option::COption::None,
state: AccountState::Initialized,
is_native: solana_sdk::program_option::COption::None,
delegated_amount: 0,
close_authority: solana_sdk::program_option::COption::None,
};
state.pack_into_slice(&mut data);
svm.set_account(
token_account,
Account {
lamports: 1_000_000_000,
data,
owner: owner_program,
executable: false,
rent_epoch: 0,
},
)
.expect("set token account");
}
#[test]
fn test_get_token_largest_accounts_returns_descending_order() {
let mut svm = LiteSVM::new();
let mut clock = svm.get_sysvar::<Clock>();
clock.unix_timestamp = 1_000_000;
svm.set_sysvar::<Clock>(&clock);
let mint = Pubkey::new_unique();
pack_mint_into_svm(&mut svm, mint, 6, spl_token::ID);
let ta_small = Pubkey::new_unique();
let ta_med = Pubkey::new_unique();
let ta_large = Pubkey::new_unique();
let owner = Pubkey::new_unique();
pack_token_account_into_svm(&mut svm, ta_small, mint, owner, 100, spl_token::ID);
pack_token_account_into_svm(&mut svm, ta_med, mint, owner, 200, spl_token::ID);
pack_token_account_into_svm(&mut svm, ta_large, mint, owner, 500, spl_token::ID);
let params = vec![serde_json::json!(mint.to_string()), serde_json::json!({})];
let result = send(&mut svm, "getTokenLargestAccounts", ¶ms).unwrap();
let value = result["value"].as_array().expect("value should be array");
assert_eq!(value.len(), 3, "expected 3 holders, got {}", value.len());
assert_eq!(value[0]["amount"].as_str(), Some("500"), "first should be largest");
assert_eq!(value[1]["amount"].as_str(), Some("200"), "second should be middle");
assert_eq!(value[2]["amount"].as_str(), Some("100"), "third should be smallest");
assert_eq!(value[0]["address"].as_str(), Some(ta_large.to_string().as_str()));
}
#[test]
fn test_get_token_largest_accounts_filters_by_mint() {
let mut svm = LiteSVM::new();
let mut clock = svm.get_sysvar::<Clock>();
clock.unix_timestamp = 1_000_000;
svm.set_sysvar::<Clock>(&clock);
let mint_a = Pubkey::new_unique();
let mint_b = Pubkey::new_unique();
pack_mint_into_svm(&mut svm, mint_a, 6, spl_token::ID);
pack_mint_into_svm(&mut svm, mint_b, 9, spl_token::ID);
let owner = Pubkey::new_unique();
let ta_a = Pubkey::new_unique();
let ta_b = Pubkey::new_unique();
pack_token_account_into_svm(&mut svm, ta_a, mint_a, owner, 111, spl_token::ID);
pack_token_account_into_svm(&mut svm, ta_b, mint_b, owner, 222, spl_token::ID);
let params = vec![serde_json::json!(mint_a.to_string()), serde_json::json!({})];
let result = send(&mut svm, "getTokenLargestAccounts", ¶ms).unwrap();
let value = result["value"].as_array().unwrap();
assert_eq!(value.len(), 1, "should only include accounts for queried mint");
assert_eq!(value[0]["address"].as_str(), Some(ta_a.to_string().as_str()));
assert_eq!(value[0]["amount"].as_str(), Some("111"));
}
#[test]
fn test_get_token_largest_accounts_supports_token_2022() {
let mut svm = LiteSVM::new();
let mut clock = svm.get_sysvar::<Clock>();
clock.unix_timestamp = 1_000_000;
svm.set_sysvar::<Clock>(&clock);
let token_2022_id = spl_token_2022_interface::ID;
let mint = Pubkey::new_unique();
pack_mint_into_svm(&mut svm, mint, 6, token_2022_id);
let owner = Pubkey::new_unique();
let ta = Pubkey::new_unique();
pack_token_account_into_svm(&mut svm, ta, mint, owner, 333, token_2022_id);
let params = vec![serde_json::json!(mint.to_string()), serde_json::json!({})];
let result = send(&mut svm, "getTokenLargestAccounts", ¶ms).unwrap();
let value = result["value"].as_array().unwrap();
assert_eq!(value.len(), 1, "should find Token-2022-owned holders");
assert_eq!(value[0]["address"].as_str(), Some(ta.to_string().as_str()));
assert_eq!(value[0]["amount"].as_str(), Some("333"));
}
#[test]
fn test_get_token_largest_accounts_returns_empty_for_unknown_mint() {
let mut svm = LiteSVM::new();
let mut clock = svm.get_sysvar::<Clock>();
clock.unix_timestamp = 1_000_000;
svm.set_sysvar::<Clock>(&clock);
let unknown_mint = Pubkey::new_unique();
let params = vec![serde_json::json!(unknown_mint.to_string()), serde_json::json!({})];
let result = send(&mut svm, "getTokenLargestAccounts", ¶ms).unwrap();
let value = result["value"].as_array().unwrap();
assert_eq!(value.len(), 0, "unknown mint should return empty list, not error");
}
}