use std::collections::HashMap;
use agave_feature_set::FeatureSet;
use itertools::Itertools;
use litesvm::{
LiteSVM,
types::{FailedTransactionMetadata, SimulatedTransactionInfo, TransactionResult},
};
use solana_account::{Account, AccountSharedData};
use solana_clock::Clock;
use solana_loader_v3_interface::get_program_data_address;
use solana_program_option::COption;
use solana_pubkey::Pubkey;
use solana_slot_hashes::SlotHashes;
#[allow(deprecated)]
use solana_sysvar::recent_blockhashes::RecentBlockhashes;
use solana_transaction::versioned::VersionedTransaction;
use crate::{
error::{SurfpoolError, SurfpoolResult},
storage::{OverlayStorage, Storage, new_kv_store},
surfnet::{GetAccountResult, locker::is_supported_token_program},
};
#[derive(Clone)]
pub struct SurfnetLiteSvm {
pub svm: LiteSVM,
pub db: Option<Box<dyn Storage<String, AccountSharedData>>>,
}
impl SurfnetLiteSvm {
pub fn new() -> Self {
Self {
svm: LiteSVM::new(),
db: None,
}
}
pub fn clone_for_profiling(&self) -> Self {
Self {
svm: self.svm.clone(),
db: self
.db
.as_ref()
.map(|db| OverlayStorage::wrap(db.clone_box())),
}
}
pub fn initialize(
mut self,
feature_set: FeatureSet,
database_url: Option<&str>,
surfnet_id: &str,
) -> SurfpoolResult<Self> {
self.svm = LiteSVM::new()
.with_blockhash_check(false)
.with_sigverify(false)
.with_feature_set(feature_set);
create_native_mint(&mut self);
if let Some(db_url) = database_url {
let db: Box<dyn Storage<String, AccountSharedData>> =
new_kv_store(&Some(db_url), "accounts", surfnet_id)?;
self.db = Some(db);
}
Ok(self)
}
pub fn shutdown(&self) {
if let Some(db) = &self.db {
db.shutdown();
}
}
pub fn reset(&mut self, feature_set: FeatureSet) -> SurfpoolResult<()> {
self.svm = LiteSVM::new()
.with_blockhash_check(false)
.with_sigverify(false)
.with_feature_set(feature_set);
create_native_mint(self);
if let Some(db) = &mut self.db {
db.clear()?;
}
Ok(())
}
#[allow(deprecated)]
pub fn garbage_collect(&mut self, feature_set: FeatureSet) {
if self.db.is_none() {
return;
}
let recent_blockhashes = self.svm.get_sysvar::<RecentBlockhashes>();
let slot_hashes = self.svm.get_sysvar::<SlotHashes>();
let clock = self.svm.get_sysvar::<Clock>();
self.svm = LiteSVM::new()
.with_blockhash_check(false)
.with_sigverify(false)
.with_feature_set(feature_set);
create_native_mint(self);
self.svm.set_sysvar(&recent_blockhashes);
self.svm.set_sysvar(&slot_hashes);
self.svm.set_sysvar(&clock);
}
pub fn apply_feature_config(&mut self, feature_set: FeatureSet) -> &mut Self {
self.svm = LiteSVM::new()
.with_blockhash_check(false)
.with_sigverify(false)
.with_feature_set(feature_set)
.with_builtins()
.with_sysvars()
.with_default_programs();
create_native_mint(self);
self
}
pub fn set_log_bytes_limit(&mut self, limit: Option<usize>) {
self.svm.set_log_bytes_limit(limit);
}
pub fn set_sigverify(&mut self, sigverify: bool) {
self.svm.set_sigverify(sigverify);
}
pub fn with_blockhash_check(mut self, check: bool) -> Self {
self.svm = self.svm.with_blockhash_check(check);
self
}
pub fn get_sysvar<T>(&self) -> T
where
T: solana_sysvar::Sysvar + solana_sysvar_id::SysvarId + serde::de::DeserializeOwned,
{
self.svm.get_sysvar()
}
pub fn set_sysvar<T>(&mut self, sysvar: &T)
where
T: solana_sysvar::Sysvar + solana_sysvar_id::SysvarId + solana_sysvar::SysvarSerialize,
{
self.svm.set_sysvar(sysvar);
}
pub fn expire_blockhash(&mut self) {
self.svm.expire_blockhash();
}
pub fn send_transaction(&mut self, tx: impl Into<VersionedTransaction>) -> TransactionResult {
self.svm.send_transaction(tx)
}
pub fn minimum_balance_for_rent_exemption(&self, data_len: usize) -> u64 {
self.svm.minimum_balance_for_rent_exemption(data_len)
}
pub fn simulate_transaction(
&self,
tx: impl Into<VersionedTransaction>,
) -> Result<SimulatedTransactionInfo, FailedTransactionMetadata> {
self.svm.simulate_transaction(tx)
}
pub fn airdrop_pubkey(&self) -> Pubkey {
self.svm.airdrop_pubkey()
}
pub fn airdrop(&mut self, pubkey: &Pubkey, lamports: u64) -> TransactionResult {
self.svm.airdrop(pubkey, lamports)
}
pub fn get_account_no_db(&self, pubkey: &Pubkey) -> Option<Account> {
self.svm.get_account(pubkey)
}
pub fn get_account(&self, pubkey: &Pubkey) -> SurfpoolResult<Option<Account>> {
if let Some(account) = self.svm.get_account(pubkey) {
return Ok(Some(account));
} else if let Some(db) = &self.db {
return Ok(db.get(&pubkey.to_string())?.map::<Account, _>(Into::into));
}
Ok(None)
}
pub fn get_account_result(&self, pubkey: &Pubkey) -> SurfpoolResult<GetAccountResult> {
if let Some(account) = self.svm.get_account(pubkey) {
return Ok(GetAccountResult::FoundAccount(
*pubkey, account,
false,
));
} else if let Some(db) = &self.db {
let mut result = None;
if let Some(account) = db.get(&pubkey.to_string())?.map::<Account, _>(Into::into) {
if is_supported_token_program(&account.owner) {
if let Ok(token_account) = crate::types::TokenAccount::unpack(&account.data) {
let mint = db.get(&token_account.mint().to_string())?.map(Into::into);
result = Some(GetAccountResult::FoundTokenAccount(
(*pubkey, account.clone()),
(token_account.mint(), mint),
));
};
} else if account.executable {
let program_data_address = get_program_data_address(pubkey);
let program_data = db.get(&program_data_address.to_string())?.map(Into::into);
result = Some(GetAccountResult::FoundProgramAccount(
(*pubkey, account.clone()),
(program_data_address, program_data),
));
}
return Ok(result.unwrap_or(GetAccountResult::FoundAccount(
*pubkey, account,
true,
)));
}
}
Ok(GetAccountResult::None(*pubkey))
}
pub fn set_account(&mut self, pubkey: Pubkey, account: Account) -> SurfpoolResult<()> {
self.set_account_in_db(pubkey, account.clone().into())?;
self.svm.set_account(pubkey, account)?;
Ok(())
}
pub fn delete_account(&mut self, pubkey: &Pubkey) -> SurfpoolResult<()> {
self.delete_account_in_db(pubkey)?;
self.svm
.set_account(*pubkey, Account::default())
.map_err(|e| SurfpoolError::set_account(*pubkey, e))?;
Ok(())
}
pub fn set_account_in_db(
&mut self,
pubkey: Pubkey,
account: AccountSharedData,
) -> SurfpoolResult<()> {
if let Some(db) = &mut self.db {
db.store(pubkey.to_string(), account)?;
}
Ok(())
}
pub fn delete_account_in_db(&mut self, pubkey: &Pubkey) -> SurfpoolResult<()> {
if let Some(db) = &mut self.db {
db.take(&pubkey.to_string())?;
}
Ok(())
}
pub fn get_all_accounts(&self) -> SurfpoolResult<Vec<(Pubkey, AccountSharedData)>> {
let mut accounts = HashMap::new();
if let Some(db) = &self.db {
let db_accounts = db.into_iter()?;
for (key, account) in db_accounts {
let pubkey = Pubkey::from_str_const(&key);
accounts.insert(pubkey, account);
}
}
for (pubkey, account) in self.svm.accounts_db().inner.iter() {
if !accounts.contains_key(pubkey) {
accounts.insert(*pubkey, account.clone());
}
}
Ok(accounts
.into_iter()
.sorted_by(|a, b| a.0.cmp(&b.0))
.collect())
}
}
fn create_native_mint(svm: &mut SurfnetLiteSvm) {
use solana_program_pack::Pack;
use solana_sysvar::rent::Rent;
use spl_token_interface::state::Mint;
let mut data = vec![0; Mint::LEN];
let mint = Mint {
mint_authority: COption::None,
supply: 0,
decimals: spl_token_interface::native_mint::DECIMALS,
is_initialized: true,
freeze_authority: COption::None,
};
Mint::pack(mint, &mut data).unwrap();
let account = Account {
lamports: svm.get_sysvar::<Rent>().minimum_balance(data.len()),
data,
owner: spl_token_interface::ID,
executable: false,
rent_epoch: 0,
};
svm.set_account(spl_token_interface::native_mint::ID, account)
.expect("Failed to create native mint account in SVM");
}