Skip to main content

surfpool_core/surfnet/
svm.rs

1use std::{
2    cmp::max,
3    collections::{BTreeMap, HashMap, HashSet, VecDeque},
4    str::FromStr,
5    sync::Arc,
6    time::SystemTime,
7};
8
9use agave_feature_set::FeatureSet;
10use base64::{Engine, prelude::BASE64_STANDARD};
11use chrono::Utc;
12use convert_case::Casing;
13use crossbeam_channel::{Receiver, Sender, unbounded};
14use litesvm::{
15    LiteSVM,
16    types::{
17        FailedTransactionMetadata, SimulatedTransactionInfo, TransactionMetadata, TransactionResult,
18    },
19};
20use solana_account::{Account, AccountSharedData, ReadableAccount};
21use solana_account_decoder::{
22    UiAccount, UiAccountData, UiAccountEncoding, UiDataSliceConfig, encode_ui_account,
23    parse_account_data::{AccountAdditionalDataV3, ParsedAccount, SplTokenAdditionalDataV2},
24};
25use solana_client::{
26    rpc_client::SerializableTransaction,
27    rpc_config::{RpcAccountInfoConfig, RpcBlockConfig, RpcTransactionLogsFilter},
28    rpc_filter::RpcFilterType,
29    rpc_response::{RpcKeyedAccount, RpcLogsResponse, RpcPerfSample},
30};
31use solana_clock::{Clock, Slot};
32use solana_commitment_config::{CommitmentConfig, CommitmentLevel};
33use solana_epoch_info::EpochInfo;
34use solana_epoch_schedule::EpochSchedule;
35use solana_genesis_config::GenesisConfig;
36use solana_hash::Hash;
37use solana_inflation::Inflation;
38use solana_loader_v3_interface::state::UpgradeableLoaderState;
39use solana_message::{
40    Message, VersionedMessage, inline_nonce::is_advance_nonce_instruction_data, v0::LoadedAddresses,
41};
42use solana_program_option::COption;
43use solana_pubkey::Pubkey;
44use solana_rpc_client_api::response::{SlotInfo, SlotTransactionStats, SlotUpdate};
45use solana_sdk_ids::{bpf_loader, system_program};
46use solana_signature::Signature;
47use solana_slot_hashes::MAX_ENTRIES as MAX_SLOT_HASHES_ENTRIES;
48use solana_system_interface::instruction as system_instruction;
49use solana_transaction::versioned::VersionedTransaction;
50use solana_transaction_error::TransactionError;
51use solana_transaction_status::{TransactionDetails, TransactionStatusMeta, UiConfirmedBlock};
52use spl_token_2022_interface::extension::{
53    BaseStateWithExtensions, StateWithExtensions, interest_bearing_mint::InterestBearingConfig,
54    scaled_ui_amount::ScaledUiAmountConfig,
55};
56use surfpool_types::{
57    AccountChange, AccountProfileState, AccountSnapshot, DEFAULT_PROFILING_MAP_CAPACITY,
58    DEFAULT_SLOT_TIME_MS, ExportSnapshotConfig, ExportSnapshotScope, FifoMap, Idl,
59    OverrideInstance, ProfileResult, RpcProfileDepth, RpcProfileResultConfig,
60    RunbookExecutionStatusReport, SimnetEvent, SvmFeatureConfig, TransactionConfirmationStatus,
61    TransactionStatusEvent, UiAccountChange, UiAccountProfileState, UiProfileResult, VersionedIdl,
62    types::{
63        ComputeUnitsEstimationResult, KeyedProfileResult, UiKeyedProfileResult, UuidOrSignature,
64    },
65};
66use txtx_addon_kit::{
67    indexmap::IndexMap,
68    types::types::{AddonJsonConverter, Value},
69};
70use txtx_addon_network_svm::codec::idl::borsh_encode_value_to_idl_type;
71use txtx_addon_network_svm_types::idl::{
72    parse_bytes_to_value_with_expected_idl_type_def_ty,
73    parse_bytes_to_value_with_expected_idl_type_def_ty_with_leftover_bytes,
74};
75use uuid::Uuid;
76
77use super::{
78    AccountSubscriptionData, BlockHeader, BlockIdentifier, FINALIZATION_SLOT_THRESHOLD,
79    GetAccountResult, GeyserBlockMetadata, GeyserEntryInfo, GeyserEvent, GeyserSlotStatus,
80    ProgramSubscriptionData, SignatureSubscriptionData, SignatureSubscriptionType,
81    SlotsUpdatesSubscriptionData, remote::SurfnetRemoteClient,
82};
83use crate::{
84    error::{AirdropError, SurfpoolError, SurfpoolResult},
85    rpc::utils::convert_transaction_metadata_from_canonical,
86    scenarios::TemplateRegistry,
87    storage::{OverlayStorage, Storage, new_kv_store, new_kv_store_with_default},
88    surfnet::{
89        LogsSubscriptionData, locker::is_supported_token_program, surfnet_lite_svm::SurfnetLiteSvm,
90    },
91    types::{
92        MintAccount, OfflineAccountConfig, SerializableAccountAdditionalData,
93        SurfnetTransactionStatus, SyntheticBlockhash, TokenAccount, TransactionWithStatusMeta,
94    },
95};
96
97lazy_static::lazy_static! {
98    /// Interval (in slots) at which to perform garbage collection on the lite SVM cache.
99    /// About 1 hour at standard 400ms slot time.
100    /// Configurable via SURFPOOL_GARBAGE_COLLECTION_INTERVAL_SLOTS env var.
101    pub static ref GARBAGE_COLLECTION_INTERVAL_SLOTS: u64 = {
102        std::env::var("SURFPOOL_GARBAGE_COLLECTION_INTERVAL_SLOTS")
103            .ok()
104            .and_then(|s| s.parse().ok())
105            .unwrap_or(9_000)
106    };
107
108    /// Interval (in slots) at which to checkpoint the latest slot to storage.
109    /// About 1 minute at standard 400ms slot time (60000ms / 400ms = 150 slots).
110    /// Configurable via SURFPOOL_CHECKPOINT_INTERVAL_SLOTS env var.
111    pub static ref CHECKPOINT_INTERVAL_SLOTS: u64 = {
112        std::env::var("SURFPOOL_CHECKPOINT_INTERVAL_SLOTS")
113            .ok()
114            .and_then(|s| s.parse().ok())
115            .unwrap_or(150)
116    };
117}
118
119/// Helper function to apply an override to a decoded account value using dot notation
120pub fn apply_override_to_decoded_account(
121    decoded_value: &mut Value,
122    path: &str,
123    value: &serde_json::Value,
124) -> SurfpoolResult<()> {
125    let parts: Vec<&str> = path.split('.').collect();
126
127    if parts.is_empty() {
128        return Err(SurfpoolError::internal("Empty path provided for override"));
129    }
130
131    // Navigate to the parent of the target field
132    let mut current = decoded_value;
133    for part in &parts[..parts.len() - 1] {
134        match current {
135            Value::Object(map) => {
136                current = map.get_mut(&part.to_string()).ok_or_else(|| {
137                    SurfpoolError::internal(format!(
138                        "Path segment '{}' not found in decoded account",
139                        part
140                    ))
141                })?;
142            }
143            _ => {
144                return Err(SurfpoolError::internal(format!(
145                    "Cannot navigate through field '{}' - not an object",
146                    part
147                )));
148            }
149        }
150    }
151
152    // Set the final field
153    let final_key = parts[parts.len() - 1];
154    match current {
155        Value::Object(map) => {
156            // Convert serde_json::Value to txtx Value
157            let txtx_value = json_to_txtx_value(value)?;
158            map.insert(final_key.to_string(), txtx_value);
159            Ok(())
160        }
161        _ => Err(SurfpoolError::internal(format!(
162            "Cannot set field '{}' - parent is not an object",
163            final_key
164        ))),
165    }
166}
167
168/// Helper function to convert serde_json::Value to txtx Value
169fn json_to_txtx_value(json: &serde_json::Value) -> SurfpoolResult<Value> {
170    match json {
171        serde_json::Value::Null => Ok(Value::Null),
172        serde_json::Value::Bool(b) => Ok(Value::Bool(*b)),
173        serde_json::Value::Number(n) => {
174            if let Some(i) = n.as_i64() {
175                Ok(Value::Integer(i as i128))
176            } else if let Some(u) = n.as_u64() {
177                Ok(Value::Integer(u as i128))
178            } else if let Some(f) = n.as_f64() {
179                Ok(Value::Float(f))
180            } else {
181                Err(SurfpoolError::internal(format!(
182                    "Unable to convert number: {}",
183                    n
184                )))
185            }
186        }
187        serde_json::Value::String(s) => Ok(Value::String(s.clone())),
188        serde_json::Value::Array(arr) => {
189            let txtx_arr: Result<Vec<Value>, _> = arr.iter().map(json_to_txtx_value).collect();
190            Ok(Value::Array(Box::new(txtx_arr?)))
191        }
192        serde_json::Value::Object(obj) => {
193            let mut txtx_obj = IndexMap::new();
194            for (k, v) in obj.iter() {
195                txtx_obj.insert(k.clone(), json_to_txtx_value(v)?);
196            }
197            Ok(Value::Object(txtx_obj))
198        }
199    }
200}
201
202pub type AccountOwner = Pubkey;
203
204#[allow(deprecated)]
205use solana_sysvar::recent_blockhashes::MAX_ENTRIES;
206
207#[allow(deprecated)]
208pub const MAX_RECENT_BLOCKHASHES_STANDARD: usize = MAX_ENTRIES;
209
210pub fn get_txtx_value_json_converters() -> Vec<AddonJsonConverter<'static>> {
211    vec![
212        Box::new(move |value: &txtx_addon_kit::types::types::Value| {
213            txtx_addon_network_svm_types::SvmValue::to_json(value)
214        }) as AddonJsonConverter<'static>,
215    ]
216}
217
218const DEFAULT_LOG_BYTES_LIMIT: Option<usize> = Some(10_000);
219
220#[derive(Debug, Clone)]
221pub struct SurfnetSvmConfig {
222    pub surfnet_id: String,
223    pub feature_config: SvmFeatureConfig,
224    pub slot_time: u64,
225    pub instruction_profiling_enabled: bool,
226    pub max_profiles: usize,
227    pub log_bytes_limit: Option<usize>,
228    pub skip_blockhash_check: bool,
229}
230
231impl Default for SurfnetSvmConfig {
232    fn default() -> Self {
233        Self {
234            surfnet_id: "default".to_string(),
235            feature_config: SvmFeatureConfig::default(),
236            slot_time: DEFAULT_SLOT_TIME_MS,
237            instruction_profiling_enabled: true,
238            max_profiles: DEFAULT_PROFILING_MAP_CAPACITY,
239            log_bytes_limit: DEFAULT_LOG_BYTES_LIMIT,
240            skip_blockhash_check: false,
241        }
242    }
243}
244
245/// `SurfnetSvm` provides a lightweight Solana Virtual Machine (SVM) for testing and simulation.
246///
247/// It supports a local in-memory blockchain state,
248/// remote RPC connections, transaction processing, and account management.
249///
250/// It also exposes channels to listen for simulation events (`SimnetEvent`) and Geyser plugin events (`GeyserEvent`).
251#[derive(Clone)]
252pub struct SurfnetSvm {
253    pub inner: SurfnetLiteSvm,
254    pub remote_rpc_url: Option<String>,
255    pub chain_tip: BlockIdentifier,
256    pub blocks: Box<dyn Storage<u64, BlockHeader>>,
257    pub transactions: Box<dyn Storage<String, SurfnetTransactionStatus>>,
258    pub jito_bundles: Box<dyn Storage<String, Vec<String>>>,
259    pub transactions_queued_for_confirmation: VecDeque<(
260        VersionedTransaction,
261        Sender<TransactionStatusEvent>,
262        Option<TransactionError>,
263    )>,
264    pub transactions_queued_for_finalization: VecDeque<(
265        Slot,
266        VersionedTransaction,
267        Sender<TransactionStatusEvent>,
268        Option<TransactionError>,
269    )>,
270    pub perf_samples: VecDeque<RpcPerfSample>,
271    pub transactions_processed: u64,
272    pub latest_epoch_info: EpochInfo,
273    pub simnet_events_tx: Sender<SimnetEvent>,
274    pub geyser_events_tx: Sender<GeyserEvent>,
275    pub signature_subscriptions: HashMap<Signature, Vec<SignatureSubscriptionData>>,
276    pub account_subscriptions: AccountSubscriptionData,
277    pub program_subscriptions: ProgramSubscriptionData,
278    pub slot_subscriptions: Vec<Sender<SlotInfo>>,
279    /// Senders fed by [`Self::subscribe_for_slots_updates`]. Each sender
280    /// delivers tagged `SlotUpdate` events (the wire format consumed by
281    /// `slotsUpdatesSubscribe` clients).
282    pub slots_updates_subscriptions: Vec<SlotsUpdatesSubscriptionData>,
283    pub profile_tag_map: Box<dyn Storage<String, Vec<UuidOrSignature>>>,
284    pub simulated_transaction_profiles: Box<dyn Storage<String, KeyedProfileResult>>,
285    pub executed_transaction_profiles: Box<dyn Storage<String, KeyedProfileResult>>,
286    pub logs_subscriptions: Vec<LogsSubscriptionData>,
287    pub snapshot_subscriptions: Vec<super::SnapshotSubscriptionData>,
288    pub updated_at: u64,
289    pub slot_time: u64,
290    pub start_time: SystemTime,
291    pub accounts_by_owner: Box<dyn Storage<String, Vec<String>>>,
292    pub account_associated_data: Box<dyn Storage<String, SerializableAccountAdditionalData>>,
293    pub token_accounts: Box<dyn Storage<String, TokenAccount>>,
294    pub token_mints: Box<dyn Storage<String, MintAccount>>,
295    pub token_accounts_by_owner: Box<dyn Storage<String, Vec<String>>>,
296    pub token_accounts_by_delegate: Box<dyn Storage<String, Vec<String>>>,
297    pub token_accounts_by_mint: Box<dyn Storage<String, Vec<String>>>,
298    pub total_supply: u64,
299    pub circulating_supply: u64,
300    pub non_circulating_supply: u64,
301    pub non_circulating_accounts: Vec<String>,
302    pub genesis_config: GenesisConfig,
303    pub inflation: Inflation,
304    /// A global monotonically increasing atomic number, which can be used to tell the order of the account update.
305    /// For example, when an account is updated in the same slot multiple times,
306    /// the update with higher write_version should supersede the one with lower write_version.
307    pub write_version: u64,
308    pub registered_idls: Box<dyn Storage<String, Vec<VersionedIdl>>>,
309    pub feature_set: FeatureSet,
310    pub instruction_profiling_enabled: bool,
311    pub max_profiles: usize,
312    pub skip_blockhash_check: bool,
313    pub runbook_executions: Vec<RunbookExecutionStatusReport>,
314    pub account_update_slots: HashMap<Pubkey, Slot>,
315    pub streamed_accounts: Box<dyn Storage<String, bool>>,
316    pub recent_blockhashes: VecDeque<(SyntheticBlockhash, i64)>,
317    pub scheduled_overrides: Box<dyn Storage<u64, Vec<OverrideInstance>>>,
318    /// Tracks accounts that should not be downloaded from the remote RPC.
319    /// This includes accounts explicitly closed locally and accounts marked offline via cheatcodes.
320    /// The key is the account pubkey as a string. If `include_owned_accounts` is true,
321    /// accounts owned by this pubkey are also marked offline and excluded from remote download.
322    pub offline_accounts: Box<dyn Storage<String, OfflineAccountConfig>>,
323    /// The slot at which this surfnet instance started (may be non-zero when connected to remote).
324    /// Used as the lower bound for block reconstruction.
325    pub genesis_slot: Slot,
326    /// The `updated_at` timestamp when this surfnet started at `genesis_slot`.
327    /// Used to reconstruct block_time: genesis_updated_at + ((slot - genesis_slot) * slot_time)
328    pub genesis_updated_at: u64,
329    /// Storage for persisting the latest slot checkpoint.
330    /// Used for recovery on restart with sparse block storage.
331    pub slot_checkpoint: Box<dyn Storage<String, u64>>,
332    /// Tracks the slot at which we last persisted the checkpoint.
333    pub last_checkpoint_slot: u64,
334}
335
336/// Add `pubkey_str` to the pubkey-list at `key`, creating the entry when absent
337/// and deduplicating on insert. The shared-pubkey indexes (`accounts_by_owner`,
338/// `token_accounts_by_owner`, `token_accounts_by_mint`,
339/// `token_accounts_by_delegate`) map one pubkey-string to the list of account
340/// pubkey-strings that share the indexed trait; the `contains` guard prevents
341/// double-registration when `update_account_registries` is called on an
342/// unchanged account.
343fn add_pubkey_to_index(
344    index: &mut Box<dyn Storage<String, Vec<String>>>,
345    key: String,
346    pubkey_str: &str,
347) -> SurfpoolResult<()> {
348    let mut accounts = index.get(&key).ok().flatten().unwrap_or_default();
349    if !accounts.iter().any(|pk| pk == pubkey_str) {
350        accounts.push(pubkey_str.to_string());
351        index.store(key, accounts)?;
352    }
353    Ok(())
354}
355
356/// Remove `pubkey_str` from the pubkey-list at `key`. When the list becomes
357/// empty the entry is taken rather than stored, so downstream `keys()`
358/// iterations don't surface empty buckets.
359fn remove_pubkey_from_index(
360    index: &mut Box<dyn Storage<String, Vec<String>>>,
361    key: &str,
362    pubkey_str: &str,
363) -> SurfpoolResult<()> {
364    let key_owned = key.to_string();
365    if let Some(mut accounts) = index.get(&key_owned).ok().flatten() {
366        accounts.retain(|pk| pk != pubkey_str);
367        if accounts.is_empty() {
368            index.take(&key_owned)?;
369        } else {
370            index.store(key_owned, accounts)?;
371        }
372    }
373    Ok(())
374}
375
376/// A bundle execution sandbox: an isolated [`SurfnetSvm`] clone whose buffered event channels
377/// can be drained on bundle commit to replay events onto the original VM. Construct via
378/// [`SurfnetSvm::clone_for_bundle_sandbox`] and consume via [`SurfnetSvm::commit_sandbox`] on
379/// success or simply drop on failure to discard all in-progress state.
380pub struct BundleSandbox {
381    pub svm: SurfnetSvm,
382    pub geyser_rx: Receiver<GeyserEvent>,
383    pub simnet_rx: Receiver<SimnetEvent>,
384}
385
386/// Generic helper: drain the overlay state of `sandbox_storage` (which must be an
387/// `OverlayStorage`-wrapped storage as produced by `clone_for_profiling`) and apply each
388/// write/delete to `target_storage`. If the sandbox overlay had been logically cleared
389/// (via `clear()`), the target is cleared first.
390///
391/// If `sandbox_storage` is not overlay-style (i.e. `as_overlay()` returns `None`), this is a
392/// no-op — that should never happen for fields constructed by `clone_for_profiling`, which
393/// always wraps every storage field with `OverlayStorage`.
394fn commit_overlay_storage<K, V>(
395    sandbox_storage: &dyn Storage<K, V>,
396    target_storage: &mut dyn Storage<K, V>,
397) -> SurfpoolResult<()> {
398    let Some(overlay) = sandbox_storage.as_overlay() else {
399        return Ok(());
400    };
401    let delta = overlay.extract_overlay()?;
402    if delta.base_cleared {
403        target_storage.clear()?;
404    }
405    for k in delta.deletes {
406        target_storage.take(&k)?;
407    }
408    for (k, v) in delta.writes {
409        target_storage.store(k, v)?;
410    }
411    Ok(())
412}
413
414/// Composes a [`FeatureSet`] from a user-supplied [`SvmFeatureConfig`].
415///
416/// The starting baseline is LiteSVM's mainnet-beta feature set (see
417/// [`LiteSVM::mainnet_feature_set`]); features listed in `config.enable` are
418/// then activated on top, and features in `config.disable` are deactivated.
419/// This is the single source of truth for the "mainnet defaults + deltas"
420/// semantic promised by `SvmFeatureConfig`.
421fn compose_feature_set(config: &SvmFeatureConfig) -> FeatureSet {
422    let mut feature_set = LiteSVM::mainnet_feature_set();
423    for pubkey in &config.enable {
424        debug!("Activating feature {}", pubkey);
425        feature_set.activate(pubkey, 0);
426    }
427    for pubkey in &config.disable {
428        debug!("Deactivating feature {}", pubkey);
429        feature_set.deactivate(pubkey);
430    }
431    feature_set
432}
433
434fn synthetic_chain_tip_at_index(index: u64) -> BlockIdentifier {
435    if index == 0 {
436        return BlockIdentifier::zero();
437    }
438
439    BlockIdentifier {
440        index,
441        hash: SyntheticBlockhash::new(index - 1).to_string(),
442    }
443}
444
445fn synthetic_blockhash_for_slot(slot: Slot, genesis_slot: Slot) -> SyntheticBlockhash {
446    if slot >= genesis_slot {
447        return SyntheticBlockhash::new(slot - genesis_slot);
448    }
449
450    // Pre-genesis slot hashes only exist to cover the finalized warmup window.
451    // Keep them deterministic and distinct from local chain-index hashes.
452    SyntheticBlockhash::new(u64::MAX - (genesis_slot - slot - 1))
453}
454
455impl SurfnetSvm {
456    pub fn default() -> (Self, Receiver<SimnetEvent>, Receiver<GeyserEvent>) {
457        Self::new(SurfnetSvmConfig::default()).unwrap()
458    }
459
460    pub fn new(
461        config: SurfnetSvmConfig,
462    ) -> SurfpoolResult<(Self, Receiver<SimnetEvent>, Receiver<GeyserEvent>)> {
463        Self::build(None, config)
464    }
465
466    pub fn new_with_db(
467        database_url: Option<&str>,
468        config: SurfnetSvmConfig,
469    ) -> SurfpoolResult<(Self, Receiver<SimnetEvent>, Receiver<GeyserEvent>)> {
470        Self::build(database_url, config)
471    }
472
473    /// Explicitly shutdown the SVM, performing cleanup like WAL checkpoint for SQLite.
474    /// This should be called before the application exits to ensure data is persisted.
475    pub fn shutdown(&self) {
476        self.inner.shutdown();
477        self.blocks.shutdown();
478        self.transactions.shutdown();
479        self.jito_bundles.shutdown();
480        self.token_accounts.shutdown();
481        self.token_mints.shutdown();
482        self.accounts_by_owner.shutdown();
483        self.token_accounts_by_owner.shutdown();
484        self.token_accounts_by_delegate.shutdown();
485        self.token_accounts_by_mint.shutdown();
486        self.streamed_accounts.shutdown();
487        self.scheduled_overrides.shutdown();
488        self.registered_idls.shutdown();
489        self.profile_tag_map.shutdown();
490        self.simulated_transaction_profiles.shutdown();
491        self.executed_transaction_profiles.shutdown();
492        self.account_associated_data.shutdown();
493        self.offline_accounts.shutdown();
494        self.slot_checkpoint.shutdown();
495    }
496
497    /// Creates a clone of the SVM with overlay storage wrappers for all database-backed fields.
498    /// This allows profiling transactions without affecting the underlying database.
499    /// All storage writes are buffered in memory and discarded when the clone is dropped.
500    ///
501    /// Subscription registries (`signature_subscriptions`, `account_subscriptions`, etc)
502    /// are all replaced with empty containers on the sandbox so that any notification
503    /// dispatched during sandbox execution cannot reach live WebSocket clients. Although
504    /// `HashMap::clone()` of the original maps is a deep copy of the container, each contained
505    /// `crossbeam_channel::Sender` is a handle to the same channel held by live subscriber
506    /// receivers — re-firing them from the sandbox would leak notifications even on bundle
507    /// abort. Emptying the containers closes that leak.
508    ///
509    /// Event channels (`simnet_events_tx`, `geyser_events_tx`) are replaced with internal
510    /// buffered channels whose receivers are kept on the sandbox under `sandbox_simnet_events_rx`
511    /// and `sandbox_geyser_events_rx`. Bundle commit code can drain these to replay the events
512    /// on the original VM's channels.
513    pub fn clone_for_profiling(&self) -> Self {
514        let (dummy_simnet_tx, _) = crossbeam_channel::bounded(1);
515        let (dummy_geyser_tx, _) = crossbeam_channel::bounded(1);
516
517        Self {
518            inner: self.inner.clone_for_profiling(),
519            remote_rpc_url: self.remote_rpc_url.clone(),
520            chain_tip: self.chain_tip.clone(),
521
522            // Wrap all storage fields with OverlayStorage
523            blocks: OverlayStorage::wrap(self.blocks.clone_box()),
524            transactions: OverlayStorage::wrap(self.transactions.clone_box()),
525            jito_bundles: OverlayStorage::wrap(self.jito_bundles.clone_box()),
526            profile_tag_map: OverlayStorage::wrap(self.profile_tag_map.clone_box()),
527            simulated_transaction_profiles: OverlayStorage::wrap(
528                self.simulated_transaction_profiles.clone_box(),
529            ),
530            executed_transaction_profiles: OverlayStorage::wrap(
531                self.executed_transaction_profiles.clone_box(),
532            ),
533            accounts_by_owner: OverlayStorage::wrap(self.accounts_by_owner.clone_box()),
534            account_associated_data: OverlayStorage::wrap(self.account_associated_data.clone_box()),
535            token_accounts: OverlayStorage::wrap(self.token_accounts.clone_box()),
536            token_mints: OverlayStorage::wrap(self.token_mints.clone_box()),
537            token_accounts_by_owner: OverlayStorage::wrap(self.token_accounts_by_owner.clone_box()),
538            token_accounts_by_delegate: OverlayStorage::wrap(
539                self.token_accounts_by_delegate.clone_box(),
540            ),
541            token_accounts_by_mint: OverlayStorage::wrap(self.token_accounts_by_mint.clone_box()),
542            registered_idls: OverlayStorage::wrap(self.registered_idls.clone_box()),
543            streamed_accounts: OverlayStorage::wrap(self.streamed_accounts.clone_box()),
544            scheduled_overrides: OverlayStorage::wrap(self.scheduled_overrides.clone_box()),
545
546            // Clone non-storage fields normally
547            transactions_queued_for_confirmation: self.transactions_queued_for_confirmation.clone(),
548            transactions_queued_for_finalization: self.transactions_queued_for_finalization.clone(),
549            perf_samples: self.perf_samples.clone(),
550            transactions_processed: self.transactions_processed,
551            latest_epoch_info: self.latest_epoch_info.clone(),
552
553            // Use dummy channels to prevent event propagation during profiling
554            simnet_events_tx: dummy_simnet_tx,
555            geyser_events_tx: dummy_geyser_tx,
556
557            signature_subscriptions: HashMap::new(),
558            account_subscriptions: HashMap::new(),
559            program_subscriptions: HashMap::new(),
560            // All subscription containers are emptied on the sandbox so that no notification
561            // dispatched during sandbox execution can reach live WebSocket subscribers. The three
562            // map-based containers above contain `crossbeam_channel::Sender` handles whose
563            // `clone()` produces a producer for the same underlying channel held by the live
564            // subscriber's receiver — emptying the map is what actually prevents the leak.
565            slot_subscriptions: Vec::new(),
566            slots_updates_subscriptions: Vec::new(),
567            logs_subscriptions: Vec::new(),
568            snapshot_subscriptions: Vec::new(),
569
570            updated_at: self.updated_at,
571            slot_time: self.slot_time,
572            start_time: self.start_time,
573
574            total_supply: self.total_supply,
575            circulating_supply: self.circulating_supply,
576            non_circulating_supply: self.non_circulating_supply,
577            non_circulating_accounts: self.non_circulating_accounts.clone(),
578            genesis_config: self.genesis_config.clone(),
579            inflation: self.inflation,
580            write_version: self.write_version,
581            feature_set: self.feature_set.clone(),
582            instruction_profiling_enabled: self.instruction_profiling_enabled,
583            max_profiles: self.max_profiles,
584            skip_blockhash_check: self.skip_blockhash_check,
585            runbook_executions: self.runbook_executions.clone(),
586            account_update_slots: self.account_update_slots.clone(),
587            recent_blockhashes: self.recent_blockhashes.clone(),
588            offline_accounts: OverlayStorage::wrap(self.offline_accounts.clone_box()),
589            genesis_slot: self.genesis_slot,
590            genesis_updated_at: self.genesis_updated_at,
591            slot_checkpoint: OverlayStorage::wrap(self.slot_checkpoint.clone_box()),
592            last_checkpoint_slot: self.last_checkpoint_slot,
593        }
594    }
595
596    pub(crate) fn default_epoch_schedule() -> EpochSchedule {
597        EpochSchedule::without_warmup()
598    }
599
600    pub(crate) fn default_epoch_info(epoch_schedule: &EpochSchedule) -> EpochInfo {
601        EpochInfo {
602            epoch: 0,
603            slot_index: 0,
604            slots_in_epoch: epoch_schedule.slots_per_epoch,
605            absolute_slot: FINALIZATION_SLOT_THRESHOLD,
606            block_height: FINALIZATION_SLOT_THRESHOLD,
607            transaction_count: None,
608        }
609    }
610
611    fn register_builtin_template_idls(&mut self) {
612        let registry = TemplateRegistry::new();
613        for (_, template) in registry.templates.into_iter() {
614            let _ = self.register_idl(template.idl, None);
615        }
616    }
617
618    /// Creates a sandbox tailored for atomic Jito-style bundle execution. Behavior matches
619    /// [`Self::clone_for_profiling`] (all storage fields are overlay-wrapped; subscription
620    /// containers are emptied so live WS subscribers cannot be notified from the sandbox),
621    /// but the `simnet_events_tx` and `geyser_events_tx` channels are replaced with
622    /// **unbounded buffered** channels whose receivers are returned alongside the sandbox.
623    /// The atomic-commit phase ([`Self::commit_sandbox`]) drains those receivers to replay
624    /// the captured events onto the original VM's real event channels on bundle success.
625    ///
626    /// On bundle failure, simply dropping the returned [`BundleSandbox`] discards every
627    /// buffered event, every overlay write, and the cloned `LiteSVM` state — the original
628    /// VM is left byte-identical to its pre-bundle state.
629    pub fn clone_for_bundle_sandbox(&self) -> BundleSandbox {
630        let mut svm = self.clone_for_profiling();
631        let (geyser_tx, geyser_rx) = crossbeam_channel::unbounded();
632        let (simnet_tx, simnet_rx) = crossbeam_channel::unbounded();
633        svm.geyser_events_tx = geyser_tx;
634        svm.simnet_events_tx = simnet_tx;
635        BundleSandbox {
636            svm,
637            geyser_rx,
638            simnet_rx,
639        }
640    }
641
642    /// Atomically commit the outcome of a fully-successful bundle sandbox onto `self`.
643    ///
644    /// This is the second half of the atomic Jito bundle pipeline. It must be invoked only
645    /// after every transaction in the bundle succeeded inside the sandbox. The caller must
646    /// hold an exclusive writer guard on `self`'s `SurfnetSvmLocker` so that no other RPC
647    /// path can observe a half-committed state.
648    ///
649    /// Order of operations is **state mutations first, side-effects second**:
650    ///   1. Drain every overlay-wrapped storage field from the sandbox onto `self`'s
651    ///      corresponding underlying storage. This is the first moment any SQLite/Postgres
652    ///      handle is touched on behalf of the bundle.
653    ///   2. Move the sandbox's `LiteSVM` (the in-memory accounts DB) onto `self`, replacing
654    ///      `self.inner.svm` with the post-bundle account state.
655    ///   3. Drain the sandbox's account-DB overlay (`inner.db`) onto `self.inner.db` so any
656    ///      SQLite-backed account persistence reflects the bundle's mutations.
657    ///   4. Pull forward counters (`write_version`, `transactions_processed`), per-account
658    ///      update slots, pending confirmation/finalization queues, perf samples, and the
659    ///      recent-blockhash deque from the sandbox.
660    ///   5. Drain the sandbox's buffered geyser events; replay each onto `self.geyser_events_tx`.
661    ///      For each `UpdateAccount` event, also fire `notify_account_subscribers` /
662    ///      `notify_program_subscribers` on `self` (the sandbox's registries were emptied,
663    ///      so those notifications could not have been delivered during sandbox execution).
664    ///   6. Drain the sandbox's buffered simnet events; replay each onto `self.simnet_events_tx`.
665    ///   7. For each transaction committed by the bundle, fire signature and logs subscribers
666    ///      at `Processed` commitment on `self`'s subscription registries, and send a
667    ///      `TransactionStatusEvent::Success(Processed)` on the per-tx status channel that
668    ///      is now part of `self.transactions_queued_for_confirmation` (so the existing
669    ///      confirmation/finalization runloop will promote bundle txs through the commitment
670    ///      ladder identically to txs submitted via `sendTransaction`).
671    ///
672    /// Returns the ordered list of signatures committed by the bundle.
673    pub fn commit_sandbox(
674        &mut self,
675        sandbox: BundleSandbox,
676        bundle_status_tx: Sender<TransactionStatusEvent>,
677    ) -> SurfpoolResult<Vec<Signature>> {
678        let BundleSandbox {
679            mut svm,
680            geyser_rx,
681            simnet_rx,
682        } = sandbox;
683
684        // 1. Drain all overlay storages onto self's real storages.
685        commit_overlay_storage(svm.blocks.as_ref(), self.blocks.as_mut())?;
686        commit_overlay_storage(svm.transactions.as_ref(), self.transactions.as_mut())?;
687        commit_overlay_storage(svm.profile_tag_map.as_ref(), self.profile_tag_map.as_mut())?;
688        commit_overlay_storage(
689            svm.simulated_transaction_profiles.as_ref(),
690            self.simulated_transaction_profiles.as_mut(),
691        )?;
692        commit_overlay_storage(
693            svm.executed_transaction_profiles.as_ref(),
694            self.executed_transaction_profiles.as_mut(),
695        )?;
696        commit_overlay_storage(
697            svm.accounts_by_owner.as_ref(),
698            self.accounts_by_owner.as_mut(),
699        )?;
700        commit_overlay_storage(
701            svm.account_associated_data.as_ref(),
702            self.account_associated_data.as_mut(),
703        )?;
704        commit_overlay_storage(svm.token_accounts.as_ref(), self.token_accounts.as_mut())?;
705        commit_overlay_storage(svm.token_mints.as_ref(), self.token_mints.as_mut())?;
706        commit_overlay_storage(
707            svm.token_accounts_by_owner.as_ref(),
708            self.token_accounts_by_owner.as_mut(),
709        )?;
710        commit_overlay_storage(
711            svm.token_accounts_by_delegate.as_ref(),
712            self.token_accounts_by_delegate.as_mut(),
713        )?;
714        commit_overlay_storage(
715            svm.token_accounts_by_mint.as_ref(),
716            self.token_accounts_by_mint.as_mut(),
717        )?;
718        commit_overlay_storage(svm.registered_idls.as_ref(), self.registered_idls.as_mut())?;
719        commit_overlay_storage(
720            svm.streamed_accounts.as_ref(),
721            self.streamed_accounts.as_mut(),
722        )?;
723        commit_overlay_storage(
724            svm.scheduled_overrides.as_ref(),
725            self.scheduled_overrides.as_mut(),
726        )?;
727        commit_overlay_storage(
728            svm.offline_accounts.as_ref(),
729            self.offline_accounts.as_mut(),
730        )?;
731        commit_overlay_storage(svm.slot_checkpoint.as_ref(), self.slot_checkpoint.as_mut())?;
732
733        // 2. Move the sandbox's executed LiteSVM accounts state onto self.
734        std::mem::swap(&mut self.inner.svm, &mut svm.inner.svm);
735
736        // 3. Drain sandbox's account-DB overlay onto self.inner.db.
737        if let (Some(sandbox_db), Some(target_db)) = (svm.inner.db.as_ref(), self.inner.db.as_mut())
738        {
739            commit_overlay_storage(sandbox_db.as_ref(), target_db.as_mut())?;
740        }
741
742        // 4. Counter/version/queue state.
743        self.transactions_processed = svm.transactions_processed;
744        self.write_version = svm.write_version;
745        for (k, v) in svm.account_update_slots.drain() {
746            self.account_update_slots.insert(k, v);
747        }
748        self.perf_samples = svm.perf_samples.clone();
749        self.recent_blockhashes = svm.recent_blockhashes.clone();
750
751        // Push sandbox's queued txs onto self's queues, rewriting the per-tx status channel
752        // to the bundle's status channel so the runloop's Confirmed/Finalized promotions
753        // flow through a single channel (the caller drops the receiver).
754        let mut signatures = Vec::new();
755        for (tx, _sandbox_status_tx, err) in svm.transactions_queued_for_confirmation.drain(..) {
756            signatures.push(tx.signatures[0]);
757            self.transactions_queued_for_confirmation.push_back((
758                tx,
759                bundle_status_tx.clone(),
760                err,
761            ));
762        }
763        for (slot, tx, _sandbox_status_tx, err) in
764            svm.transactions_queued_for_finalization.drain(..)
765        {
766            self.transactions_queued_for_finalization.push_back((
767                slot,
768                tx,
769                bundle_status_tx.clone(),
770                err,
771            ));
772        }
773
774        // 5. Drain buffered geyser events; replay onto self's real channel; for each
775        //    UpdateAccount, also fire account/program subscribers on self's registries.
776        while let Ok(event) = geyser_rx.try_recv() {
777            if let GeyserEvent::UpdateAccount(update) = &event {
778                self.notify_account_subscribers(&update.pubkey, &update.account);
779                self.notify_program_subscribers(&update.pubkey, &update.account);
780            }
781            let _ = self.geyser_events_tx.send(event);
782        }
783
784        // 6. Drain buffered simnet events; replay onto self's real channel.
785        while let Ok(event) = simnet_rx.try_recv() {
786            let _ = self.simnet_events_tx.try_send(event);
787        }
788
789        // 7. Fire signature/logs subscribers and Success acks for each committed tx.
790        //    Use the now-committed `self.transactions` storage as the source of err/logs.
791        let slot = self.get_latest_absolute_slot();
792        for sig in &signatures {
793            let (err, logs) = match self.transactions.get(&sig.to_string()).ok().flatten() {
794                Some(SurfnetTransactionStatus::Processed(boxed)) => {
795                    let (meta, _mutated) = boxed.as_ref();
796                    let err = meta.meta.status.clone().err();
797                    let logs = meta.meta.log_messages.clone().unwrap_or_default();
798                    (err, logs)
799                }
800                _ => (None, Vec::new()),
801            };
802            self.notify_signature_subscribers(
803                SignatureSubscriptionType::processed(),
804                sig,
805                slot,
806                err.clone(),
807            );
808            self.notify_logs_subscribers(sig, err, logs, CommitmentLevel::Processed);
809            let _ = bundle_status_tx.try_send(TransactionStatusEvent::Success(
810                TransactionConfirmationStatus::Processed,
811            ));
812        }
813
814        Ok(signatures)
815    }
816
817    /// Creates a new instance of `SurfnetSvm`.
818    ///
819    /// Returns a tuple containing the SVM instance, a receiver for simulation events, and a receiver for Geyser plugin events.
820    fn build(
821        database_url: Option<&str>,
822        config: SurfnetSvmConfig,
823    ) -> SurfpoolResult<(Self, Receiver<SimnetEvent>, Receiver<GeyserEvent>)> {
824        let (simnet_events_tx, simnet_events_rx) = crossbeam_channel::bounded(1024);
825        let (geyser_events_tx, geyser_events_rx) = crossbeam_channel::bounded(1024);
826        let surfnet_id = config.surfnet_id;
827
828        // Compose the final feature set up front (mainnet baseline +
829        // config.enable - config.disable) so that the inner LiteSVM is
830        // constructed exactly once, with the correct features and feature
831        // accounts loaded. See `compose_feature_set` for the composition rules.
832        let feature_set = compose_feature_set(&config.feature_config);
833        let inner = SurfnetLiteSvm::new(database_url, &surfnet_id, feature_set.clone())?;
834
835        let native_mint_account = inner
836            .get_account(&spl_token_interface::native_mint::ID)?
837            .unwrap();
838
839        let native_mint_associated_data = {
840            let mint = StateWithExtensions::<spl_token_2022_interface::state::Mint>::unpack(
841                &native_mint_account.data,
842            )
843            .unwrap();
844            let unix_timestamp = inner.get_sysvar::<Clock>().unix_timestamp;
845            let interest_bearing_config = mint
846                .get_extension::<InterestBearingConfig>()
847                .map(|x| (*x, unix_timestamp))
848                .ok();
849            let scaled_ui_amount_config = mint
850                .get_extension::<ScaledUiAmountConfig>()
851                .map(|x| (*x, unix_timestamp))
852                .ok();
853            AccountAdditionalDataV3 {
854                spl_token_additional_data: Some(SplTokenAdditionalDataV2 {
855                    decimals: mint.base.decimals,
856                    interest_bearing_config,
857                    scaled_ui_amount_config,
858                }),
859            }
860        };
861        let parsed_mint_account = MintAccount::unpack(&native_mint_account.data).unwrap();
862
863        // Load native mint into owned account and token mint indexes
864        let mut accounts_by_owner_db: Box<dyn Storage<String, Vec<String>>> =
865            new_kv_store(&database_url, "accounts_by_owner", &surfnet_id)?;
866        accounts_by_owner_db.store(
867            native_mint_account.owner.to_string(),
868            vec![spl_token_interface::native_mint::ID.to_string()],
869        )?;
870        let blocks_db = new_kv_store(&database_url, "blocks", &surfnet_id)?;
871        let transactions_db = new_kv_store(&database_url, "transactions", &surfnet_id)?;
872        let jito_bundles_db = new_kv_store(&database_url, "jito_bundles", &surfnet_id)?;
873        let token_accounts_db = new_kv_store(&database_url, "token_accounts", &surfnet_id)?;
874        let mut token_mints_db: Box<dyn Storage<String, MintAccount>> =
875            new_kv_store(&database_url, "token_mints", &surfnet_id)?;
876        let mut account_associated_data_db: Box<
877            dyn Storage<String, SerializableAccountAdditionalData>,
878        > = new_kv_store(&database_url, "account_associated_data", &surfnet_id)?;
879        // Store initial account associated data (native mint)
880        account_associated_data_db.store(
881            spl_token_interface::native_mint::ID.to_string(),
882            native_mint_associated_data.into(),
883        )?;
884        token_mints_db.store(
885            spl_token_interface::native_mint::ID.to_string(),
886            parsed_mint_account,
887        )?;
888        let token_accounts_by_owner_db: Box<dyn Storage<String, Vec<String>>> =
889            new_kv_store(&database_url, "token_accounts_by_owner", &surfnet_id)?;
890        let token_accounts_by_delegate_db: Box<dyn Storage<String, Vec<String>>> =
891            new_kv_store(&database_url, "token_accounts_by_delegate", &surfnet_id)?;
892        let token_accounts_by_mint_db: Box<dyn Storage<String, Vec<String>>> =
893            new_kv_store(&database_url, "token_accounts_by_mint", &surfnet_id)?;
894        let streamed_accounts_db: Box<dyn Storage<String, bool>> =
895            new_kv_store(&database_url, "streamed_accounts", &surfnet_id)?;
896        let scheduled_overrides_db: Box<dyn Storage<u64, Vec<OverrideInstance>>> =
897            new_kv_store(&database_url, "scheduled_overrides", &surfnet_id)?;
898        let offline_accounts_db: Box<dyn Storage<String, OfflineAccountConfig>> =
899            new_kv_store(&database_url, "offline_accounts", &surfnet_id)?;
900        let registered_idls_db: Box<dyn Storage<String, Vec<VersionedIdl>>> =
901            new_kv_store(&database_url, "registered_idls", &surfnet_id)?;
902        let profile_tag_map_db: Box<dyn Storage<String, Vec<UuidOrSignature>>> =
903            new_kv_store(&database_url, "profile_tag_map", &surfnet_id)?;
904        let simulated_transaction_profiles_db: Box<dyn Storage<String, KeyedProfileResult>> =
905            new_kv_store(&database_url, "simulated_transaction_profiles", &surfnet_id)?;
906        let executed_transaction_profiles_db: Box<dyn Storage<String, KeyedProfileResult>> = {
907            // Ensure max_profiles is at least 1 to avoid creating a zero-capacity FifoMap
908            let max_profiles = max(1, config.max_profiles);
909            new_kv_store_with_default(
910                &database_url,
911                "executed_transaction_profiles",
912                &surfnet_id,
913                // Use FifoMap for executed_transaction_profiles to maintain FIFO eviction behavior
914                // (when no on-disk DB is provided)
915                move || Box::new(FifoMap::<String, KeyedProfileResult>::new(max_profiles)),
916            )?
917        };
918        let epoch_schedule = Self::default_epoch_schedule();
919        let mut epoch_info = Self::default_epoch_info(&epoch_schedule);
920        let default_genesis_slot = epoch_info.absolute_slot;
921        let slot_checkpoint_db: Box<dyn Storage<String, u64>> =
922            new_kv_store(&database_url, "slot_checkpoint", &surfnet_id)?;
923
924        // Recover chain state: prefer slot checkpoint, fall back to max block in DB.
925        let checkpoint_slot = slot_checkpoint_db.get(&"latest_slot".to_string())?;
926        let max_block_slot = blocks_db
927            .into_iter()
928            .unwrap()
929            .max_by_key(|(slot, _): &(u64, BlockHeader)| *slot);
930        let has_persisted_chain_state = checkpoint_slot.is_some() || max_block_slot.is_some();
931
932        let (chain_tip, recovered_slot, recovered_block_height) =
933            match (checkpoint_slot, max_block_slot) {
934                // Prefer checkpoint if it's higher than the max stored block.
935                (Some(checkpoint), Some((block_slot, block))) => {
936                    if checkpoint > block_slot {
937                        (
938                            synthetic_chain_tip_at_index(checkpoint),
939                            checkpoint,
940                            checkpoint,
941                        )
942                    } else {
943                        (
944                            BlockIdentifier {
945                                index: block.block_height,
946                                hash: block.hash,
947                            },
948                            block_slot,
949                            block.block_height,
950                        )
951                    }
952                }
953                (Some(checkpoint), None) => (
954                    synthetic_chain_tip_at_index(checkpoint),
955                    checkpoint,
956                    checkpoint,
957                ),
958                (None, Some((block_slot, block))) => (
959                    BlockIdentifier {
960                        index: block.block_height,
961                        hash: block.hash,
962                    },
963                    block_slot,
964                    block.block_height,
965                ),
966                (None, None) => (
967                    BlockIdentifier::zero(),
968                    epoch_info.absolute_slot,
969                    epoch_info.block_height,
970                ),
971            };
972
973        if has_persisted_chain_state {
974            let (epoch, slot_index) = epoch_schedule.get_epoch_and_slot_index(recovered_slot);
975            epoch_info.epoch = epoch;
976            epoch_info.slot_index = slot_index;
977            epoch_info.absolute_slot = recovered_slot;
978            epoch_info.block_height = recovered_block_height;
979        }
980
981        // Initialize transactions_processed from database count for persistent storage
982        let transactions_processed = transactions_db.count()?;
983        let updated_at = Utc::now().timestamp_millis() as u64;
984        let last_checkpoint_slot = checkpoint_slot.unwrap_or_else(|| {
985            if has_persisted_chain_state {
986                recovered_slot
987            } else {
988                0
989            }
990        });
991
992        let mut svm = Self {
993            inner,
994            remote_rpc_url: None,
995            chain_tip,
996            blocks: blocks_db,
997            transactions: transactions_db,
998            jito_bundles: jito_bundles_db,
999            perf_samples: VecDeque::new(),
1000            transactions_processed,
1001            simnet_events_tx,
1002            geyser_events_tx,
1003            latest_epoch_info: epoch_info.clone(),
1004            transactions_queued_for_confirmation: VecDeque::new(),
1005            transactions_queued_for_finalization: VecDeque::new(),
1006            signature_subscriptions: HashMap::new(),
1007            account_subscriptions: HashMap::new(),
1008            program_subscriptions: HashMap::new(),
1009            slot_subscriptions: Vec::new(),
1010            slots_updates_subscriptions: Vec::new(),
1011            profile_tag_map: profile_tag_map_db,
1012            simulated_transaction_profiles: simulated_transaction_profiles_db,
1013            executed_transaction_profiles: executed_transaction_profiles_db,
1014            logs_subscriptions: Vec::new(),
1015            snapshot_subscriptions: Vec::new(),
1016            updated_at,
1017            slot_time: config.slot_time,
1018            start_time: SystemTime::now(),
1019            accounts_by_owner: accounts_by_owner_db,
1020            account_associated_data: account_associated_data_db,
1021            token_accounts: token_accounts_db,
1022            token_mints: token_mints_db,
1023            token_accounts_by_owner: token_accounts_by_owner_db,
1024            token_accounts_by_delegate: token_accounts_by_delegate_db,
1025            token_accounts_by_mint: token_accounts_by_mint_db,
1026            total_supply: 0,
1027            circulating_supply: 0,
1028            non_circulating_supply: 0,
1029            non_circulating_accounts: Vec::new(),
1030            genesis_config: GenesisConfig::default(),
1031            inflation: Inflation::default(),
1032            write_version: 0,
1033            registered_idls: registered_idls_db,
1034            feature_set,
1035            instruction_profiling_enabled: config.instruction_profiling_enabled,
1036            max_profiles: config.max_profiles,
1037            skip_blockhash_check: config.skip_blockhash_check,
1038            runbook_executions: Vec::new(),
1039            account_update_slots: HashMap::new(),
1040            streamed_accounts: streamed_accounts_db,
1041            recent_blockhashes: VecDeque::new(),
1042            scheduled_overrides: scheduled_overrides_db,
1043            offline_accounts: offline_accounts_db,
1044            genesis_slot: default_genesis_slot,
1045            genesis_updated_at: updated_at,
1046            slot_checkpoint: slot_checkpoint_db,
1047            last_checkpoint_slot,
1048        };
1049
1050        svm.inner.set_log_bytes_limit(config.log_bytes_limit);
1051        if !has_persisted_chain_state {
1052            svm.chain_tip = svm.new_blockhash();
1053        }
1054        svm.register_builtin_template_idls();
1055        svm.inner.set_sysvar(&epoch_schedule);
1056        svm.reconstruct_sysvars();
1057
1058        Ok((svm, simnet_events_rx, geyser_events_rx))
1059    }
1060
1061    pub fn increment_write_version(&mut self) -> u64 {
1062        self.write_version += 1;
1063        self.write_version
1064    }
1065
1066    /// Initializes the SVM with the provided epoch info and epoch schedule.
1067    ///
1068    /// This is reserved for remote-derived startup data that is not known until the runloop
1069    /// is ready to connect to a remote RPC.
1070    ///
1071    /// # Arguments
1072    /// * `epoch_info` - The epoch information to initialize with.
1073    pub fn initialize(&mut self, epoch_info: EpochInfo, epoch_schedule: EpochSchedule) {
1074        self.chain_tip = self.new_blockhash();
1075        self.latest_epoch_info = epoch_info.clone();
1076        // Set genesis_slot to the current slot when initializing (syncing with remote)
1077        // This marks the starting point for this surfnet instance
1078        self.genesis_slot = epoch_info.absolute_slot;
1079        self.updated_at = Utc::now().timestamp_millis() as u64;
1080        // Update genesis_updated_at to match the new genesis_slot
1081        self.genesis_updated_at = self.updated_at;
1082
1083        self.inner.set_sysvar(&epoch_schedule);
1084
1085        // Reconstruct all sysvars (RecentBlockhashes, SlotHashes, Clock)
1086        self.reconstruct_sysvars();
1087    }
1088
1089    pub fn set_profile_instructions(&mut self, do_profile_instructions: bool) {
1090        self.instruction_profiling_enabled = do_profile_instructions;
1091    }
1092
1093    /// Airdrops a specified amount of lamports to a single public key.
1094    ///
1095    /// Validates the amount before doing anything observable: an airdrop of 0
1096    /// lamports, or an amount below the rent-exempt minimum for an empty
1097    /// account, is rejected up front so no synthetic transaction is written
1098    /// and no balances are captured. The underlying SVM would not persist a
1099    /// sub-rent-exempt recipient account, and a follow-up account lookup
1100    /// would then panic on the missing account.
1101    ///
1102    /// # Arguments
1103    /// * `pubkey` - The recipient public key.
1104    /// * `lamports` - The amount of lamports to airdrop.
1105    pub fn airdrop(
1106        &mut self,
1107        pubkey: &Pubkey,
1108        lamports: u64,
1109    ) -> Result<TransactionResult, AirdropError> {
1110        if lamports == 0 {
1111            return Err(AirdropError::ZeroAmount);
1112        }
1113        let min_rent = self.inner.minimum_balance_for_rent_exemption(0);
1114        if lamports < min_rent {
1115            return Err(AirdropError::BelowRentExemption { lamports, min_rent });
1116        }
1117
1118        // Capture pre-airdrop balances for the airdrop account, recipient, and system program.
1119        let airdrop_pubkey = self.inner.airdrop_pubkey();
1120
1121        let airdrop_account_before = self
1122            .get_account(&airdrop_pubkey)?
1123            .unwrap_or_else(|| Account::default());
1124        let recipient_account_before = self
1125            .get_account(pubkey)?
1126            .unwrap_or_else(|| Account::default());
1127        let system_account_before = self
1128            .get_account(&system_program::id())?
1129            .unwrap_or_else(|| Account::default());
1130
1131        let res = self.inner.airdrop(pubkey, lamports);
1132        let (status_tx, _rx) = unbounded();
1133        if let Ok(ref tx_result) = res {
1134            let slot = self.latest_epoch_info.absolute_slot;
1135            // Capture post-airdrop balances
1136            let airdrop_account_after = self
1137                .get_account(&airdrop_pubkey)?
1138                .unwrap_or_else(|| Account::default());
1139            let recipient_account_after = self
1140                .get_account(pubkey)?
1141                .unwrap_or_else(|| Account::default());
1142            let system_account_after = self
1143                .get_account(&system_program::id())?
1144                .unwrap_or_else(|| Account::default());
1145
1146            // Construct a synthetic transaction that mirrors the underlying airdrop.
1147            let tx = VersionedTransaction {
1148                signatures: vec![tx_result.signature],
1149                message: VersionedMessage::Legacy(Message::new(
1150                    &[system_instruction::transfer(
1151                        &airdrop_pubkey,
1152                        pubkey,
1153                        lamports,
1154                    )],
1155                    Some(&airdrop_pubkey),
1156                )),
1157            };
1158
1159            self.transactions.store(
1160                tx.get_signature().to_string(),
1161                SurfnetTransactionStatus::processed(
1162                    TransactionWithStatusMeta {
1163                        slot,
1164                        transaction: tx.clone(),
1165                        meta: TransactionStatusMeta {
1166                            status: Ok(()),
1167                            fee: 5000,
1168                            pre_balances: vec![
1169                                airdrop_account_before.lamports,
1170                                recipient_account_before.lamports,
1171                                system_account_before.lamports,
1172                            ],
1173                            post_balances: vec![
1174                                airdrop_account_after.lamports,
1175                                recipient_account_after.lamports,
1176                                system_account_after.lamports,
1177                            ],
1178                            inner_instructions: Some(vec![]),
1179                            log_messages: Some(tx_result.logs.clone()),
1180                            pre_token_balances: Some(vec![]),
1181                            post_token_balances: Some(vec![]),
1182                            rewards: Some(vec![]),
1183                            loaded_addresses: LoadedAddresses::default(),
1184                            return_data: Some(tx_result.return_data.clone()),
1185                            compute_units_consumed: Some(tx_result.compute_units_consumed),
1186                            cost_units: None,
1187                        },
1188                    },
1189                    HashSet::from([*pubkey]),
1190                ),
1191            )?;
1192            self.notify_signature_subscribers(
1193                SignatureSubscriptionType::processed(),
1194                tx.get_signature(),
1195                slot,
1196                None,
1197            );
1198            self.notify_logs_subscribers(
1199                tx.get_signature(),
1200                None,
1201                tx_result.logs.clone(),
1202                CommitmentLevel::Processed,
1203            );
1204            self.transactions_queued_for_confirmation
1205                .push_back((tx, status_tx.clone(), None));
1206            if let Some(account) = self.get_account(pubkey)? {
1207                self.set_account(pubkey, account)?;
1208            }
1209        }
1210        Ok(res)
1211    }
1212
1213    /// Airdrops a specified amount of lamports to a list of public keys.
1214    ///
1215    /// Defers the zero-amount and below-rent-exemption checks to
1216    /// [`Self::airdrop`]; on either of those variants the whole batch is
1217    /// abandoned after a single info/error event, since the rejection
1218    /// depends only on `lamports` and would otherwise repeat per recipient.
1219    ///
1220    /// # Arguments
1221    /// * `lamports` - The amount of lamports to airdrop.
1222    /// * `addresses` - Slice of recipient public keys.
1223    pub fn airdrop_pubkeys(&mut self, lamports: u64, addresses: &[Pubkey]) {
1224        for recipient in addresses {
1225            match self.airdrop(recipient, lamports) {
1226                Ok(_) => {
1227                    let _ = self.simnet_events_tx.send(SimnetEvent::info(format!(
1228                        "Genesis airdrop successful {}: {}",
1229                        recipient, lamports
1230                    )));
1231                }
1232                Err(AirdropError::ZeroAmount) => {
1233                    let _ = self
1234                        .simnet_events_tx
1235                        .send(SimnetEvent::info("Skipping 0 lamport airdrop"));
1236                    return;
1237                }
1238                Err(AirdropError::BelowRentExemption { lamports, min_rent }) => {
1239                    let _ = self.simnet_events_tx.send(SimnetEvent::error(format!(
1240                        "Skipping invalid airdrop: amount {lamports} is below the rent-exempt minimum of {min_rent} lamports"
1241                    )));
1242                    return;
1243                }
1244                Err(AirdropError::Other(e)) => {
1245                    let _ = self.simnet_events_tx.send(SimnetEvent::error(format!(
1246                        "Genesis airdrop failed {}: {}",
1247                        recipient, e
1248                    )));
1249                }
1250            };
1251        }
1252    }
1253
1254    /// Returns the latest known absolute slot from the local epoch info.
1255    pub const fn get_latest_absolute_slot(&self) -> Slot {
1256        self.latest_epoch_info.absolute_slot
1257    }
1258
1259    /// Returns the latest blockhash known by the SVM.
1260    pub fn latest_blockhash(&self) -> solana_hash::Hash {
1261        Hash::from_str(&self.chain_tip.hash).expect("Invalid blockhash")
1262    }
1263
1264    /// Returns the latest epoch info known by the `SurfnetSvm`.
1265    pub fn latest_epoch_info(&self) -> EpochInfo {
1266        self.latest_epoch_info.clone()
1267    }
1268
1269    /// Calculates the block time for a given slot based on genesis timestamp.
1270    /// Returns the time in milliseconds since genesis.
1271    pub fn calculate_block_time_for_slot(&self, slot: Slot) -> u64 {
1272        // Calculate time relative to genesis_slot (when this surfnet started)
1273        let slots_since_genesis = slot.saturating_sub(self.genesis_slot);
1274        self.genesis_updated_at + (slots_since_genesis * self.slot_time)
1275    }
1276
1277    /// Checks if a slot is within the valid range for sparse block storage.
1278    /// A slot is valid if it's between genesis_slot (inclusive) and latest_slot (inclusive).
1279    ///
1280    /// # Arguments
1281    /// * `slot` - The slot number to check.
1282    ///
1283    /// # Returns
1284    /// `true` if the slot is within the valid range, `false` otherwise.
1285    pub fn is_slot_in_valid_range(&self, slot: Slot) -> bool {
1286        let latest_slot = self.get_latest_absolute_slot();
1287        slot >= self.genesis_slot && slot <= latest_slot
1288    }
1289
1290    /// Gets a block from storage, or reconstructs an empty block if the slot is within
1291    /// the valid range (sparse block storage).
1292    ///
1293    /// # Arguments
1294    /// * `slot` - The slot number to retrieve.
1295    ///
1296    /// # Returns
1297    /// * `Ok(Some(BlockHeader))` - If the block exists or can be reconstructed
1298    /// * `Ok(None)` - If the slot is outside the valid range
1299    /// * `Err(_)` - If there was an error accessing storage
1300    pub fn get_block_or_reconstruct(&self, slot: Slot) -> SurfpoolResult<Option<BlockHeader>> {
1301        match self.blocks.get(&slot)? {
1302            Some(block) => Ok(Some(block)),
1303            None => {
1304                if self.is_slot_in_valid_range(slot) {
1305                    Ok(Some(self.reconstruct_empty_block(slot)))
1306                } else {
1307                    Ok(None)
1308                }
1309            }
1310        }
1311    }
1312
1313    /// Reconstructs an empty block header for a slot that wasn't stored.
1314    /// This is used for sparse block storage where empty blocks are not persisted.
1315    pub fn reconstruct_empty_block(&self, slot: Slot) -> BlockHeader {
1316        let block_height = slot;
1317        BlockHeader {
1318            hash: SyntheticBlockhash::new(block_height).to_string(),
1319            previous_blockhash: SyntheticBlockhash::new(block_height.saturating_sub(1)).to_string(),
1320            parent_slot: slot.saturating_sub(1),
1321            block_time: (self.calculate_block_time_for_slot(slot) / 1_000) as i64,
1322            block_height,
1323            signatures: vec![],
1324        }
1325    }
1326
1327    /// Reconstructs RecentBlockhashes, SlotHashes, and Clock sysvars deterministically
1328    /// from the current slot. Called on startup and after garbage collection to ensure
1329    /// consistent sysvar state without requiring database persistence.
1330    ///
1331    /// Note: SyntheticBlockhash uses chain_tip.index (relative index), while SlotHashes
1332    /// and Clock use absolute slots (chain_tip.index + genesis_slot).
1333    #[allow(deprecated)]
1334    pub fn reconstruct_sysvars(&mut self) {
1335        use solana_slot_hashes::SlotHashes;
1336        use solana_sysvar::recent_blockhashes::{IterItem, RecentBlockhashes};
1337
1338        let current_index = self.chain_tip.index;
1339        let current_absolute_slot = self.get_latest_absolute_slot();
1340
1341        // Calculate range for blockhashes - use relative indices for SyntheticBlockhash
1342        let start_index = current_index.saturating_sub(MAX_RECENT_BLOCKHASHES_STANDARD as u64 - 1);
1343
1344        // Generate all synthetic blockhashes using relative indices (chain_tip.index style)
1345        // This matches how new_blockhash() generates hashes
1346        let synthetic_hashes: Vec<_> = (start_index..=current_index)
1347            .rev()
1348            .map(SyntheticBlockhash::new)
1349            .collect();
1350
1351        // 1. Reconstruct RecentBlockhashes (last 150 blockhashes)
1352        let recent_blockhashes_vec: Vec<_> = synthetic_hashes
1353            .iter()
1354            .enumerate()
1355            .map(|(index, hash)| IterItem(index as u64, hash.hash(), 0))
1356            .collect();
1357        let recent_blockhashes = RecentBlockhashes::from_iter(recent_blockhashes_vec);
1358        self.inner.set_sysvar(&recent_blockhashes);
1359
1360        // 2. Reconstruct SlotHashes - maps absolute slots to blockhashes.
1361        // The local ledger starts at genesis_slot, but finalized commitment is
1362        // reported as current_slot - FINALIZATION_SLOT_THRESHOLD. During the
1363        // initial warmup this points before genesis_slot, so seed those
1364        // synthetic slots into SlotHashes while preserving the local hash
1365        // mapping for genesis_slot and later.
1366        let slot_hash_start_index =
1367            current_index.saturating_sub(MAX_SLOT_HASHES_ENTRIES as u64 - 1);
1368        let local_start_absolute_slot = slot_hash_start_index + self.genesis_slot;
1369        let finalized_start_absolute_slot =
1370            current_absolute_slot.saturating_sub(FINALIZATION_SLOT_THRESHOLD);
1371        let earliest_retained_slot =
1372            current_absolute_slot.saturating_sub(MAX_SLOT_HASHES_ENTRIES as u64 - 1);
1373        let start_absolute_slot = local_start_absolute_slot
1374            .min(finalized_start_absolute_slot)
1375            .max(earliest_retained_slot);
1376        let slot_hashes_vec: Vec<_> = (start_absolute_slot..=current_absolute_slot)
1377            .rev()
1378            .map(|slot| {
1379                (
1380                    slot,
1381                    *synthetic_blockhash_for_slot(slot, self.genesis_slot).hash(),
1382                )
1383            })
1384            .collect();
1385        let slot_hashes = SlotHashes::new(&slot_hashes_vec);
1386        self.inner.set_sysvar(&slot_hashes);
1387
1388        // 3. Reconstruct Clock using absolute slot
1389        let unix_timestamp = self.calculate_block_time_for_slot(current_absolute_slot) / 1_000;
1390        let clock = Clock {
1391            slot: current_absolute_slot,
1392            epoch: self.latest_epoch_info.epoch,
1393            unix_timestamp: unix_timestamp as i64,
1394            epoch_start_timestamp: 0,
1395            leader_schedule_epoch: 0,
1396        };
1397        self.inner.set_sysvar(&clock);
1398    }
1399
1400    /// Generates and sets a new blockhash, updating the RecentBlockhashes sysvar.
1401    ///
1402    /// # Returns
1403    /// A new `BlockIdentifier` for the updated blockhash.
1404    #[allow(deprecated)]
1405    fn new_blockhash(&mut self) -> BlockIdentifier {
1406        use solana_slot_hashes::SlotHashes;
1407        use solana_sysvar::recent_blockhashes::{IterItem, RecentBlockhashes};
1408        // Backup the current block hashes
1409        let recent_blockhashes_backup = self.inner.get_sysvar::<RecentBlockhashes>();
1410        let num_blockhashes_expected = recent_blockhashes_backup
1411            .len()
1412            .min(MAX_RECENT_BLOCKHASHES_STANDARD);
1413        // Invalidate the current block hash.
1414        // LiteSVM bug / feature: calling this method empties `sysvar::<RecentBlockhashes>()`
1415        self.inner.expire_blockhash();
1416        // Rebuild recent blockhashes
1417        let mut recent_blockhashes = Vec::with_capacity(num_blockhashes_expected);
1418        let recent_blockhashes_overriden = self.inner.get_sysvar::<RecentBlockhashes>();
1419        let latest_entry = recent_blockhashes_overriden
1420            .first()
1421            .expect("Latest blockhash not found");
1422
1423        let new_synthetic_blockhash = SyntheticBlockhash::new(self.chain_tip.index);
1424        let new_synthetic_blockhash_str = new_synthetic_blockhash.to_string();
1425
1426        recent_blockhashes.push(IterItem(
1427            0,
1428            new_synthetic_blockhash.hash(),
1429            latest_entry.fee_calculator.lamports_per_signature,
1430        ));
1431
1432        // Append the previous blockhashes, ignoring the first one
1433        for (index, entry) in recent_blockhashes_backup.iter().enumerate() {
1434            if recent_blockhashes.len() >= MAX_RECENT_BLOCKHASHES_STANDARD {
1435                break;
1436            }
1437            recent_blockhashes.push(IterItem(
1438                (index + 1) as u64,
1439                &entry.blockhash,
1440                entry.fee_calculator.lamports_per_signature,
1441            ));
1442        }
1443
1444        self.inner
1445            .set_sysvar(&RecentBlockhashes::from_iter(recent_blockhashes));
1446
1447        let mut slot_hashes = self.inner.get_sysvar::<SlotHashes>();
1448        slot_hashes.add(
1449            self.get_latest_absolute_slot() + 1,
1450            *new_synthetic_blockhash.hash(),
1451        );
1452        self.inner.set_sysvar(&SlotHashes::new(&slot_hashes));
1453
1454        BlockIdentifier::new(
1455            self.chain_tip.index + 1,
1456            new_synthetic_blockhash_str.as_str(),
1457        )
1458    }
1459
1460    /// Checks if the provided blockhash is recent (present in the RecentBlockhashes sysvar).
1461    ///
1462    /// # Arguments
1463    /// * `recent_blockhash` - The blockhash to check.
1464    ///
1465    /// # Returns
1466    /// `true` if the blockhash is recent, `false` otherwise.
1467    pub fn check_blockhash_is_recent(&self, recent_blockhash: &Hash) -> bool {
1468        #[allow(deprecated)]
1469        self.inner
1470            .get_sysvar::<solana_sysvar::recent_blockhashes::RecentBlockhashes>()
1471            .iter()
1472            .any(|entry| entry.blockhash == *recent_blockhash)
1473    }
1474
1475    /// Validates the blockhash of a transaction, considering nonce accounts if present.
1476    /// If the transaction uses a nonce account, the blockhash is validated against the nonce account's stored blockhash.
1477    /// Otherwise, it is validated against the RecentBlockhashes sysvar.
1478    ///
1479    /// # Arguments
1480    /// * `tx` - The transaction to validate.
1481    ///
1482    /// # Returns
1483    /// `true` if the transaction blockhash is valid, `false` otherwise.
1484    pub fn validate_transaction_blockhash(&self, tx: &VersionedTransaction) -> bool {
1485        if self.skip_blockhash_check {
1486            return true;
1487        }
1488
1489        let recent_blockhash = tx.message.recent_blockhash();
1490
1491        let some_nonce_account_index = tx
1492            .message
1493            .instructions()
1494            .get(solana_nonce::NONCED_TX_MARKER_IX_INDEX as usize)
1495            .filter(|instruction| {
1496                matches!(
1497                    tx.message.static_account_keys().get(instruction.program_id_index as usize),
1498                    Some(program_id) if system_program::check_id(program_id)
1499                ) && is_advance_nonce_instruction_data(&instruction.data)
1500            })
1501            .map(|instruction| {
1502                // nonce account is the first account in the instruction
1503                instruction.accounts.get(0)
1504            });
1505
1506        debug!(
1507            "Validating tx blockhash: {}; is nonce tx?: {}",
1508            recent_blockhash,
1509            some_nonce_account_index.is_some()
1510        );
1511
1512        if let Some(nonce_account_index) = some_nonce_account_index {
1513            trace!(
1514                "Nonce tx detected. Nonce account index: {:?}",
1515                nonce_account_index
1516            );
1517            let Some(nonce_account_index) = nonce_account_index else {
1518                return false;
1519            };
1520
1521            let Some(nonce_account_pubkey) = tx
1522                .message
1523                .static_account_keys()
1524                .get(*nonce_account_index as usize)
1525            else {
1526                return false;
1527            };
1528
1529            trace!("Nonce account pubkey: {:?}", nonce_account_pubkey,);
1530
1531            // Here we're swallowing errors in the storage - if we fail to fetch the account because of a storage error,
1532            // we're just considering the blockhash to be invalid.
1533            let Ok(Some(nonce_account)) = self.get_account(nonce_account_pubkey) else {
1534                return false;
1535            };
1536            trace!("Nonce account: {:?}", nonce_account);
1537
1538            let Some(nonce_data) =
1539                bincode::deserialize::<solana_nonce::versions::Versions>(&nonce_account.data).ok()
1540            else {
1541                return false;
1542            };
1543            trace!("Nonce account data: {:?}", nonce_data);
1544
1545            let nonce_state = nonce_data.state();
1546            let initialized_state = match nonce_state {
1547                solana_nonce::state::State::Uninitialized => return false,
1548                solana_nonce::state::State::Initialized(data) => data,
1549            };
1550            return initialized_state.blockhash() == *recent_blockhash;
1551        } else {
1552            self.check_blockhash_is_recent(recent_blockhash)
1553        }
1554    }
1555
1556    /// Verifies the signature of a transaction and validates that it hasn't already been processed.
1557    /// ### Note
1558    /// LiteSVM also can do this for our transactions, but we disable it.
1559    /// If sigverify is enabled at the LiteSVM level, the transaction simulations are always verified as well.
1560    /// So, if the user is trying to skip signature verification for a simulation, we'd need to unset and set this value,
1561    /// requiring a mutable reference to the SVM, which we don't have/want in the simulation path.
1562    /// Additionally, having this function internally lets us do this check before we start fetching accounts from mainnet.
1563    pub fn sigverify(&self, tx: &VersionedTransaction) -> Result<(), FailedTransactionMetadata> {
1564        let signature = tx.signatures[0];
1565
1566        if tx.verify_with_results().iter().any(|valid| !*valid) {
1567            return Err(FailedTransactionMetadata {
1568                err: TransactionError::SignatureFailure,
1569                meta: TransactionMetadata::default(),
1570            });
1571        }
1572
1573        if matches!(
1574            self.transactions.get(&signature.to_string()),
1575            Ok(Some(SurfnetTransactionStatus::Processed(_)))
1576        ) {
1577            return Err(FailedTransactionMetadata {
1578                err: TransactionError::AlreadyProcessed,
1579                meta: TransactionMetadata::default(),
1580            });
1581        }
1582        Ok(())
1583    }
1584
1585    /// Sets an account in the local SVM state and notifies listeners.
1586    ///
1587    /// # Arguments
1588    /// * `pubkey` - The public key of the account.
1589    /// * `account` - The [Account] to insert.
1590    ///
1591    /// # Returns
1592    /// `Ok(())` on success, or an error if the operation fails.
1593    pub fn set_account(&mut self, pubkey: &Pubkey, account: Account) -> SurfpoolResult<()> {
1594        self.inner
1595            .set_account(*pubkey, account.clone())
1596            .map_err(|e| SurfpoolError::set_account(*pubkey, e))?;
1597
1598        self.account_update_slots
1599            .insert(*pubkey, self.get_latest_absolute_slot());
1600
1601        // Update the account registries and indexes
1602        self.update_account_registries(pubkey, &account)?;
1603
1604        // Notify account subscribers
1605        self.notify_account_subscribers(pubkey, &account);
1606
1607        // Notify program subscribers
1608        self.notify_program_subscribers(pubkey, &account);
1609
1610        let _ = self
1611            .simnet_events_tx
1612            .send(SimnetEvent::account_update(*pubkey));
1613        Ok(())
1614    }
1615
1616    pub fn update_account_registries(
1617        &mut self,
1618        pubkey: &Pubkey,
1619        account: &Account,
1620    ) -> SurfpoolResult<()> {
1621        let is_deleted_account = account == &Account::default();
1622
1623        // Mirror the SVM state into the backing database. The inner SVM is
1624        // already up to date by the time this function runs; the database is
1625        // the side-effect target.
1626        if is_deleted_account {
1627            self.inner.delete_account_in_db(pubkey)?;
1628        } else {
1629            self.inner
1630                .set_account_in_db(*pubkey, account.clone().into())?;
1631        }
1632
1633        if is_deleted_account {
1634            // Record the account as offline so the surfnet does not re-fetch
1635            // it from the upstream RPC, then drop any stale index entries
1636            // that pointed at its prior on-chain state.
1637            self.offline_accounts.store(
1638                pubkey.to_string(),
1639                OfflineAccountConfig {
1640                    include_owned_accounts: false,
1641                },
1642            )?;
1643            if let Some(old_account) = self.get_account(pubkey)? {
1644                self.remove_from_indexes(pubkey, &old_account)?;
1645            }
1646            return Ok(());
1647        }
1648
1649        // Drop any stale owner/mint/delegate entries for the prior version of
1650        // the account before indexing the new one; otherwise a change of
1651        // owner would leave the old owner's bucket pointing at `pubkey`.
1652        if let Some(old_account) = self.get_account(pubkey)? {
1653            self.remove_from_indexes(pubkey, &old_account)?;
1654        }
1655
1656        let pubkey_str = pubkey.to_string();
1657        add_pubkey_to_index(
1658            &mut self.accounts_by_owner,
1659            account.owner.to_string(),
1660            &pubkey_str,
1661        )?;
1662
1663        if is_supported_token_program(&account.owner) {
1664            self.index_token_account_variant(pubkey, &pubkey_str, account)?;
1665            self.index_mint_account_variant(pubkey, account)?;
1666            self.index_token_2022_mint_extensions(pubkey, account)?;
1667        }
1668        Ok(())
1669    }
1670
1671    /// If `account.data` decodes as a token account, add it to the
1672    /// owner/mint/delegate indexes and cache the unpacked `TokenAccount`.
1673    /// A decode failure is treated as "not a token account" (not an error);
1674    /// the enclosing call only dispatches here when the owner program is
1675    /// already known to be a supported token program.
1676    fn index_token_account_variant(
1677        &mut self,
1678        pubkey: &Pubkey,
1679        pubkey_str: &str,
1680        account: &Account,
1681    ) -> SurfpoolResult<()> {
1682        let Ok(token_account) = TokenAccount::unpack(&account.data) else {
1683            return Ok(());
1684        };
1685        add_pubkey_to_index(
1686            &mut self.token_accounts_by_owner,
1687            token_account.owner().to_string(),
1688            pubkey_str,
1689        )?;
1690        add_pubkey_to_index(
1691            &mut self.token_accounts_by_mint,
1692            token_account.mint().to_string(),
1693            pubkey_str,
1694        )?;
1695        if let COption::Some(delegate) = token_account.delegate() {
1696            add_pubkey_to_index(
1697                &mut self.token_accounts_by_delegate,
1698                delegate.to_string(),
1699                pubkey_str,
1700            )?;
1701        }
1702        self.token_accounts
1703            .store(pubkey.to_string(), token_account)?;
1704        Ok(())
1705    }
1706
1707    /// If `account.data` decodes as a mint, cache the unpacked `MintAccount`.
1708    fn index_mint_account_variant(
1709        &mut self,
1710        pubkey: &Pubkey,
1711        account: &Account,
1712    ) -> SurfpoolResult<()> {
1713        let Ok(mint_account) = MintAccount::unpack(&account.data) else {
1714            return Ok(());
1715        };
1716        self.token_mints.store(pubkey.to_string(), mint_account)?;
1717        Ok(())
1718    }
1719
1720    /// If `account.data` decodes as a Token-2022 mint with extensions,
1721    /// snapshot the decimals and rate-limited extension state
1722    /// (`InterestBearingConfig`, `ScaledUiAmountConfig`) into
1723    /// `account_associated_data` so the RPC layer can serve UI-amount
1724    /// conversions without re-parsing the raw account on every request.
1725    fn index_token_2022_mint_extensions(
1726        &mut self,
1727        pubkey: &Pubkey,
1728        account: &Account,
1729    ) -> SurfpoolResult<()> {
1730        let Ok(mint) =
1731            StateWithExtensions::<spl_token_2022_interface::state::Mint>::unpack(&account.data)
1732        else {
1733            return Ok(());
1734        };
1735        let unix_timestamp = self.inner.get_sysvar::<Clock>().unix_timestamp;
1736        let interest_bearing_config = mint
1737            .get_extension::<InterestBearingConfig>()
1738            .map(|x| (*x, unix_timestamp))
1739            .ok();
1740        let scaled_ui_amount_config = mint
1741            .get_extension::<ScaledUiAmountConfig>()
1742            .map(|x| (*x, unix_timestamp))
1743            .ok();
1744        let additional_data: SerializableAccountAdditionalData = AccountAdditionalDataV3 {
1745            spl_token_additional_data: Some(SplTokenAdditionalDataV2 {
1746                decimals: mint.base.decimals,
1747                interest_bearing_config,
1748                scaled_ui_amount_config,
1749            }),
1750        }
1751        .into();
1752        self.account_associated_data
1753            .store(pubkey.to_string(), additional_data)?;
1754        Ok(())
1755    }
1756
1757    fn remove_from_indexes(
1758        &mut self,
1759        pubkey: &Pubkey,
1760        old_account: &Account,
1761    ) -> SurfpoolResult<()> {
1762        let pubkey_str = pubkey.to_string();
1763        remove_pubkey_from_index(
1764            &mut self.accounts_by_owner,
1765            &old_account.owner.to_string(),
1766            &pubkey_str,
1767        )?;
1768
1769        if is_supported_token_program(&old_account.owner)
1770            && let Some(old_token_account) = self.token_accounts.take(&pubkey_str)?
1771        {
1772            remove_pubkey_from_index(
1773                &mut self.token_accounts_by_owner,
1774                &old_token_account.owner().to_string(),
1775                &pubkey_str,
1776            )?;
1777            remove_pubkey_from_index(
1778                &mut self.token_accounts_by_mint,
1779                &old_token_account.mint().to_string(),
1780                &pubkey_str,
1781            )?;
1782            if let COption::Some(delegate) = old_token_account.delegate() {
1783                remove_pubkey_from_index(
1784                    &mut self.token_accounts_by_delegate,
1785                    &delegate.to_string(),
1786                    &pubkey_str,
1787                )?;
1788            }
1789        }
1790        Ok(())
1791    }
1792
1793    pub fn reset_network(
1794        &mut self,
1795        epoch_info: EpochInfo,
1796        epoch_schedule: EpochSchedule,
1797    ) -> SurfpoolResult<()> {
1798        self.inner.reset(self.feature_set.clone())?;
1799
1800        let native_mint_account = self
1801            .inner
1802            .get_account(&spl_token_interface::native_mint::ID)?
1803            .unwrap();
1804
1805        let native_mint_associated_data = {
1806            let mint = StateWithExtensions::<spl_token_2022_interface::state::Mint>::unpack(
1807                &native_mint_account.data,
1808            )
1809            .unwrap();
1810            let unix_timestamp = self.inner.get_sysvar::<Clock>().unix_timestamp;
1811            let interest_bearing_config = mint
1812                .get_extension::<InterestBearingConfig>()
1813                .map(|x| (*x, unix_timestamp))
1814                .ok();
1815            let scaled_ui_amount_config = mint
1816                .get_extension::<ScaledUiAmountConfig>()
1817                .map(|x| (*x, unix_timestamp))
1818                .ok();
1819            AccountAdditionalDataV3 {
1820                spl_token_additional_data: Some(SplTokenAdditionalDataV2 {
1821                    decimals: mint.base.decimals,
1822                    interest_bearing_config,
1823                    scaled_ui_amount_config,
1824                }),
1825            }
1826        };
1827
1828        let parsed_mint_account = MintAccount::unpack(&native_mint_account.data).unwrap();
1829
1830        self.blocks.clear()?;
1831        self.transactions.clear()?;
1832        self.transactions_queued_for_confirmation.clear();
1833        self.transactions_queued_for_finalization.clear();
1834        self.perf_samples.clear();
1835        self.transactions_processed = 0;
1836        self.profile_tag_map.clear()?;
1837        self.simulated_transaction_profiles.clear()?;
1838        self.executed_transaction_profiles.clear()?;
1839        self.accounts_by_owner.clear()?;
1840        self.accounts_by_owner.store(
1841            native_mint_account.owner.to_string(),
1842            vec![spl_token_interface::native_mint::ID.to_string()],
1843        )?;
1844        self.account_associated_data.clear()?;
1845        self.account_associated_data.store(
1846            spl_token_interface::native_mint::ID.to_string(),
1847            native_mint_associated_data.into(),
1848        )?;
1849        self.token_accounts.clear()?;
1850        self.token_mints.clear()?;
1851        self.token_mints.store(
1852            spl_token_interface::native_mint::ID.to_string(),
1853            parsed_mint_account,
1854        )?;
1855        self.token_accounts_by_owner.clear()?;
1856        self.token_accounts_by_delegate.clear()?;
1857        self.token_accounts_by_mint.clear()?;
1858        self.non_circulating_accounts.clear();
1859        self.registered_idls.clear()?;
1860        self.register_builtin_template_idls();
1861        self.runbook_executions.clear();
1862        self.streamed_accounts.clear()?;
1863        self.scheduled_overrides.clear()?;
1864
1865        let current_time = chrono::Utc::now().timestamp_millis() as u64;
1866        self.updated_at = current_time;
1867        self.genesis_updated_at = current_time;
1868        self.latest_epoch_info = epoch_info.clone();
1869        // Set genesis_slot to the current slot when resetting (similar to initialize)
1870        self.genesis_slot = epoch_info.absolute_slot;
1871        let chain_tip_hash = SyntheticBlockhash::new(epoch_info.block_height).to_string();
1872        self.chain_tip = BlockIdentifier::new(epoch_info.block_height, chain_tip_hash.as_str());
1873        self.inner.set_sysvar(&epoch_schedule);
1874        // Rebuild sysvars so getLatestBlockhash / sendTransaction stay aligned after reset.
1875        self.reconstruct_sysvars();
1876        // Reset checkpoint state to avoid recovering stale chain tips after a reset.
1877        self.slot_checkpoint.clear()?;
1878        self.last_checkpoint_slot = self.genesis_slot;
1879        self.recent_blockhashes.clear();
1880
1881        Ok(())
1882    }
1883
1884    pub fn reset_account(
1885        &mut self,
1886        pubkey: &Pubkey,
1887        include_owned_accounts: bool,
1888    ) -> SurfpoolResult<()> {
1889        let Some(account) = self.get_account(pubkey)? else {
1890            return Ok(());
1891        };
1892
1893        if account.executable {
1894            // Handle upgradeable program - also reset the program data account
1895            if account.owner == solana_sdk_ids::bpf_loader_upgradeable::id() {
1896                let program_data_pubkey =
1897                    solana_loader_v3_interface::get_program_data_address(pubkey);
1898
1899                // Reset the program data account first
1900                self.purge_account_from_cache(&account, &program_data_pubkey)?;
1901            }
1902        }
1903        if include_owned_accounts {
1904            let owned_accounts = self.get_account_owned_by(pubkey)?;
1905            for (owned_pubkey, _) in owned_accounts {
1906                // Avoid infinite recursion by not cascading further
1907                self.purge_account_from_cache(&account, &owned_pubkey)?;
1908            }
1909        }
1910        // Reset the account itself
1911        self.purge_account_from_cache(&account, pubkey)?;
1912        Ok(())
1913    }
1914
1915    fn purge_account_from_cache(
1916        &mut self,
1917        account: &Account,
1918        pubkey: &Pubkey,
1919    ) -> SurfpoolResult<()> {
1920        self.remove_from_indexes(pubkey, account)?;
1921
1922        self.inner.delete_account(pubkey)?;
1923
1924        Ok(())
1925    }
1926
1927    /// Restores account state from a snapshot file.
1928    ///
1929    /// The snapshot should be a JSON file containing a map of pubkey strings to AccountSnapshot objects,
1930    /// as generated by the `surfnet_exportSnapshot` RPC method.
1931    ///
1932    /// # Arguments
1933    /// * `snapshot_path` - Path to the snapshot JSON file
1934    ///
1935    /// # Returns
1936    /// The number of accounts restored, or an error if the operation fails.
1937    pub fn restore_from_snapshot(&mut self, snapshot_path: &str) -> SurfpoolResult<usize> {
1938        use std::{collections::HashMap, fs, str::FromStr};
1939
1940        use surfpool_types::AccountSnapshot;
1941
1942        let content = fs::read_to_string(snapshot_path).map_err(|e| {
1943            SurfpoolError::internal(format!(
1944                "Failed to read snapshot file '{}': {}",
1945                snapshot_path, e
1946            ))
1947        })?;
1948
1949        let snapshot: HashMap<String, AccountSnapshot> =
1950            serde_json::from_str(&content).map_err(|e| {
1951                SurfpoolError::internal(format!(
1952                    "Failed to parse snapshot file '{}': {}",
1953                    snapshot_path, e
1954                ))
1955            })?;
1956
1957        let mut restored_count = 0;
1958        let mut errors: Vec<String> = Vec::new();
1959
1960        for (pubkey_str, account_snapshot) in snapshot.iter() {
1961            let pubkey = match Pubkey::from_str(pubkey_str) {
1962                Ok(pk) => pk,
1963                Err(e) => {
1964                    errors.push(format!("Invalid pubkey '{}': {}", pubkey_str, e));
1965                    continue;
1966                }
1967            };
1968
1969            let account = match account_snapshot.to_account() {
1970                Ok(acc) => acc,
1971                Err(e) => {
1972                    errors.push(format!("Failed to convert account '{}': {}", pubkey_str, e));
1973                    continue;
1974                }
1975            };
1976
1977            if let Err(e) = self.set_account(&pubkey, account) {
1978                errors.push(format!("Failed to set account '{}': {}", pubkey_str, e));
1979                continue;
1980            }
1981
1982            restored_count += 1;
1983        }
1984
1985        // Log any errors that occurred
1986        if !errors.is_empty() {
1987            let _ = self.simnet_events_tx.send(SimnetEvent::warn(format!(
1988                "Snapshot restore completed with {} errors: {}",
1989                errors.len(),
1990                errors.join("; ")
1991            )));
1992        }
1993
1994        let _ = self.simnet_events_tx.send(SimnetEvent::info(format!(
1995            "Restored {} accounts from snapshot",
1996            restored_count
1997        )));
1998
1999        Ok(restored_count)
2000    }
2001
2002    /// Sends a transaction to the system for execution.
2003    ///
2004    /// This function attempts to send a transaction to the blockchain. It first increments the `transactions_processed` counter.
2005    /// Then it sends the transaction to the system and updates its status. If the transaction is successfully processed, it is
2006    /// cached locally, and a "transaction processed" event is sent. If the transaction fails, the error is recorded and an event
2007    /// is sent indicating the failure.
2008    ///
2009    /// # Arguments
2010    /// * `tx` - The transaction to send.
2011    /// * `cu_analysis_enabled` - Whether compute unit analysis is enabled.
2012    ///
2013    /// # Returns
2014    /// `Ok(res)` if processed successfully, or `Err(tx_failure)` if failed.
2015    #[allow(clippy::result_large_err)]
2016    pub fn send_transaction(
2017        &mut self,
2018        tx: VersionedTransaction,
2019        cu_analysis_enabled: bool,
2020        sigverify: bool,
2021    ) -> TransactionResult {
2022        if sigverify {
2023            self.sigverify(&tx)?;
2024        }
2025
2026        if cu_analysis_enabled {
2027            let estimation_result = self.estimate_compute_units(&tx);
2028            let _ = self.simnet_events_tx.try_send(SimnetEvent::info(format!(
2029                "CU Estimation for tx: {} | Consumed: {} | Success: {} | Logs: {:?} | Error: {:?}",
2030                tx.signatures
2031                    .first()
2032                    .map_or_else(|| "N/A".to_string(), |s| s.to_string()),
2033                estimation_result.compute_units_consumed,
2034                estimation_result.success,
2035                estimation_result.log_messages,
2036                estimation_result.error_message
2037            )));
2038        }
2039        self.transactions_processed += 1;
2040
2041        if !self.validate_transaction_blockhash(&tx) {
2042            let meta = TransactionMetadata::default();
2043            let err = solana_transaction_error::TransactionError::BlockhashNotFound;
2044
2045            let transaction_meta = convert_transaction_metadata_from_canonical(&meta);
2046
2047            let _ = self
2048                .simnet_events_tx
2049                .try_send(SimnetEvent::transaction_processed(
2050                    transaction_meta,
2051                    Some(err.clone()),
2052                ));
2053            return Err(FailedTransactionMetadata { err, meta });
2054        }
2055
2056        match self.inner.send_transaction(tx.clone()) {
2057            Ok(res) => Ok(res),
2058            Err(tx_failure) => {
2059                let transaction_meta =
2060                    convert_transaction_metadata_from_canonical(&tx_failure.meta);
2061
2062                let _ = self
2063                    .simnet_events_tx
2064                    .try_send(SimnetEvent::transaction_processed(
2065                        transaction_meta,
2066                        Some(tx_failure.err.clone()),
2067                    ));
2068                Err(tx_failure)
2069            }
2070        }
2071    }
2072
2073    /// Estimates the compute units that a transaction will consume by simulating it.
2074    ///
2075    /// Does not commit any state changes to the SVM.
2076    ///
2077    /// # Arguments
2078    /// * `transaction` - The transaction to simulate.
2079    ///
2080    /// # Returns
2081    /// A `ComputeUnitsEstimationResult` with simulation details.
2082    pub fn estimate_compute_units(
2083        &self,
2084        transaction: &VersionedTransaction,
2085    ) -> ComputeUnitsEstimationResult {
2086        if !self.validate_transaction_blockhash(transaction) {
2087            return ComputeUnitsEstimationResult {
2088                success: false,
2089                compute_units_consumed: 0,
2090                log_messages: None,
2091                error_message: Some(
2092                    solana_transaction_error::TransactionError::BlockhashNotFound.to_string(),
2093                ),
2094            };
2095        }
2096
2097        match self.inner.simulate_transaction(transaction.clone()) {
2098            Ok(sim_info) => ComputeUnitsEstimationResult {
2099                success: true,
2100                compute_units_consumed: sim_info.meta.compute_units_consumed,
2101                log_messages: Some(sim_info.meta.logs),
2102                error_message: None,
2103            },
2104            Err(failed_meta) => ComputeUnitsEstimationResult {
2105                success: false,
2106                compute_units_consumed: failed_meta.meta.compute_units_consumed,
2107                log_messages: Some(failed_meta.meta.logs),
2108                error_message: Some(failed_meta.err.to_string()),
2109            },
2110        }
2111    }
2112
2113    /// Simulates a transaction and returns detailed simulation info or failure metadata.
2114    ///
2115    /// # Arguments
2116    /// * `tx` - The transaction to simulate.
2117    ///
2118    /// # Returns
2119    /// `Ok(SimulatedTransactionInfo)` if successful, or `Err(FailedTransactionMetadata)` if failed.
2120    #[allow(clippy::result_large_err)]
2121    pub fn simulate_transaction(
2122        &self,
2123        tx: VersionedTransaction,
2124        sigverify: bool,
2125    ) -> Result<SimulatedTransactionInfo, FailedTransactionMetadata> {
2126        if sigverify {
2127            self.sigverify(&tx)?;
2128        }
2129
2130        if !self.validate_transaction_blockhash(&tx) {
2131            let meta = TransactionMetadata::default();
2132            let err = TransactionError::BlockhashNotFound;
2133
2134            return Err(FailedTransactionMetadata { err, meta });
2135        }
2136        self.inner.simulate_transaction(tx)
2137    }
2138
2139    /// Confirms transactions queued for confirmation, updates epoch/slot, and sends events.
2140    ///
2141    /// # Returns
2142    /// `Ok((Vec<Signature>, u64))` with confirmed signatures and the number
2143    /// of transactions whose execution returned an error, or
2144    /// `Err(SurfpoolError)` on error. The failure count powers the `stats`
2145    /// field of `SlotUpdate::Frozen` notifications emitted to
2146    /// `slotsUpdatesSubscribe` clients.
2147    fn confirm_transactions(&mut self) -> Result<(Vec<Signature>, u64), SurfpoolError> {
2148        let mut confirmed_transactions = vec![];
2149        let mut num_failed: u64 = 0;
2150        let slot = self.latest_epoch_info.slot_index;
2151        let current_slot = self.latest_epoch_info.absolute_slot;
2152
2153        while let Some((tx, status_tx, error)) =
2154            self.transactions_queued_for_confirmation.pop_front()
2155        {
2156            if error.is_some() {
2157                num_failed = num_failed.saturating_add(1);
2158            }
2159            let _ = status_tx.try_send(TransactionStatusEvent::Success(
2160                TransactionConfirmationStatus::Confirmed,
2161            ));
2162            let signature = tx.signatures[0];
2163            let finalized_at = self.latest_epoch_info.absolute_slot + FINALIZATION_SLOT_THRESHOLD;
2164            self.transactions_queued_for_finalization.push_back((
2165                finalized_at,
2166                tx,
2167                status_tx,
2168                error.clone(),
2169            ));
2170
2171            self.notify_signature_subscribers(
2172                SignatureSubscriptionType::confirmed(),
2173                &signature,
2174                slot,
2175                error,
2176            );
2177
2178            let Some(SurfnetTransactionStatus::Processed(tx_data)) =
2179                self.transactions.get(&signature.to_string()).ok().flatten()
2180            else {
2181                continue;
2182            };
2183            let (tx_with_status_meta, mutated_account_keys) = tx_data.as_ref();
2184
2185            for pubkey in mutated_account_keys {
2186                self.account_update_slots.insert(*pubkey, current_slot);
2187            }
2188
2189            self.notify_logs_subscribers(
2190                &signature,
2191                None,
2192                tx_with_status_meta
2193                    .meta
2194                    .log_messages
2195                    .clone()
2196                    .unwrap_or(vec![]),
2197                CommitmentLevel::Confirmed,
2198            );
2199            confirmed_transactions.push(signature);
2200        }
2201
2202        Ok((confirmed_transactions, num_failed))
2203    }
2204
2205    /// Finalizes transactions queued for finalization, sending finalized events as needed.
2206    ///
2207    /// # Returns
2208    /// `Ok(())` on success, or `Err(SurfpoolError)` on error.
2209    fn finalize_transactions(&mut self) -> Result<(), SurfpoolError> {
2210        let current_slot = self.latest_epoch_info.absolute_slot;
2211        let mut requeue = VecDeque::new();
2212        while let Some((finalized_at, tx, status_tx, error)) =
2213            self.transactions_queued_for_finalization.pop_front()
2214        {
2215            if current_slot >= finalized_at {
2216                let _ = status_tx.try_send(TransactionStatusEvent::Success(
2217                    TransactionConfirmationStatus::Finalized,
2218                ));
2219                let signature = &tx.signatures[0];
2220                self.notify_signature_subscribers(
2221                    SignatureSubscriptionType::finalized(),
2222                    signature,
2223                    self.latest_epoch_info.absolute_slot,
2224                    error,
2225                );
2226                let Some(SurfnetTransactionStatus::Processed(tx_data)) =
2227                    self.transactions.get(&signature.to_string()).ok().flatten()
2228                else {
2229                    continue;
2230                };
2231                let (tx_with_status_meta, _) = tx_data.as_ref();
2232                let logs = tx_with_status_meta
2233                    .meta
2234                    .log_messages
2235                    .clone()
2236                    .unwrap_or(vec![]);
2237                self.notify_logs_subscribers(signature, None, logs, CommitmentLevel::Finalized);
2238            } else {
2239                requeue.push_back((finalized_at, tx, status_tx, error));
2240            }
2241        }
2242        // Requeue any transactions that are not yet finalized
2243        self.transactions_queued_for_finalization
2244            .append(&mut requeue);
2245
2246        Ok(())
2247    }
2248
2249    /// Writes account updates to the SVM state based on the provided account update result.
2250    ///
2251    /// # Arguments
2252    /// * `account_update` - The account update result to process.
2253    pub fn write_account_update(&mut self, account_update: GetAccountResult) {
2254        let init_programdata_account = |program_account: &Account| {
2255            if !program_account.executable {
2256                return None;
2257            }
2258            if !program_account
2259                .owner
2260                .eq(&solana_sdk_ids::bpf_loader_upgradeable::id())
2261            {
2262                return None;
2263            }
2264            let Ok(UpgradeableLoaderState::Program {
2265                programdata_address,
2266            }) = bincode::deserialize::<UpgradeableLoaderState>(&program_account.data)
2267            else {
2268                return None;
2269            };
2270
2271            let programdata_state = UpgradeableLoaderState::ProgramData {
2272                upgrade_authority_address: Some(system_program::id()),
2273                slot: self.get_latest_absolute_slot(),
2274            };
2275            let mut data = bincode::serialize(&programdata_state).unwrap();
2276
2277            data.extend_from_slice(crate::surfnet::noop_program::NOOP_PROGRAM_ELF);
2278            let lamports = self.inner.minimum_balance_for_rent_exemption(data.len());
2279            Some((
2280                programdata_address,
2281                Account {
2282                    lamports,
2283                    data,
2284                    owner: solana_sdk_ids::bpf_loader_upgradeable::id(),
2285                    executable: false,
2286                    rent_epoch: 0,
2287                },
2288            ))
2289        };
2290        match account_update {
2291            GetAccountResult::FoundAccount(pubkey, account, do_update_account) => {
2292                if do_update_account {
2293                    if let Some((programdata_address, programdata_account)) =
2294                        init_programdata_account(&account)
2295                    {
2296                        match self.get_account(&programdata_address) {
2297                            Ok(None) => {
2298                                if let Err(e) =
2299                                    self.set_account(&programdata_address, programdata_account)
2300                                {
2301                                    let _ = self
2302                                        .simnet_events_tx
2303                                        .send(SimnetEvent::error(e.to_string()));
2304                                }
2305                            }
2306                            Ok(Some(_)) => {}
2307                            Err(e) => {
2308                                let _ = self
2309                                    .simnet_events_tx
2310                                    .send(SimnetEvent::error(e.to_string()));
2311                            }
2312                        }
2313                    }
2314                    if let Err(e) = self.set_account(&pubkey, account.clone()) {
2315                        let _ = self
2316                            .simnet_events_tx
2317                            .send(SimnetEvent::error(e.to_string()));
2318                    }
2319                }
2320            }
2321            GetAccountResult::FoundProgramAccount((pubkey, account), (_, None)) => {
2322                if let Some((programdata_address, programdata_account)) =
2323                    init_programdata_account(&account)
2324                {
2325                    match self.get_account(&programdata_address) {
2326                        Ok(None) => {
2327                            if let Err(e) =
2328                                self.set_account(&programdata_address, programdata_account)
2329                            {
2330                                let _ = self
2331                                    .simnet_events_tx
2332                                    .send(SimnetEvent::error(e.to_string()));
2333                            }
2334                        }
2335                        Ok(Some(_)) => {}
2336                        Err(e) => {
2337                            let _ = self
2338                                .simnet_events_tx
2339                                .send(SimnetEvent::error(e.to_string()));
2340                        }
2341                    }
2342                }
2343                if let Err(e) = self.set_account(&pubkey, account.clone()) {
2344                    let _ = self
2345                        .simnet_events_tx
2346                        .send(SimnetEvent::error(e.to_string()));
2347                }
2348            }
2349            GetAccountResult::FoundTokenAccount((pubkey, account), (_, None)) => {
2350                if let Err(e) = self.set_account(&pubkey, account.clone()) {
2351                    let _ = self
2352                        .simnet_events_tx
2353                        .send(SimnetEvent::error(e.to_string()));
2354                }
2355            }
2356            GetAccountResult::FoundProgramAccount(
2357                (pubkey, account),
2358                (coupled_pubkey, Some(coupled_account)),
2359            )
2360            | GetAccountResult::FoundTokenAccount(
2361                (pubkey, account),
2362                (coupled_pubkey, Some(coupled_account)),
2363            ) => {
2364                // The data account _must_ be set first, as the program account depends on it.
2365                if let Err(e) = self.set_account(&coupled_pubkey, coupled_account.clone()) {
2366                    let _ = self
2367                        .simnet_events_tx
2368                        .send(SimnetEvent::error(e.to_string()));
2369                }
2370                if let Err(e) = self.set_account(&pubkey, account.clone()) {
2371                    let _ = self
2372                        .simnet_events_tx
2373                        .send(SimnetEvent::error(e.to_string()));
2374                }
2375            }
2376            GetAccountResult::None(_) => {}
2377        }
2378    }
2379
2380    pub fn confirm_current_block(&mut self) -> SurfpoolResult<()> {
2381        let slot = self.get_latest_absolute_slot();
2382        // `slotsUpdatesSubscribe` clients expect millisecond-precision Unix
2383        // timestamps regardless of the simulator's slot_time, so we anchor
2384        // every variant emitted from this function to a single wall-clock
2385        // sample. See https://solana.com/docs/rpc/websocket/slotsupdatessubscribe
2386        let slots_update_ts: u64 = Utc::now().timestamp_millis().max(0) as u64;
2387        let previous_chain_tip = self.chain_tip.clone();
2388        if slot % *GARBAGE_COLLECTION_INTERVAL_SLOTS == 0 {
2389            debug!("Clearing liteSVM cache at slot {}", slot);
2390            self.inner.garbage_collect(self.feature_set.clone());
2391        }
2392        self.chain_tip = self.new_blockhash();
2393        // Confirm processed transactions
2394        let (confirmed_signatures, num_failed_transactions) = self.confirm_transactions()?;
2395
2396        let num_transactions = confirmed_signatures.len() as u64;
2397        let num_successful_transactions = num_transactions.saturating_sub(num_failed_transactions);
2398        self.updated_at += self.slot_time;
2399
2400        // Only store blocks that have transactions (sparse block storage)
2401        // Empty blocks can be reconstructed on-the-fly from their slot number
2402        if !confirmed_signatures.is_empty() {
2403            self.blocks.store(
2404                slot,
2405                BlockHeader {
2406                    hash: self.chain_tip.hash.clone(),
2407                    previous_blockhash: previous_chain_tip.hash.clone(),
2408                    block_time: self.updated_at as i64 / 1_000,
2409                    block_height: self.chain_tip.index,
2410                    parent_slot: slot,
2411                    signatures: confirmed_signatures,
2412                },
2413            )?;
2414        }
2415
2416        // Checkpoint the latest slot periodically (~every 150 slots / 1 minute at standard slot time)
2417        // This allows recovery after restart without storing every empty block
2418        if slot.saturating_sub(self.last_checkpoint_slot) >= *CHECKPOINT_INTERVAL_SLOTS {
2419            self.slot_checkpoint
2420                .store("latest_slot".to_string(), slot)?;
2421            self.last_checkpoint_slot = slot;
2422        }
2423
2424        if self.perf_samples.len() > 30 {
2425            self.perf_samples.pop_back();
2426        }
2427        self.perf_samples.push_front(RpcPerfSample {
2428            slot,
2429            num_slots: 1,
2430            sample_period_secs: 1,
2431            num_transactions,
2432            num_non_vote_transactions: Some(num_transactions),
2433        });
2434
2435        self.latest_epoch_info.slot_index += 1;
2436        self.latest_epoch_info.block_height = self.chain_tip.index;
2437        self.latest_epoch_info.absolute_slot += 1;
2438        if self.latest_epoch_info.slot_index > self.latest_epoch_info.slots_in_epoch {
2439            self.latest_epoch_info.slot_index = 0;
2440            self.latest_epoch_info.epoch += 1;
2441        }
2442        let total_transactions = self.latest_epoch_info.transaction_count.unwrap_or(0);
2443        self.latest_epoch_info.transaction_count = Some(total_transactions + num_transactions);
2444
2445        let parent_slot = self.latest_epoch_info.absolute_slot.saturating_sub(1);
2446        let new_slot = self.latest_epoch_info.absolute_slot;
2447        let root = new_slot.saturating_sub(FINALIZATION_SLOT_THRESHOLD);
2448        self.notify_slot_subscribers(new_slot, parent_slot, root);
2449
2450        // Emit `slotsUpdatesNotification` events for the lifecycle transition
2451        // we just completed:
2452        //   * `Frozen`     – the slot that was just closed (`slot`) is now
2453        //                    immutable; surface its execution stats.
2454        //   * `CreatedBank`– the next slot (`new_slot`) has just been opened
2455        //                    on top of `parent_slot` (= the freshly-frozen slot).
2456        // Surfpool's execution model has no gossip layer, so the
2457        // `FirstShredReceived` / `Completed` / `Dead` variants documented by
2458        // Solana are intentionally not produced here.
2459        // Surfpool executes transactions sequentially in a single entry per
2460        // block, unlike real Solana where a block can contain multiple entries
2461        // (parallel execution batches). As a result `max_transactions_per_entry`
2462        // equals the total transaction count for the slot.
2463        const SURFPOOL_ENTRIES_PER_BLOCK: u64 = 1;
2464        self.notify_slots_updates_subscribers(SlotUpdate::Frozen {
2465            slot,
2466            timestamp: slots_update_ts,
2467            stats: SlotTransactionStats {
2468                num_transaction_entries: SURFPOOL_ENTRIES_PER_BLOCK,
2469                num_successful_transactions,
2470                num_failed_transactions,
2471                max_transactions_per_entry: num_transactions,
2472            },
2473        });
2474        self.notify_slots_updates_subscribers(SlotUpdate::CreatedBank {
2475            slot: new_slot,
2476            parent: parent_slot,
2477            timestamp: slots_update_ts,
2478        });
2479
2480        let geyser_parent_slot = slot.saturating_sub(1);
2481
2482        // Emit confirmation for the same slot used by processed account/transaction updates.
2483        self.geyser_events_tx
2484            .send(GeyserEvent::UpdateSlotStatus {
2485                slot,
2486                parent: slot.checked_sub(1),
2487                status: GeyserSlotStatus::Confirmed,
2488            })
2489            .ok();
2490        // Mirror the Confirmed Geyser event as an `OptimisticConfirmation`
2491        // notification for `slotsUpdatesSubscribe` clients.
2492        self.notify_slots_updates_subscribers(SlotUpdate::OptimisticConfirmation {
2493            slot,
2494            timestamp: slots_update_ts,
2495        });
2496
2497        // Notify geyser plugins of block metadata
2498        let block_metadata = GeyserBlockMetadata {
2499            slot,
2500            blockhash: self.chain_tip.hash.clone(),
2501            parent_slot: geyser_parent_slot,
2502            parent_blockhash: previous_chain_tip.hash.clone(),
2503            block_time: Some(self.updated_at as i64 / 1_000),
2504            block_height: Some(self.chain_tip.index),
2505            executed_transaction_count: num_transactions,
2506            entry_count: 1, // Surfpool produces 1 entry per block
2507        };
2508        self.geyser_events_tx
2509            .send(GeyserEvent::NotifyBlockMetadata(block_metadata))
2510            .ok();
2511
2512        // Notify geyser plugins of entry (Surfpool emits 1 entry per block)
2513        let entry_hash = solana_hash::Hash::from_str(&self.chain_tip.hash)
2514            .map(|h| h.to_bytes().to_vec())
2515            .unwrap_or_else(|_| vec![0u8; 32]);
2516        let entry_info = GeyserEntryInfo {
2517            slot,
2518            index: 0, // Single entry per block
2519            num_hashes: 1,
2520            hash: entry_hash,
2521            executed_transaction_count: num_transactions,
2522            starting_transaction_index: 0,
2523        };
2524        self.geyser_events_tx
2525            .send(GeyserEvent::NotifyEntry(entry_info))
2526            .ok();
2527
2528        let clock: Clock = Clock {
2529            slot: self.latest_epoch_info.absolute_slot,
2530            epoch: self.latest_epoch_info.epoch,
2531            unix_timestamp: self.updated_at as i64 / 1_000,
2532            epoch_start_timestamp: 0, // todo
2533            leader_schedule_epoch: 0, // todo
2534        };
2535
2536        let _ = self
2537            .simnet_events_tx
2538            .send(SimnetEvent::SystemClockUpdated(clock.clone()));
2539        self.inner.set_sysvar(&clock);
2540
2541        self.finalize_transactions()?;
2542
2543        // Notify geyser plugins of newly rooted (finalized) slot
2544        // Only emit if root is a valid slot (greater than genesis)
2545        if root >= self.genesis_slot {
2546            self.geyser_events_tx
2547                .send(GeyserEvent::UpdateSlotStatus {
2548                    slot: root,
2549                    parent: root.checked_sub(1),
2550                    status: GeyserSlotStatus::Rooted,
2551                })
2552                .ok();
2553            // Mirror the Rooted Geyser event as a `Root` notification for
2554            // `slotsUpdatesSubscribe` clients.
2555            self.notify_slots_updates_subscribers(SlotUpdate::Root {
2556                slot: root,
2557                timestamp: slots_update_ts,
2558            });
2559        }
2560
2561        // Evict the accounts marked as streamed from cache to enforce them to be fetched again
2562        let accounts_to_reset: Vec<_> = self.streamed_accounts.into_iter()?.collect();
2563        for (pubkey_str, include_owned_accounts) in accounts_to_reset {
2564            let pubkey = Pubkey::from_str(&pubkey_str)
2565                .map_err(|e| SurfpoolError::invalid_pubkey(&pubkey_str, e.to_string()))?;
2566            self.reset_account(&pubkey, include_owned_accounts)?;
2567        }
2568
2569        Ok(())
2570    }
2571
2572    /// Materializes scheduled overrides for the current slot
2573    ///
2574    /// This function:
2575    /// 1. Dequeues overrides scheduled for the current slot
2576    /// 2. Resolves account addresses (Pubkey or PDA)
2577    /// 3. Optionally fetches fresh account data from remote if `fetch_before_use` is enabled
2578    /// 4. Applies the overrides to the account data
2579    /// 5. Updates the SVM state
2580    pub async fn materialize_overrides(
2581        &mut self,
2582        remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>,
2583    ) -> SurfpoolResult<()> {
2584        let current_slot = self.latest_epoch_info.absolute_slot;
2585
2586        self.materialize_overrides_for_slot(remote_ctx, current_slot)
2587            .await
2588    }
2589
2590    /// Materializes scheduled overrides for a specific slot
2591    pub async fn materialize_overrides_for_slot(
2592        &mut self,
2593        remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>,
2594        target_slot: Slot,
2595    ) -> SurfpoolResult<()> {
2596        // Remove and get overrides for this slot
2597        let Some(overrides) = self.scheduled_overrides.take(&target_slot)? else {
2598            // No overrides for this slot
2599            return Ok(());
2600        };
2601
2602        debug!(
2603            "Materializing {} override(s) for slot {}",
2604            overrides.len(),
2605            target_slot
2606        );
2607
2608        for override_instance in overrides {
2609            if !override_instance.enabled {
2610                debug!("Skipping disabled override: {}", override_instance.id);
2611                continue;
2612            }
2613
2614            // Resolve account address using the centralized method
2615            let account_pubkey = match override_instance
2616                .account
2617                .resolve(Some(&override_instance.values))
2618            {
2619                Some(pubkey) => {
2620                    if matches!(
2621                        &override_instance.account,
2622                        surfpool_types::AccountAddress::Pda { .. }
2623                    ) {
2624                        debug!(
2625                            "Derived PDA {} for override {}",
2626                            pubkey, override_instance.id
2627                        );
2628                    }
2629                    pubkey
2630                }
2631                None => {
2632                    warn!(
2633                        "Failed to resolve account address for override {}",
2634                        override_instance.id
2635                    );
2636                    continue;
2637                }
2638            };
2639
2640            debug!(
2641                "Processing override {} for account {} (label: {:?})",
2642                override_instance.id, account_pubkey, override_instance.label
2643            );
2644
2645            // Fetch fresh account data from remote if requested
2646            if override_instance.fetch_before_use {
2647                if let Some((client, _)) = remote_ctx {
2648                    debug!(
2649                        "Fetching fresh account data for {} from remote",
2650                        account_pubkey
2651                    );
2652
2653                    match client
2654                        .get_account(&account_pubkey, CommitmentConfig::confirmed())
2655                        .await
2656                    {
2657                        Ok(GetAccountResult::FoundAccount(_pubkey, remote_account, _)) => {
2658                            debug!(
2659                                "Fetched account {} from remote: {} lamports, {} bytes",
2660                                account_pubkey,
2661                                remote_account.lamports(),
2662                                remote_account.data().len()
2663                            );
2664
2665                            // Set the fresh account data in the SVM
2666                            if let Err(e) = self.inner.set_account(account_pubkey, remote_account) {
2667                                warn!(
2668                                    "Failed to set account {} from remote: {}",
2669                                    account_pubkey, e
2670                                );
2671                            }
2672                        }
2673                        Ok(GetAccountResult::None(_)) => {
2674                            debug!("Account {} not found on remote", account_pubkey);
2675                        }
2676                        Ok(_) => {
2677                            debug!("Account {} fetched (other variant)", account_pubkey);
2678                        }
2679                        Err(e) => {
2680                            warn!(
2681                                "Failed to fetch account {} from remote: {}",
2682                                account_pubkey, e
2683                            );
2684                        }
2685                    }
2686                } else {
2687                    debug!(
2688                        "fetch_before_use enabled but no remote client available for override {}",
2689                        override_instance.id
2690                    );
2691                }
2692            }
2693
2694            // Apply the override values to the account data
2695            if !override_instance.values.is_empty() {
2696                // Filter out values that are only used for PDA derivation (not account data)
2697                let pda_refs = override_instance.account.get_pda_seed_references();
2698                let account_values: HashMap<String, serde_json::Value> = override_instance
2699                    .values
2700                    .iter()
2701                    .filter(|(key, _)| !pda_refs.contains(key))
2702                    .map(|(k, v)| (k.clone(), v.clone()))
2703                    .collect();
2704
2705                if account_values.is_empty() {
2706                    debug!(
2707                        "Override {} has no account data modifications (all values are PDA seeds)",
2708                        override_instance.id
2709                    );
2710                    continue;
2711                }
2712
2713                debug!(
2714                    "Override {} applying {} field modification(s) to account {} (filtered {} PDA seed refs)",
2715                    override_instance.id,
2716                    account_values.len(),
2717                    account_pubkey,
2718                    pda_refs.len()
2719                );
2720
2721                // Get the account from the SVM
2722                let Some(account) = self.inner.get_account(&account_pubkey)? else {
2723                    warn!(
2724                        "Account {} not found in SVM for override {}, skipping modifications",
2725                        account_pubkey, override_instance.id
2726                    );
2727                    continue;
2728                };
2729
2730                // Get the account owner (program ID)
2731                let owner_program_id = account.owner();
2732
2733                // Look up the IDL for the owner program
2734                let idl_versions = match self.registered_idls.get(&owner_program_id.to_string()) {
2735                    Ok(Some(versions)) => versions,
2736                    Ok(None) => {
2737                        warn!(
2738                            "No IDL registered for program {} (owner of account {}), skipping override {}",
2739                            owner_program_id, account_pubkey, override_instance.id
2740                        );
2741                        continue;
2742                    }
2743                    Err(e) => {
2744                        warn!(
2745                            "Failed to get IDL for program {}: {}, skipping override {}",
2746                            owner_program_id, e, override_instance.id
2747                        );
2748                        continue;
2749                    }
2750                };
2751
2752                // Get the latest IDL version (first in the sorted Vec)
2753                let Some(versioned_idl) = idl_versions.first() else {
2754                    warn!(
2755                        "IDL versions empty for program {}, skipping override {}",
2756                        owner_program_id, override_instance.id
2757                    );
2758                    continue;
2759                };
2760
2761                let idl = &versioned_idl.1;
2762
2763                // Get account data
2764                let account_data = account.data();
2765
2766                // Check if account data is valid (has at least discriminator)
2767                if account_data.len() < 8 {
2768                    warn!(
2769                        "Account {} has insufficient data ({} bytes) for override {}. \
2770                        Enable fetchBeforeUse: true to fetch account data from mainnet first.",
2771                        account_pubkey,
2772                        account_data.len(),
2773                        override_instance.id
2774                    );
2775                    continue;
2776                }
2777
2778                // Use get_forged_account_data to apply the overrides (with PDA refs filtered out)
2779                let new_account_data = match self.get_forged_account_data(
2780                    &account_pubkey,
2781                    account_data,
2782                    idl,
2783                    &account_values,
2784                ) {
2785                    Ok(data) => data,
2786                    Err(e) => {
2787                        warn!(
2788                            "Failed to forge account data for {} (override {}): {}. \
2789                            If the account doesn't exist locally, enable fetchBeforeUse: true.",
2790                            account_pubkey, override_instance.id, e
2791                        );
2792                        continue;
2793                    }
2794                };
2795
2796                // Create a new account with modified data
2797                let modified_account = Account {
2798                    lamports: account.lamports(),
2799                    data: new_account_data,
2800                    owner: *account.owner(),
2801                    executable: account.executable(),
2802                    rent_epoch: account.rent_epoch(),
2803                };
2804
2805                // Update the account in the SVM
2806                if let Err(e) = self.inner.set_account(account_pubkey, modified_account) {
2807                    warn!(
2808                        "Failed to set modified account {} in SVM: {}",
2809                        account_pubkey, e
2810                    );
2811                } else {
2812                    debug!(
2813                        "Successfully applied {} override(s) to account {} (override {})",
2814                        override_instance.values.len(),
2815                        account_pubkey,
2816                        override_instance.id
2817                    );
2818                }
2819            }
2820        }
2821
2822        Ok(())
2823    }
2824
2825    /// Forges account data by applying overrides to existing account data
2826    ///
2827    /// This function:
2828    /// 1. Validates account data size (must be at least 8 bytes for discriminator)
2829    /// 2. Splits discriminator and serialized data
2830    /// 3. Finds the account type in the IDL using the discriminator
2831    /// 4. Deserializes the account data
2832    /// 5. Applies field overrides using dot notation
2833    /// 6. Re-serializes the modified data
2834    /// 7. Reconstructs the account data with the original discriminator
2835    ///
2836    /// # Arguments
2837    /// * `account_pubkey` - The account address (for error messages)
2838    /// * `account_data` - The original account data bytes
2839    /// * `idl` - The IDL for the account's program
2840    /// * `overrides` - Map of field paths to new values
2841    ///
2842    /// # Returns
2843    /// The forged account data as bytes, or an error
2844    pub fn get_forged_account_data(
2845        &self,
2846        account_pubkey: &Pubkey,
2847        account_data: &[u8],
2848        idl: &Idl,
2849        overrides: &HashMap<String, serde_json::Value>,
2850    ) -> SurfpoolResult<Vec<u8>> {
2851        // Validate account data size
2852        if account_data.len() < 8 {
2853            return Err(SurfpoolError::invalid_account_data(
2854                account_pubkey,
2855                "Account data too small to be an Anchor account (need at least 8 bytes for discriminator)",
2856                Some("Data length too small"),
2857            ));
2858        }
2859
2860        // Split discriminator and data
2861        let discriminator = &account_data[..8];
2862        let serialized_data = &account_data[8..];
2863
2864        // Find the account type using the discriminator
2865        let account_def = idl
2866            .accounts
2867            .iter()
2868            .find(|acc| acc.discriminator.eq(discriminator))
2869            .ok_or_else(|| {
2870                SurfpoolError::internal(format!(
2871                    "Account with discriminator '{:?}' not found in IDL",
2872                    discriminator
2873                ))
2874            })?;
2875
2876        // Find the corresponding type definition
2877        let account_type = idl
2878            .types
2879            .iter()
2880            .find(|t| t.name == account_def.name)
2881            .ok_or_else(|| {
2882                SurfpoolError::internal(format!(
2883                    "Type definition for account '{}' not found in IDL",
2884                    account_def.name
2885                ))
2886            })?;
2887
2888        // Set up generics for parsing
2889        let empty_vec = vec![];
2890        let idl_type_def_generics = idl
2891            .types
2892            .iter()
2893            .find(|t| t.name == account_type.name)
2894            .map(|t| &t.generics);
2895
2896        // Deserialize the account data using proper Borsh deserialization
2897        // Use the version that returns leftover bytes to preserve any trailing padding
2898        let (mut parsed_value, leftover_bytes) =
2899            parse_bytes_to_value_with_expected_idl_type_def_ty_with_leftover_bytes(
2900                serialized_data,
2901                &account_type.ty,
2902                &idl.types,
2903                &vec![],
2904                idl_type_def_generics.unwrap_or(&empty_vec),
2905            )
2906            .map_err(|e| {
2907                SurfpoolError::deserialize_error(
2908                    "account data",
2909                    format!("Failed to deserialize account data using Borsh: {}", e),
2910                )
2911            })?;
2912
2913        // Apply overrides to the decoded value
2914        for (path, value) in overrides {
2915            apply_override_to_decoded_account(&mut parsed_value, path, value)?;
2916        }
2917
2918        // Construct an IdlType::Defined that references the account type
2919        // This is needed because borsh_encode_value_to_idl_type expects IdlType, not IdlTypeDefTy
2920        use anchor_lang_idl::types::{IdlGenericArg, IdlType};
2921        let defined_type = IdlType::Defined {
2922            name: account_type.name.clone(),
2923            generics: account_type
2924                .generics
2925                .iter()
2926                .map(|_| IdlGenericArg::Type {
2927                    ty: IdlType::String,
2928                })
2929                .collect(),
2930        };
2931
2932        // Re-encode the value using Borsh
2933        let re_encoded_data =
2934            borsh_encode_value_to_idl_type(&parsed_value, &defined_type, &idl.types, None)
2935                .map_err(|e| {
2936                    SurfpoolError::internal(format!(
2937                        "Failed to re-encode account data using Borsh: {}",
2938                        e
2939                    ))
2940                })?;
2941
2942        // Reconstruct the account data with discriminator and preserve any trailing bytes
2943        let mut new_account_data =
2944            Vec::with_capacity(8 + re_encoded_data.len() + leftover_bytes.len());
2945        new_account_data.extend_from_slice(discriminator);
2946        new_account_data.extend_from_slice(&re_encoded_data);
2947        new_account_data.extend_from_slice(leftover_bytes);
2948
2949        Ok(new_account_data)
2950    }
2951
2952    /// Subscribes for updates on a transaction signature for a given subscription type.
2953    ///
2954    /// # Arguments
2955    /// * `signature` - The transaction signature to subscribe to.
2956    /// * `subscription_type` - The type of subscription (confirmed/finalized).
2957    ///
2958    /// # Returns
2959    /// A receiver for slot and transaction error updates.
2960    pub fn subscribe_for_signature_updates(
2961        &mut self,
2962        signature: &Signature,
2963        subscription_type: SignatureSubscriptionType,
2964    ) -> Receiver<(Slot, Option<TransactionError>)> {
2965        let (tx, rx) = unbounded();
2966        self.signature_subscriptions
2967            .entry(*signature)
2968            .or_default()
2969            .push((subscription_type, tx));
2970        rx
2971    }
2972
2973    pub fn subscribe_for_account_updates(
2974        &mut self,
2975        account_pubkey: &Pubkey,
2976        encoding: Option<UiAccountEncoding>,
2977    ) -> Receiver<UiAccount> {
2978        let (tx, rx) = unbounded();
2979        self.account_subscriptions
2980            .entry(*account_pubkey)
2981            .or_default()
2982            .push((encoding, tx));
2983        rx
2984    }
2985
2986    pub fn subscribe_for_program_updates(
2987        &mut self,
2988        program_id: &Pubkey,
2989        encoding: Option<UiAccountEncoding>,
2990        filters: Option<Vec<RpcFilterType>>,
2991    ) -> Receiver<RpcKeyedAccount> {
2992        let (tx, rx) = unbounded();
2993        self.program_subscriptions
2994            .entry(*program_id)
2995            .or_default()
2996            .push((encoding, filters, tx));
2997        rx
2998    }
2999
3000    /// Notifies signature subscribers of a status update, sending slot and error info.
3001    ///
3002    /// # Arguments
3003    /// * `status` - The subscription type (confirmed/finalized).
3004    /// * `signature` - The transaction signature.
3005    /// * `slot` - The slot number.
3006    /// * `err` - Optional transaction error.
3007    pub fn notify_signature_subscribers(
3008        &mut self,
3009        status: SignatureSubscriptionType,
3010        signature: &Signature,
3011        slot: Slot,
3012        err: Option<TransactionError>,
3013    ) {
3014        let mut remaining = vec![];
3015        if let Some(subscriptions) = self.signature_subscriptions.remove(signature) {
3016            for (subscription_type, tx) in subscriptions {
3017                if status.eq(&subscription_type) {
3018                    if tx.send((slot, err.clone())).is_err() {
3019                        // The receiver has been dropped, so we can skip notifying
3020                        continue;
3021                    }
3022                } else {
3023                    remaining.push((subscription_type, tx));
3024                }
3025            }
3026            if !remaining.is_empty() {
3027                self.signature_subscriptions.insert(*signature, remaining);
3028            }
3029        }
3030    }
3031
3032    pub fn notify_account_subscribers(
3033        &mut self,
3034        account_updated_pubkey: &Pubkey,
3035        account: &Account,
3036    ) {
3037        let mut remaining = vec![];
3038        if let Some(subscriptions) = self.account_subscriptions.remove(account_updated_pubkey) {
3039            for (encoding, tx) in subscriptions {
3040                let config = RpcAccountInfoConfig {
3041                    encoding,
3042                    ..Default::default()
3043                };
3044                let account = self
3045                    .account_to_rpc_keyed_account(account_updated_pubkey, account, &config, None)
3046                    .account;
3047                if tx.send(account).is_err() {
3048                    // The receiver has been dropped, so we can skip notifying
3049                    continue;
3050                } else {
3051                    remaining.push((encoding, tx));
3052                }
3053            }
3054            if !remaining.is_empty() {
3055                self.account_subscriptions
3056                    .insert(*account_updated_pubkey, remaining);
3057            }
3058        }
3059    }
3060
3061    pub fn notify_program_subscribers(&mut self, account_pubkey: &Pubkey, account: &Account) {
3062        let program_id = account.owner;
3063        let mut remaining = vec![];
3064        if let Some(subscriptions) = self.program_subscriptions.remove(&program_id) {
3065            for (encoding, filters, tx) in subscriptions {
3066                // Apply filters if present
3067                if let Some(ref active_filters) = filters {
3068                    match super::locker::apply_rpc_filters(&account.data, active_filters) {
3069                        Ok(true) => {} // Account matches all filters
3070                        Ok(false) => {
3071                            // Filtered out - keep subscription active but don't notify
3072                            remaining.push((encoding, filters, tx));
3073                            continue;
3074                        }
3075                        Err(_) => {
3076                            // Error applying filter - keep subscription, skip notification
3077                            remaining.push((encoding, filters, tx));
3078                            continue;
3079                        }
3080                    }
3081                }
3082
3083                let config = RpcAccountInfoConfig {
3084                    encoding,
3085                    ..Default::default()
3086                };
3087                let keyed_account =
3088                    self.account_to_rpc_keyed_account(account_pubkey, account, &config, None);
3089                if tx.send(keyed_account).is_err() {
3090                    // The receiver has been dropped, so we can skip notifying
3091                    continue;
3092                } else {
3093                    remaining.push((encoding, filters, tx));
3094                }
3095            }
3096            if !remaining.is_empty() {
3097                self.program_subscriptions.insert(program_id, remaining);
3098            }
3099        }
3100    }
3101
3102    /// Retrieves a confirmed block at the given slot, including transactions and metadata.
3103    ///
3104    /// # Arguments
3105    /// * `slot` - The slot number to retrieve the block for.
3106    /// * `config` - The configuration for the block retrieval.
3107    ///
3108    /// # Returns
3109    /// `Some(UiConfirmedBlock)` if found, or `None` if not present.
3110    pub fn get_block_at_slot(
3111        &self,
3112        slot: Slot,
3113        config: &RpcBlockConfig,
3114    ) -> SurfpoolResult<Option<UiConfirmedBlock>> {
3115        // Try to get stored block, or reconstruct empty block if within valid range
3116        let Some(block) = self.get_block_or_reconstruct(slot)? else {
3117            return Ok(None);
3118        };
3119
3120        let show_rewards = config.rewards.unwrap_or(true);
3121        let transaction_details = config
3122            .transaction_details
3123            .unwrap_or(TransactionDetails::Full);
3124
3125        let transactions = match transaction_details {
3126            TransactionDetails::Full => Some(
3127                block
3128                    .signatures
3129                    .iter()
3130                    .filter_map(|sig| self.transactions.get(&sig.to_string()).ok().flatten())
3131                    .map(|tx_with_meta| {
3132                        let (meta, _) = tx_with_meta.expect_processed();
3133                        meta.encode(
3134                            config.encoding.unwrap_or(
3135                                solana_transaction_status::UiTransactionEncoding::JsonParsed,
3136                            ),
3137                            config.max_supported_transaction_version,
3138                            show_rewards,
3139                        )
3140                    })
3141                    .collect::<Result<Vec<_>, _>>()
3142                    .map_err(SurfpoolError::from)?,
3143            ),
3144            TransactionDetails::Signatures => None,
3145            TransactionDetails::None => None,
3146            TransactionDetails::Accounts => Some(
3147                block
3148                    .signatures
3149                    .iter()
3150                    .filter_map(|sig| self.transactions.get(&sig.to_string()).ok().flatten())
3151                    .map(|tx_with_meta| {
3152                        let (meta, _) = tx_with_meta.expect_processed();
3153                        meta.to_json_accounts(
3154                            config.max_supported_transaction_version,
3155                            show_rewards,
3156                        )
3157                    })
3158                    .collect::<Result<Vec<_>, _>>()
3159                    .map_err(SurfpoolError::from)?,
3160            ),
3161        };
3162
3163        let signatures = match transaction_details {
3164            TransactionDetails::Signatures => {
3165                Some(block.signatures.iter().map(|t| t.to_string()).collect())
3166            }
3167            TransactionDetails::Full | TransactionDetails::Accounts | TransactionDetails::None => {
3168                None
3169            }
3170        };
3171
3172        let block = UiConfirmedBlock {
3173            previous_blockhash: block.previous_blockhash.clone(),
3174            blockhash: block.hash.clone(),
3175            parent_slot: block.parent_slot,
3176            transactions,
3177            signatures,
3178            rewards: if show_rewards { Some(vec![]) } else { None },
3179            num_reward_partitions: None,
3180            block_time: Some(block.block_time / 1000),
3181            block_height: Some(block.block_height),
3182        };
3183        Ok(Some(block))
3184    }
3185
3186    /// Returns the blockhash for a given slot, if available.
3187    pub fn blockhash_for_slot(&self, slot: Slot) -> Option<Hash> {
3188        self.blocks
3189            .get(&slot)
3190            .unwrap()
3191            .and_then(|header| header.hash.parse().ok())
3192    }
3193
3194    /// Gets all accounts owned by a specific program ID from the account registry.
3195    ///
3196    /// # Arguments
3197    ///
3198    /// * `program_id` - The program ID to search for owned accounts.
3199    ///
3200    /// # Returns
3201    ///
3202    /// * A vector of (account_pubkey, account) tuples for all accounts owned by the program.
3203    pub fn get_account_owned_by(
3204        &self,
3205        program_id: &Pubkey,
3206    ) -> SurfpoolResult<Vec<(Pubkey, Account)>> {
3207        let account_pubkeys = self
3208            .accounts_by_owner
3209            .get(&program_id.to_string())
3210            .ok()
3211            .flatten()
3212            .unwrap_or_default();
3213
3214        account_pubkeys
3215            .iter()
3216            .filter_map(|pk_str| {
3217                let pk = Pubkey::from_str(pk_str).ok()?;
3218                self.get_account(&pk)
3219                    .map(|res| res.map(|account| (pk, account.clone())))
3220                    .transpose()
3221            })
3222            .collect::<Result<Vec<_>, SurfpoolError>>()
3223    }
3224
3225    fn get_additional_data(
3226        &self,
3227        pubkey: &Pubkey,
3228        token_mint: Option<Pubkey>,
3229    ) -> Option<AccountAdditionalDataV3> {
3230        let token_mint = if let Some(mint) = token_mint {
3231            Some(mint)
3232        } else {
3233            self.token_accounts
3234                .get(&pubkey.to_string())
3235                .ok()
3236                .flatten()
3237                .map(|ta| ta.mint())
3238        };
3239
3240        token_mint.and_then(|mint| {
3241            self.account_associated_data
3242                .get(&mint.to_string())
3243                .ok()
3244                .flatten()
3245                .and_then(|data| data.try_into().ok())
3246        })
3247    }
3248
3249    pub fn account_to_rpc_keyed_account<T: ReadableAccount>(
3250        &self,
3251        pubkey: &Pubkey,
3252        account: &T,
3253        config: &RpcAccountInfoConfig,
3254        token_mint: Option<Pubkey>,
3255    ) -> RpcKeyedAccount {
3256        let additional_data = self.get_additional_data(pubkey, token_mint);
3257
3258        RpcKeyedAccount {
3259            pubkey: pubkey.to_string(),
3260            account: self.encode_ui_account(
3261                pubkey,
3262                account,
3263                config.encoding.unwrap_or(UiAccountEncoding::Base64),
3264                additional_data,
3265                config.data_slice,
3266            ),
3267        }
3268    }
3269
3270    /// Gets all token accounts that have delegated authority to a specific delegate.
3271    ///
3272    /// # Arguments
3273    ///
3274    /// * `delegate` - The delegate pubkey to search for token accounts that have granted authority.
3275    ///
3276    /// # Returns
3277    ///
3278    /// * A vector of (account_pubkey, token_account) tuples for all token accounts delegated to the specified delegate.
3279    pub fn get_token_accounts_by_delegate(&self, delegate: &Pubkey) -> Vec<(Pubkey, TokenAccount)> {
3280        if let Some(account_pubkeys) = self
3281            .token_accounts_by_delegate
3282            .get(&delegate.to_string())
3283            .ok()
3284            .flatten()
3285        {
3286            account_pubkeys
3287                .iter()
3288                .filter_map(|pk_str| {
3289                    let pk = Pubkey::from_str(pk_str).ok()?;
3290                    self.token_accounts
3291                        .get(pk_str)
3292                        .ok()
3293                        .flatten()
3294                        .map(|ta| (pk, ta))
3295                })
3296                .collect()
3297        } else {
3298            Vec::new()
3299        }
3300    }
3301
3302    /// Gets all token accounts owned by a specific owner.
3303    ///
3304    /// # Arguments
3305    ///
3306    /// * `owner` - The owner pubkey to search for token accounts.
3307    ///
3308    /// # Returns
3309    ///
3310    /// * A vector of (account_pubkey, token_account) tuples for all token accounts owned by the specified owner.
3311    pub fn get_parsed_token_accounts_by_owner(
3312        &self,
3313        owner: &Pubkey,
3314    ) -> Vec<(Pubkey, TokenAccount)> {
3315        if let Some(account_pubkeys) = self
3316            .token_accounts_by_owner
3317            .get(&owner.to_string())
3318            .ok()
3319            .flatten()
3320        {
3321            account_pubkeys
3322                .iter()
3323                .filter_map(|pk_str| {
3324                    let pk = Pubkey::from_str(pk_str).ok()?;
3325                    self.token_accounts
3326                        .get(pk_str)
3327                        .ok()
3328                        .flatten()
3329                        .map(|ta| (pk, ta))
3330                })
3331                .collect()
3332        } else {
3333            Vec::new()
3334        }
3335    }
3336
3337    pub fn get_token_accounts_by_owner(
3338        &self,
3339        owner: &Pubkey,
3340    ) -> SurfpoolResult<Vec<(Pubkey, Account)>> {
3341        let account_pubkeys = self
3342            .token_accounts_by_owner
3343            .get(&owner.to_string())
3344            .ok()
3345            .flatten()
3346            .unwrap_or_default();
3347
3348        account_pubkeys
3349            .iter()
3350            .filter_map(|pk_str| {
3351                let pk = Pubkey::from_str(pk_str).ok()?;
3352                self.get_account(&pk)
3353                    .map(|res| res.map(|account| (pk, account.clone())))
3354                    .transpose()
3355            })
3356            .collect::<Result<Vec<_>, SurfpoolError>>()
3357    }
3358
3359    /// Gets all token accounts for a specific mint (token type).
3360    ///
3361    /// # Arguments
3362    ///
3363    /// * `mint` - The mint pubkey to search for token accounts.
3364    ///
3365    /// # Returns
3366    ///
3367    /// * A vector of (account_pubkey, token_account) tuples for all token accounts of the specified mint.
3368    pub fn get_token_accounts_by_mint(&self, mint: &Pubkey) -> Vec<(Pubkey, TokenAccount)> {
3369        if let Some(account_pubkeys) = self
3370            .token_accounts_by_mint
3371            .get(&mint.to_string())
3372            .ok()
3373            .flatten()
3374        {
3375            account_pubkeys
3376                .iter()
3377                .filter_map(|pk_str| {
3378                    let pk = Pubkey::from_str(pk_str).ok()?;
3379                    self.token_accounts
3380                        .get(pk_str)
3381                        .ok()
3382                        .flatten()
3383                        .map(|ta| (pk, ta))
3384                })
3385                .collect()
3386        } else {
3387            Vec::new()
3388        }
3389    }
3390
3391    pub fn subscribe_for_slot_updates(&mut self) -> Receiver<SlotInfo> {
3392        let (tx, rx) = unbounded();
3393        self.slot_subscriptions.push(tx);
3394        rx
3395    }
3396
3397    pub fn notify_slot_subscribers(&mut self, slot: Slot, parent: Slot, root: Slot) {
3398        self.slot_subscriptions
3399            .retain(|tx| tx.send(SlotInfo { slot, parent, root }).is_ok());
3400    }
3401
3402    /// Registers a new sender for `slotsUpdatesSubscribe` notifications and
3403    /// returns the matching receiver.
3404    ///
3405    /// The returned channel will receive every tagged `SlotUpdate` produced
3406    /// by [`Self::notify_slots_updates_subscribers`] until the receiver is
3407    /// dropped (at which point the SVM will self-prune the sender on the
3408    /// next notification).
3409    pub fn subscribe_for_slots_updates(&mut self) -> Receiver<Arc<SlotUpdate>> {
3410        let (tx, rx) = unbounded();
3411        self.slots_updates_subscriptions.push(tx);
3412        rx
3413    }
3414
3415    /// Fan-out a tagged `SlotUpdate` to every active
3416    /// `slotsUpdatesSubscribe` subscriber. The update is allocated once and
3417    /// shared via `Arc` across all senders. Disconnected receivers cause
3418    /// their sender to be pruned, mirroring the cleanup pattern used by
3419    /// [`Self::notify_slot_subscribers`].
3420    pub fn notify_slots_updates_subscribers(&mut self, update: SlotUpdate) {
3421        if self.slots_updates_subscriptions.is_empty() {
3422            return;
3423        }
3424        let arc = Arc::new(update);
3425        self.slots_updates_subscriptions
3426            .retain(|tx| tx.send(arc.clone()).is_ok());
3427    }
3428
3429    pub fn write_simulated_profile_result(
3430        &mut self,
3431        uuid: Uuid,
3432        tag: Option<String>,
3433        profile_result: KeyedProfileResult,
3434    ) -> SurfpoolResult<()> {
3435        self.simulated_transaction_profiles
3436            .store(uuid.to_string(), profile_result)?;
3437
3438        let tag = tag.unwrap_or_else(|| uuid.to_string());
3439        let mut tags = self
3440            .profile_tag_map
3441            .get(&tag)
3442            .ok()
3443            .flatten()
3444            .unwrap_or_default();
3445        tags.push(UuidOrSignature::Uuid(uuid));
3446        self.profile_tag_map.store(tag, tags)?;
3447        Ok(())
3448    }
3449
3450    pub fn write_executed_profile_result(
3451        &mut self,
3452        signature: Signature,
3453        profile_result: KeyedProfileResult,
3454    ) -> SurfpoolResult<()> {
3455        self.executed_transaction_profiles
3456            .store(signature.to_string(), profile_result)?;
3457        let tag = signature.to_string();
3458        let mut tags = self
3459            .profile_tag_map
3460            .get(&tag)
3461            .ok()
3462            .flatten()
3463            .unwrap_or_default();
3464        tags.push(UuidOrSignature::Signature(signature));
3465        self.profile_tag_map.store(tag, tags)?;
3466        Ok(())
3467    }
3468
3469    pub fn subscribe_for_logs_updates(
3470        &mut self,
3471        commitment_level: &CommitmentLevel,
3472        filter: &RpcTransactionLogsFilter,
3473    ) -> Receiver<(Slot, RpcLogsResponse)> {
3474        let (tx, rx) = unbounded();
3475        self.logs_subscriptions
3476            .push((*commitment_level, filter.clone(), tx));
3477        rx
3478    }
3479
3480    pub fn notify_logs_subscribers(
3481        &mut self,
3482        signature: &Signature,
3483        err: Option<TransactionError>,
3484        logs: Vec<String>,
3485        commitment_level: CommitmentLevel,
3486    ) {
3487        for (expected_level, filter, tx) in self.logs_subscriptions.iter() {
3488            if !expected_level.eq(&commitment_level) {
3489                continue; // Skip if commitment level is not expected
3490            }
3491
3492            let should_notify = match filter {
3493                RpcTransactionLogsFilter::All | RpcTransactionLogsFilter::AllWithVotes => true,
3494
3495                RpcTransactionLogsFilter::Mentions(mentioned_accounts) => {
3496                    // Get the tx accounts including loaded addresses
3497                    let transaction_accounts =
3498                        if let Some(SurfnetTransactionStatus::Processed(tx_data)) =
3499                            self.transactions.get(&signature.to_string()).ok().flatten()
3500                        {
3501                            let (tx_meta, _) = tx_data.as_ref();
3502                            let mut accounts =
3503                                tx_meta.transaction.message.static_account_keys().to_vec();
3504
3505                            accounts.extend(&tx_meta.meta.loaded_addresses.writable);
3506                            accounts.extend(&tx_meta.meta.loaded_addresses.readonly);
3507                            Some(accounts)
3508                        } else {
3509                            None
3510                        };
3511
3512                    let Some(accounts) = transaction_accounts else {
3513                        continue;
3514                    };
3515
3516                    mentioned_accounts.iter().any(|filtered_acc| {
3517                        if let Ok(filtered_pubkey) = Pubkey::from_str(&filtered_acc) {
3518                            accounts.contains(&filtered_pubkey)
3519                        } else {
3520                            false
3521                        }
3522                    })
3523                }
3524            };
3525
3526            if should_notify {
3527                let message = RpcLogsResponse {
3528                    signature: signature.to_string(),
3529                    err: err.clone().map(|e| e.into()),
3530                    logs: logs.clone(),
3531                };
3532                let _ = tx.send((self.get_latest_absolute_slot(), message));
3533            }
3534        }
3535    }
3536
3537    /// Registers a snapshot subscription and returns a sender and receiver for notifications.
3538    /// The actual import logic should be handled by the caller (SurfnetSvmLocker).
3539    pub fn register_snapshot_subscription(
3540        &mut self,
3541    ) -> (
3542        Sender<super::SnapshotImportNotification>,
3543        Receiver<super::SnapshotImportNotification>,
3544    ) {
3545        let (tx, rx) = unbounded();
3546        self.snapshot_subscriptions.push(tx.clone());
3547        (tx, rx)
3548    }
3549
3550    pub async fn fetch_snapshot_from_url(
3551        snapshot_url: &str,
3552    ) -> Result<
3553        std::collections::BTreeMap<String, Option<surfpool_types::AccountSnapshot>>,
3554        Box<dyn std::error::Error + Send + Sync>,
3555    > {
3556        let response = reqwest::get(snapshot_url).await?;
3557        let text = response.text().await?;
3558
3559        // Parse the JSON snapshot data
3560        let snapshot: std::collections::BTreeMap<String, Option<surfpool_types::AccountSnapshot>> =
3561            serde_json::from_str(&text)?;
3562
3563        Ok(snapshot)
3564    }
3565
3566    pub fn register_idl(&mut self, idl: Idl, slot: Option<Slot>) -> SurfpoolResult<()> {
3567        let slot = slot.unwrap_or(self.latest_epoch_info.absolute_slot);
3568        let program_id = Pubkey::from_str_const(&idl.address);
3569        let program_id_str = program_id.to_string();
3570        let mut idl_versions = self
3571            .registered_idls
3572            .get(&program_id_str)
3573            .ok()
3574            .flatten()
3575            .unwrap_or_default();
3576        idl_versions.push(VersionedIdl(slot, idl));
3577        // Sort by slot descending so the latest IDL is first
3578        idl_versions.sort_by(|a, b| b.0.cmp(&a.0));
3579        self.registered_idls.store(program_id_str, idl_versions)?;
3580        Ok(())
3581    }
3582
3583    fn encode_ui_account_profile_state(
3584        &self,
3585        pubkey: &Pubkey,
3586        account_profile_state: AccountProfileState,
3587        encoding: &UiAccountEncoding,
3588    ) -> UiAccountProfileState {
3589        let additional_data = self.get_additional_data(pubkey, None);
3590
3591        match account_profile_state {
3592            AccountProfileState::Readonly => UiAccountProfileState::Readonly,
3593            AccountProfileState::Writable(account_change) => {
3594                let change = match account_change {
3595                    AccountChange::Create(account) => UiAccountChange::Create(
3596                        self.encode_ui_account(pubkey, &account, *encoding, additional_data, None),
3597                    ),
3598                    AccountChange::Update(account_before, account_after) => {
3599                        UiAccountChange::Update(
3600                            self.encode_ui_account(
3601                                pubkey,
3602                                &account_before,
3603                                *encoding,
3604                                additional_data,
3605                                None,
3606                            ),
3607                            self.encode_ui_account(
3608                                pubkey,
3609                                &account_after,
3610                                *encoding,
3611                                additional_data,
3612                                None,
3613                            ),
3614                        )
3615                    }
3616                    AccountChange::Delete(account) => UiAccountChange::Delete(
3617                        self.encode_ui_account(pubkey, &account, *encoding, additional_data, None),
3618                    ),
3619                    AccountChange::Unchanged(account) => {
3620                        UiAccountChange::Unchanged(account.map(|account| {
3621                            self.encode_ui_account(
3622                                pubkey,
3623                                &account,
3624                                *encoding,
3625                                additional_data,
3626                                None,
3627                            )
3628                        }))
3629                    }
3630                };
3631                UiAccountProfileState::Writable(change)
3632            }
3633        }
3634    }
3635
3636    fn encode_ui_profile_result(
3637        &self,
3638        profile_result: ProfileResult,
3639        readonly_accounts: &[Pubkey],
3640        encoding: &UiAccountEncoding,
3641    ) -> UiProfileResult {
3642        let ProfileResult {
3643            pre_execution_capture,
3644            post_execution_capture,
3645            compute_units_consumed,
3646            log_messages,
3647            error_message,
3648        } = profile_result;
3649
3650        let account_states = pre_execution_capture
3651            .into_iter()
3652            .zip(post_execution_capture)
3653            .map(|((pubkey, pre_account), (_, post_account))| {
3654                // if pubkey != post {
3655                //     panic!(
3656                //         "Pre-execution pubkey {} does not match post-execution pubkey {}",
3657                //         pubkey, post
3658                //     );
3659                // }
3660                let state =
3661                    AccountProfileState::new(pubkey, pre_account, post_account, readonly_accounts);
3662                (
3663                    pubkey,
3664                    self.encode_ui_account_profile_state(&pubkey, state, encoding),
3665                )
3666            })
3667            .collect::<IndexMap<Pubkey, UiAccountProfileState>>();
3668
3669        UiProfileResult {
3670            account_states,
3671            compute_units_consumed,
3672            log_messages,
3673            error_message,
3674        }
3675    }
3676
3677    pub fn encode_ui_keyed_profile_result(
3678        &self,
3679        keyed_profile_result: KeyedProfileResult,
3680        config: &RpcProfileResultConfig,
3681    ) -> UiKeyedProfileResult {
3682        let KeyedProfileResult {
3683            slot,
3684            key,
3685            instruction_profiles,
3686            transaction_profile,
3687            readonly_account_states,
3688        } = keyed_profile_result;
3689
3690        let encoding = config.encoding.unwrap_or(UiAccountEncoding::JsonParsed);
3691
3692        let readonly_accounts = readonly_account_states.keys().cloned().collect::<Vec<_>>();
3693
3694        let default = RpcProfileDepth::default();
3695        let instruction_profiles = match *config.depth.as_ref().unwrap_or(&default) {
3696            RpcProfileDepth::Transaction => None,
3697            RpcProfileDepth::Instruction => instruction_profiles.map(|instruction_profiles| {
3698                instruction_profiles
3699                    .into_iter()
3700                    .map(|p| self.encode_ui_profile_result(p, &readonly_accounts, &encoding))
3701                    .collect()
3702            }),
3703        };
3704
3705        let transaction_profile =
3706            self.encode_ui_profile_result(transaction_profile, &readonly_accounts, &encoding);
3707
3708        let readonly_account_states = readonly_account_states
3709            .into_iter()
3710            .map(|(pubkey, account)| {
3711                let account = self.encode_ui_account(&pubkey, &account, encoding, None, None);
3712                (pubkey, account)
3713            })
3714            .collect();
3715
3716        UiKeyedProfileResult {
3717            slot,
3718            key,
3719            instruction_profiles,
3720            transaction_profile,
3721            readonly_account_states,
3722        }
3723    }
3724
3725    pub fn encode_ui_account<T: ReadableAccount>(
3726        &self,
3727        pubkey: &Pubkey,
3728        account: &T,
3729        encoding: UiAccountEncoding,
3730        additional_data: Option<AccountAdditionalDataV3>,
3731        data_slice_config: Option<UiDataSliceConfig>,
3732    ) -> UiAccount {
3733        let owner_program_id = account.owner();
3734        let data = account.data();
3735
3736        if encoding == UiAccountEncoding::JsonParsed {
3737            if let Ok(Some(registered_idls)) =
3738                self.registered_idls.get(&owner_program_id.to_string())
3739            {
3740                let filter_slot = self.latest_epoch_info.absolute_slot;
3741                // IDLs are stored sorted by slot descending (most recent first)
3742                let ordered_available_idls = registered_idls
3743                    .iter()
3744                    // only get IDLs that are active (their slot is before the latest slot)
3745                    .filter_map(|VersionedIdl(slot, idl)| {
3746                        if *slot <= filter_slot {
3747                            Some(idl)
3748                        } else {
3749                            None
3750                        }
3751                    })
3752                    .collect::<Vec<_>>();
3753
3754                // if we have none in this loop, it means the only IDLs registered for this pubkey are for a
3755                // future slot, for some reason. if we have some, we'll try each one in this loop, starting
3756                // with the most recent one, to see if the account data can be parsed to the IDL type
3757                for idl in &ordered_available_idls {
3758                    // If we have a valid IDL, use it to parse the account data
3759                    let discriminator = &data[..8];
3760                    if let Some(matching_account) = idl
3761                        .accounts
3762                        .iter()
3763                        .find(|a| a.discriminator.eq(&discriminator))
3764                    {
3765                        // If we found a matching account, we can look up the type to parse the account
3766                        if let Some(account_type) =
3767                            idl.types.iter().find(|t| t.name == matching_account.name)
3768                        {
3769                            let empty_vec = vec![];
3770                            let idl_type_def_generics = idl
3771                                .types
3772                                .iter()
3773                                .find(|t| t.name == account_type.name)
3774                                .map(|t| &t.generics);
3775
3776                            // If we found a matching account type, we can use it to parse the account data
3777                            let rest = data[8..].as_ref();
3778                            if let Ok(parsed_value) =
3779                                parse_bytes_to_value_with_expected_idl_type_def_ty(
3780                                    rest,
3781                                    &account_type.ty,
3782                                    &idl.types,
3783                                    &vec![],
3784                                    idl_type_def_generics.unwrap_or(&empty_vec),
3785                                )
3786                            {
3787                                return UiAccount {
3788                                    lamports: account.lamports(),
3789                                    data: UiAccountData::Json(ParsedAccount {
3790                                        program: idl
3791                                            .metadata
3792                                            .name
3793                                            .to_string()
3794                                            .to_case(convert_case::Case::Kebab),
3795                                        parsed: parsed_value
3796                                            .to_json(Some(&get_txtx_value_json_converters())),
3797                                        space: data.len() as u64,
3798                                    }),
3799                                    owner: owner_program_id.to_string(),
3800                                    executable: account.executable(),
3801                                    rent_epoch: account.rent_epoch(),
3802                                    space: Some(data.len() as u64),
3803                                };
3804                            }
3805                        }
3806                    }
3807                }
3808            }
3809        }
3810
3811        // Fall back to the default encoding
3812        encode_ui_account(
3813            pubkey,
3814            account,
3815            encoding,
3816            additional_data,
3817            data_slice_config,
3818        )
3819    }
3820
3821    pub fn get_account(&self, pubkey: &Pubkey) -> SurfpoolResult<Option<Account>> {
3822        self.inner.get_account(pubkey)
3823    }
3824
3825    pub fn get_all_accounts(&self) -> SurfpoolResult<Vec<(Pubkey, AccountSharedData)>> {
3826        self.inner.get_all_accounts()
3827    }
3828
3829    pub fn get_transaction(
3830        &self,
3831        signature: &Signature,
3832    ) -> SurfpoolResult<Option<SurfnetTransactionStatus>> {
3833        Ok(self.transactions.get(&signature.to_string())?)
3834    }
3835
3836    pub fn start_runbook_execution(&mut self, runbook_id: String) {
3837        self.runbook_executions
3838            .push(RunbookExecutionStatusReport::new(runbook_id));
3839    }
3840
3841    pub fn complete_runbook_execution(&mut self, runbook_id: &str, error: Option<Vec<String>>) {
3842        if let Some(execution) = self
3843            .runbook_executions
3844            .iter_mut()
3845            .find(|e| e.runbook_id.eq(runbook_id) && e.completed_at.is_none())
3846        {
3847            execution.mark_completed(error);
3848        }
3849    }
3850
3851    /// Export all accounts to a JSON file suitable for test fixtures
3852    ///
3853    /// # Arguments
3854    /// * `encoding` - The encoding to use for account data (Base64, JsonParsed, etc.)
3855    ///
3856    /// # Returns
3857    /// A BTreeMap of pubkey -> AccountFixture that can be serialized to JSON.
3858    pub fn export_snapshot(
3859        &self,
3860        config: ExportSnapshotConfig,
3861    ) -> SurfpoolResult<BTreeMap<String, AccountSnapshot>> {
3862        let mut fixtures = BTreeMap::new();
3863        let encoding = if config.include_parsed_accounts.unwrap_or_default() {
3864            UiAccountEncoding::JsonParsed
3865        } else {
3866            UiAccountEncoding::Base64
3867        };
3868        let filter = config.filter.unwrap_or_default();
3869        let include_program_accounts = filter.include_program_accounts.unwrap_or(false);
3870        let include_accounts = filter.include_accounts.unwrap_or_default();
3871        let exclude_accounts = filter.exclude_accounts.unwrap_or_default();
3872        let exclude_sysvars = filter.exclude_sysvars.unwrap_or(false);
3873        let exclude_feature_gates = filter.exclude_feature_gates.unwrap_or(false);
3874
3875        fn is_program_account(pubkey: &Pubkey) -> bool {
3876            pubkey == &bpf_loader::id()
3877                || pubkey == &solana_sdk_ids::bpf_loader_deprecated::id()
3878                || pubkey == &solana_sdk_ids::bpf_loader_upgradeable::id()
3879        }
3880
3881        // Helper function to process an account and add it to fixtures
3882        let mut process_account = |pubkey: &Pubkey, account: &Account| {
3883            let is_include_account = include_accounts.iter().any(|k| k.eq(&pubkey.to_string()));
3884            let is_exclude_account = exclude_accounts.iter().any(|k| k.eq(&pubkey.to_string()));
3885            let is_program_account = is_program_account(&account.owner);
3886            if is_exclude_account
3887                || ((is_program_account && !include_program_accounts) && !is_include_account)
3888            {
3889                return;
3890            }
3891            if !is_include_account {
3892                if exclude_sysvars && account.owner == solana_sdk_ids::sysvar::id() {
3893                    return;
3894                }
3895                if exclude_feature_gates && agave_feature_set::FEATURE_NAMES.contains_key(pubkey) {
3896                    return;
3897                }
3898            }
3899
3900            // For token accounts, we need to provide the mint additional data
3901            let additional_data: Option<AccountAdditionalDataV3> = if account.owner
3902                == spl_token_interface::id()
3903                || account.owner == spl_token_2022_interface::id()
3904            {
3905                if let Ok(token_account) = TokenAccount::unpack(&account.data) {
3906                    self.account_associated_data
3907                        .get(&token_account.mint().to_string())
3908                        .ok()
3909                        .flatten()
3910                        .and_then(|data| data.try_into().ok())
3911                } else {
3912                    self.account_associated_data
3913                        .get(&pubkey.to_string())
3914                        .ok()
3915                        .flatten()
3916                        .and_then(|data| data.try_into().ok())
3917                }
3918            } else {
3919                self.account_associated_data
3920                    .get(&pubkey.to_string())
3921                    .ok()
3922                    .flatten()
3923                    .and_then(|data| data.try_into().ok())
3924            };
3925
3926            let ui_account =
3927                self.encode_ui_account(pubkey, account, encoding, additional_data, None);
3928
3929            let (base64, parsed_data) = match ui_account.data {
3930                UiAccountData::Json(parsed_account) => {
3931                    (BASE64_STANDARD.encode(account.data()), Some(parsed_account))
3932                }
3933                UiAccountData::Binary(base64, _) => (base64, None),
3934                UiAccountData::LegacyBinary(_) => unreachable!(),
3935            };
3936
3937            let account_snapshot = AccountSnapshot::new(
3938                account.lamports,
3939                account.owner.to_string(),
3940                account.executable,
3941                account.rent_epoch,
3942                base64,
3943                parsed_data,
3944            );
3945
3946            fixtures.insert(pubkey.to_string(), account_snapshot);
3947        };
3948
3949        match &config.scope {
3950            ExportSnapshotScope::Network => {
3951                // Export all network accounts (current behavior)
3952                for (pubkey, account_shared_data) in self.get_all_accounts()? {
3953                    let account = Account::from(account_shared_data.clone());
3954                    process_account(&pubkey, &account);
3955                }
3956            }
3957            ExportSnapshotScope::PreTransaction(signature_str) => {
3958                // Export accounts from a specific transaction's pre-execution state
3959                if let Ok(signature) = Signature::from_str(signature_str) {
3960                    if let Ok(Some(profile)) = self
3961                        .executed_transaction_profiles
3962                        .get(&signature.to_string())
3963                    {
3964                        // Collect accounts from pre-execution capture only
3965                        // This gives us the account state BEFORE the transaction executed
3966                        for (pubkey, account_opt) in
3967                            &profile.transaction_profile.pre_execution_capture
3968                        {
3969                            if let Some(account) = account_opt {
3970                                process_account(pubkey, account);
3971                            }
3972                        }
3973
3974                        // Also collect readonly account states (these don't change)
3975                        for (pubkey, account) in &profile.readonly_account_states {
3976                            process_account(pubkey, account);
3977                        }
3978                    }
3979                }
3980            }
3981        }
3982
3983        Ok(fixtures)
3984    }
3985
3986    /// Registers a scenario for execution by scheduling its overrides
3987    ///
3988    /// The `slot` parameter is the base slot from which relative override slot heights are calculated.
3989    /// If not provided, uses the current slot.
3990    pub fn register_scenario(
3991        &mut self,
3992        scenario: surfpool_types::Scenario,
3993        slot: Option<Slot>,
3994    ) -> SurfpoolResult<()> {
3995        // Use provided slot or current slot as the base for relative slot heights
3996        let base_slot = slot.unwrap_or(self.latest_epoch_info.absolute_slot);
3997
3998        info!(
3999            "Registering scenario: {} ({}) with {} overrides at base slot {}",
4000            scenario.name,
4001            scenario.id,
4002            scenario.overrides.len(),
4003            base_slot
4004        );
4005
4006        // Schedule overrides by adding base slot to their scenario-relative slots
4007        for override_instance in scenario.overrides {
4008            let scenario_relative_slot = override_instance.scenario_relative_slot;
4009            let absolute_slot = base_slot + scenario_relative_slot;
4010
4011            debug!(
4012                "Scheduling override at absolute slot {} (base {} + relative {})",
4013                absolute_slot, base_slot, scenario_relative_slot
4014            );
4015
4016            let mut slot_overrides = self
4017                .scheduled_overrides
4018                .get(&absolute_slot)
4019                .ok()
4020                .flatten()
4021                .unwrap_or_default();
4022            slot_overrides.push(override_instance);
4023            self.scheduled_overrides
4024                .store(absolute_slot, slot_overrides)?;
4025        }
4026
4027        Ok(())
4028    }
4029}
4030
4031#[cfg(test)]
4032mod tests {
4033    use agave_feature_set::{
4034        blake3_syscall_enabled, curve25519_syscall_enabled, disable_fees_sysvar,
4035        enable_extend_program_checked, enable_loader_v4, enable_sbpf_v1_deployment_and_execution,
4036        enable_sbpf_v2_deployment_and_execution, enable_sbpf_v3_deployment_and_execution,
4037        formalize_loaded_transaction_data_size, move_precompile_verification_to_svm,
4038        raise_cpi_nesting_limit_to_8, stake_raise_minimum_delegation_to_1_sol,
4039    };
4040    use base64::{Engine, engine::general_purpose};
4041    use borsh::BorshSerialize;
4042    // use test_log::test; // uncomment to get logs from litesvm
4043    use solana_account::Account;
4044    use solana_hash::Hash;
4045    use solana_keypair::Keypair;
4046    use solana_loader_v3_interface::get_program_data_address;
4047    use solana_message::VersionedMessage;
4048    use solana_program_pack::Pack;
4049    use solana_signer::Signer;
4050    use solana_system_interface::instruction as system_instruction;
4051    use solana_transaction::Transaction;
4052    use solana_transaction_error::TransactionError;
4053    use spl_token_interface::state::{Account as TokenAccount, AccountState};
4054    use surfpool_types::ExportSnapshotFilter;
4055    use test_case::test_case;
4056
4057    use super::*;
4058    use crate::storage::tests::TestType;
4059
4060    fn build_transfer_transaction(
4061        payer: &Keypair,
4062        recipient: &Pubkey,
4063        lamports: u64,
4064        recent_blockhash: Hash,
4065    ) -> VersionedTransaction {
4066        let tx = Transaction::new_signed_with_payer(
4067            &[system_instruction::transfer(
4068                &payer.pubkey(),
4069                recipient,
4070                lamports,
4071            )],
4072            Some(&payer.pubkey()),
4073            &[payer],
4074            recent_blockhash,
4075        );
4076
4077        VersionedTransaction {
4078            signatures: tx.signatures,
4079            message: VersionedMessage::Legacy(tx.message),
4080        }
4081    }
4082
4083    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4084    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4085    #[test_case(TestType::no_db(); "with no db")]
4086    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4087    fn test_synthetic_blockhash_generation(test_type: TestType) {
4088        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4089
4090        // Test with different chain tip indices
4091        let test_cases = vec![0, 1, 42, 255, 1000, 0x12345678];
4092
4093        for index in test_cases {
4094            svm.chain_tip = BlockIdentifier::new(index, "test_hash");
4095
4096            // Generate the synthetic blockhash
4097            let new_blockhash = svm.new_blockhash();
4098
4099            // Verify the blockhash string contains our expected pattern
4100            let blockhash_str = new_blockhash.hash.clone();
4101            println!("Index {} -> Blockhash: {}", index, blockhash_str);
4102
4103            // The blockhash should be a valid base58 string
4104            assert!(!blockhash_str.is_empty());
4105            assert!(blockhash_str.len() > 20); // Base58 encoded 32 bytes should be around 44 chars
4106
4107            // Verify it's deterministic - same index should produce same blockhash
4108            svm.chain_tip = BlockIdentifier::new(index, "test_hash");
4109            let new_blockhash2 = svm.new_blockhash();
4110            assert_eq!(new_blockhash.hash, new_blockhash2.hash);
4111        }
4112    }
4113
4114    #[test]
4115    fn test_synthetic_blockhash_base58_encoding() {
4116        // Test the base58 encoding logic directly
4117        let test_index = 42u64;
4118        let index_hex = format!("{:08x}", test_index)
4119            .replace('0', "x")
4120            .replace('O', "x");
4121
4122        let target_length = 43;
4123        let padding_needed = target_length - SyntheticBlockhash::PREFIX.len() - index_hex.len();
4124        let padding = "x".repeat(padding_needed.max(0));
4125        let target_string = format!("{}{}{}", SyntheticBlockhash::PREFIX, padding, index_hex);
4126
4127        println!("Target string: {}", target_string);
4128
4129        // Verify the string is valid base58
4130        let decoded_bytes = bs58::decode(&target_string).into_vec();
4131        assert!(decoded_bytes.is_ok(), "String should be valid base58");
4132
4133        let bytes = decoded_bytes.unwrap();
4134        assert!(bytes.len() <= 32, "Decoded bytes should fit in 32 bytes");
4135
4136        // Test that we can create a hash from these bytes
4137        let mut blockhash_bytes = [0u8; 32];
4138        blockhash_bytes[..bytes.len().min(32)].copy_from_slice(&bytes[..bytes.len().min(32)]);
4139        let hash = Hash::new_from_array(blockhash_bytes);
4140
4141        // Verify the hash can be converted back to string
4142        let hash_str = hash.to_string();
4143        assert!(!hash_str.is_empty());
4144        println!("Generated hash: {}", hash_str);
4145    }
4146
4147    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4148    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4149    #[test_case(TestType::no_db(); "with no db")]
4150    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4151    fn test_blockhash_consistency_across_calls(test_type: TestType) {
4152        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4153
4154        // Set a specific chain tip
4155        svm.chain_tip = BlockIdentifier::new(123, "initial_hash");
4156
4157        // Generate multiple blockhashes and verify they're consistent
4158        let mut previous_hash: Option<BlockIdentifier> = None;
4159        for i in 0..5 {
4160            let new_blockhash = svm.new_blockhash();
4161            println!(
4162                "Call {}: index={}, hash={}",
4163                i, new_blockhash.index, new_blockhash.hash
4164            );
4165
4166            if let Some(prev) = previous_hash {
4167                // Each call should increment the index
4168                assert_eq!(new_blockhash.index, prev.index + 1);
4169                // But the hash should be different (since index changed)
4170                assert_ne!(new_blockhash.hash, prev.hash);
4171            } else {
4172                // First call should increment from the initial chain tip
4173                assert_eq!(new_blockhash.index, svm.chain_tip.index + 1);
4174            }
4175
4176            previous_hash = Some(new_blockhash.clone());
4177            // Update the chain tip for the next iteration
4178            svm.chain_tip = new_blockhash;
4179        }
4180    }
4181
4182    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4183    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4184    #[test_case(TestType::no_db(); "with no db")]
4185    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4186    fn test_token_account_indexing(test_type: TestType) {
4187        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4188
4189        let owner = Pubkey::new_unique();
4190        let delegate = Pubkey::new_unique();
4191        let mint = Pubkey::new_unique();
4192        let token_account_pubkey = Pubkey::new_unique();
4193
4194        // create a token account with delegate
4195        let mut token_account_data = [0u8; TokenAccount::LEN];
4196        let token_account = TokenAccount {
4197            mint,
4198            owner,
4199            amount: 1000,
4200            delegate: COption::Some(delegate),
4201            state: AccountState::Initialized,
4202            is_native: COption::None,
4203            delegated_amount: 500,
4204            close_authority: COption::None,
4205        };
4206        token_account.pack_into_slice(&mut token_account_data);
4207
4208        let account = Account {
4209            lamports: 1000000,
4210            data: token_account_data.to_vec(),
4211            owner: spl_token_interface::id(),
4212            executable: false,
4213            rent_epoch: 0,
4214        };
4215
4216        svm.set_account(&token_account_pubkey, account).unwrap();
4217
4218        // test all indexes were created correctly
4219        assert_eq!(svm.token_accounts.keys().unwrap().len(), 1);
4220
4221        // test owner index
4222        let owner_accounts = svm.get_parsed_token_accounts_by_owner(&owner);
4223        assert_eq!(owner_accounts.len(), 1);
4224        assert_eq!(owner_accounts[0].0, token_account_pubkey);
4225
4226        // test delegate index
4227        let delegate_accounts = svm.get_token_accounts_by_delegate(&delegate);
4228        assert_eq!(delegate_accounts.len(), 1);
4229        assert_eq!(delegate_accounts[0].0, token_account_pubkey);
4230
4231        // test mint index
4232        let mint_accounts = svm.get_token_accounts_by_mint(&mint);
4233        assert_eq!(mint_accounts.len(), 1);
4234        assert_eq!(mint_accounts[0].0, token_account_pubkey);
4235    }
4236
4237    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4238    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4239    #[test_case(TestType::no_db(); "with no db")]
4240    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4241    fn test_account_update_removes_old_indexes(test_type: TestType) {
4242        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4243
4244        let owner = Pubkey::new_unique();
4245        let old_delegate = Pubkey::new_unique();
4246        let new_delegate = Pubkey::new_unique();
4247        let mint = Pubkey::new_unique();
4248        let token_account_pubkey = Pubkey::new_unique();
4249
4250        //  reate initial token account with old delegate
4251        let mut token_account_data = [0u8; TokenAccount::LEN];
4252        let token_account = TokenAccount {
4253            mint,
4254            owner,
4255            amount: 1000,
4256            delegate: COption::Some(old_delegate),
4257            state: AccountState::Initialized,
4258            is_native: COption::None,
4259            delegated_amount: 500,
4260            close_authority: COption::None,
4261        };
4262        token_account.pack_into_slice(&mut token_account_data);
4263
4264        let account = Account {
4265            lamports: 1000000,
4266            data: token_account_data.to_vec(),
4267            owner: spl_token_interface::id(),
4268            executable: false,
4269            rent_epoch: 0,
4270        };
4271
4272        // insert initial account
4273        svm.set_account(&token_account_pubkey, account).unwrap();
4274
4275        // verify old delegate has the account
4276        assert_eq!(svm.get_token_accounts_by_delegate(&old_delegate).len(), 1);
4277        assert_eq!(svm.get_token_accounts_by_delegate(&new_delegate).len(), 0);
4278
4279        // update with new delegate
4280        let updated_token_account = TokenAccount {
4281            mint,
4282            owner,
4283            amount: 1000,
4284            delegate: COption::Some(new_delegate),
4285            state: AccountState::Initialized,
4286            is_native: COption::None,
4287            delegated_amount: 500,
4288            close_authority: COption::None,
4289        };
4290        updated_token_account.pack_into_slice(&mut token_account_data);
4291
4292        let updated_account = Account {
4293            lamports: 1000000,
4294            data: token_account_data.to_vec(),
4295            owner: spl_token_interface::id(),
4296            executable: false,
4297            rent_epoch: 0,
4298        };
4299
4300        // update the account
4301        svm.set_account(&token_account_pubkey, updated_account)
4302            .unwrap();
4303
4304        // verify indexes were updated correctly
4305        assert_eq!(svm.get_token_accounts_by_delegate(&old_delegate).len(), 0);
4306        assert_eq!(svm.get_token_accounts_by_delegate(&new_delegate).len(), 1);
4307        assert_eq!(svm.get_parsed_token_accounts_by_owner(&owner).len(), 1);
4308    }
4309
4310    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4311    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4312    #[test_case(TestType::no_db(); "with no db")]
4313    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4314    fn test_non_token_accounts_not_indexed(test_type: TestType) {
4315        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4316
4317        let system_account_pubkey = Pubkey::new_unique();
4318        let account = Account {
4319            lamports: 1000000,
4320            data: vec![],
4321            owner: solana_system_interface::program::id(), // system program, not token program
4322            executable: false,
4323            rent_epoch: 0,
4324        };
4325
4326        svm.set_account(&system_account_pubkey, account).unwrap();
4327
4328        // should be in general registry but not token indexes
4329        assert_eq!(svm.token_accounts.keys().unwrap().len(), 0);
4330        assert_eq!(svm.token_accounts_by_owner.keys().unwrap().len(), 0);
4331        assert_eq!(svm.token_accounts_by_delegate.keys().unwrap().len(), 0);
4332        assert_eq!(svm.token_accounts_by_mint.keys().unwrap().len(), 0);
4333    }
4334
4335    fn expect_account_update_event(
4336        events_rx: &Receiver<SimnetEvent>,
4337        svm: &SurfnetSvm,
4338        pubkey: &Pubkey,
4339        expected_account: &Account,
4340    ) -> bool {
4341        match events_rx.recv() {
4342            Ok(event) => match event {
4343                SimnetEvent::AccountUpdate(_, account_pubkey) => {
4344                    assert_eq!(pubkey, &account_pubkey);
4345                    assert_eq!(
4346                        svm.get_account(&pubkey).unwrap().as_ref(),
4347                        Some(expected_account)
4348                    );
4349                    true
4350                }
4351                event => {
4352                    println!("unexpected simnet event: {:?}", event);
4353                    false
4354                }
4355            },
4356            Err(_) => false,
4357        }
4358    }
4359
4360    fn _expect_error_event(events_rx: &Receiver<SimnetEvent>, expected_error: &str) -> bool {
4361        match events_rx.recv() {
4362            Ok(event) => match event {
4363                SimnetEvent::ErrorLog(_, err) => {
4364                    assert_eq!(err, expected_error);
4365
4366                    true
4367                }
4368                event => {
4369                    println!("unexpected simnet event: {:?}", event);
4370                    false
4371                }
4372            },
4373            Err(_) => false,
4374        }
4375    }
4376
4377    fn create_program_accounts() -> (Pubkey, Account, Pubkey, Account) {
4378        let program_pubkey = Pubkey::new_unique();
4379        let program_data_address = get_program_data_address(&program_pubkey);
4380        let program_account = Account {
4381            lamports: 1000000000000,
4382            data: bincode::serialize(
4383                &solana_loader_v3_interface::state::UpgradeableLoaderState::Program {
4384                    programdata_address: program_data_address,
4385                },
4386            )
4387            .unwrap(),
4388            owner: solana_sdk_ids::bpf_loader_upgradeable::ID,
4389            executable: true,
4390            rent_epoch: 10000000000000,
4391        };
4392
4393        let mut bin = include_bytes!("../tests/assets/metaplex_program.bin").to_vec();
4394        let mut data = bincode::serialize(
4395            &solana_loader_v3_interface::state::UpgradeableLoaderState::ProgramData {
4396                slot: 0,
4397                upgrade_authority_address: Some(Pubkey::new_unique()),
4398            },
4399        )
4400        .unwrap();
4401        data.append(&mut bin); // push our binary after the state data
4402        let program_data_account = Account {
4403            lamports: 10000000000000,
4404            data,
4405            owner: solana_sdk_ids::bpf_loader_upgradeable::ID,
4406            executable: false,
4407            rent_epoch: 10000000000000,
4408        };
4409        (
4410            program_pubkey,
4411            program_account,
4412            program_data_address,
4413            program_data_account,
4414        )
4415    }
4416
4417    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4418    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4419    #[test_case(TestType::no_db(); "with no db")]
4420    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4421    fn test_inserting_account_updates(test_type: TestType) {
4422        let (mut svm, events_rx, _geyser_rx) = test_type.initialize_svm();
4423
4424        let pubkey = Pubkey::new_unique();
4425        let account = Account {
4426            lamports: 1000,
4427            data: vec![1, 2, 3],
4428            owner: Pubkey::new_unique(),
4429            executable: false,
4430            rent_epoch: 0,
4431        };
4432
4433        // GetAccountResult::None should be a noop when writing account updates
4434        {
4435            let index_before = svm.get_all_accounts().unwrap();
4436            let empty_update = GetAccountResult::None(pubkey);
4437            svm.write_account_update(empty_update);
4438            assert_eq!(svm.get_all_accounts().unwrap(), index_before);
4439        }
4440
4441        // GetAccountResult::FoundAccount with `DoUpdateSvm` flag to false should be a noop
4442        {
4443            let index_before = svm.get_all_accounts().unwrap();
4444            let found_update = GetAccountResult::FoundAccount(pubkey, account.clone(), false);
4445            svm.write_account_update(found_update);
4446            assert_eq!(svm.get_all_accounts().unwrap(), index_before);
4447        }
4448
4449        // GetAccountResult::FoundAccount with `DoUpdateSvm` flag to true should update the account
4450        {
4451            let index_before = svm.get_all_accounts().unwrap();
4452            let found_update = GetAccountResult::FoundAccount(pubkey, account.clone(), true);
4453            svm.write_account_update(found_update);
4454            assert_eq!(
4455                svm.get_all_accounts().unwrap().len(),
4456                index_before.len() + 1
4457            );
4458            if !expect_account_update_event(&events_rx, &svm, &pubkey, &account) {
4459                panic!(
4460                    "Expected account update event not received after GetAccountResult::FoundAccount update"
4461                );
4462            }
4463        }
4464
4465        // GetAccountResult::FoundProgramAccount with no program account inserts a default programdata account
4466        {
4467            let (program_address, program_account, program_data_address, _) =
4468                create_program_accounts();
4469
4470            let mut data = bincode::serialize(
4471                &solana_loader_v3_interface::state::UpgradeableLoaderState::ProgramData {
4472                    slot: svm.get_latest_absolute_slot(),
4473                    upgrade_authority_address: Some(system_program::id()),
4474                },
4475            )
4476            .unwrap();
4477
4478            let mut bin = crate::surfnet::noop_program::NOOP_PROGRAM_ELF.to_vec();
4479            data.append(&mut bin); // push our binary after the state data
4480            let lamports = svm.inner.minimum_balance_for_rent_exemption(data.len());
4481            let default_program_data_account = Account {
4482                lamports,
4483                data,
4484                owner: solana_sdk_ids::bpf_loader_upgradeable::ID,
4485                executable: false,
4486                rent_epoch: 0,
4487            };
4488
4489            let index_before = svm.get_all_accounts().unwrap();
4490            let found_program_account_update = GetAccountResult::FoundProgramAccount(
4491                (program_address, program_account.clone()),
4492                (program_data_address, None),
4493            );
4494            svm.write_account_update(found_program_account_update);
4495
4496            if !expect_account_update_event(
4497                &events_rx,
4498                &svm,
4499                &program_data_address,
4500                &default_program_data_account,
4501            ) {
4502                panic!(
4503                    "Expected account update event not received after inserting default program data account"
4504                );
4505            }
4506
4507            if !expect_account_update_event(&events_rx, &svm, &program_address, &program_account) {
4508                panic!(
4509                    "Expected account update event not received after GetAccountResult::FoundProgramAccount update for program pubkey"
4510                );
4511            }
4512            assert_eq!(
4513                svm.get_all_accounts().unwrap().len(),
4514                index_before.len() + 2
4515            );
4516        }
4517
4518        // GetAccountResult::FoundProgramAccount with program account + program data account inserts two accounts
4519        {
4520            let (program_address, program_account, program_data_address, program_data_account) =
4521                create_program_accounts();
4522
4523            let index_before = svm.get_all_accounts().unwrap();
4524            let found_program_account_update = GetAccountResult::FoundProgramAccount(
4525                (program_address, program_account.clone()),
4526                (program_data_address, Some(program_data_account.clone())),
4527            );
4528            svm.write_account_update(found_program_account_update);
4529            assert_eq!(
4530                svm.get_all_accounts().unwrap().len(),
4531                index_before.len() + 2
4532            );
4533            if !expect_account_update_event(
4534                &events_rx,
4535                &svm,
4536                &program_data_address,
4537                &program_data_account,
4538            ) {
4539                panic!(
4540                    "Expected account update event not received after GetAccountResult::FoundProgramAccount update for program data pubkey"
4541                );
4542            }
4543
4544            if !expect_account_update_event(&events_rx, &svm, &program_address, &program_account) {
4545                panic!(
4546                    "Expected account update event not received after GetAccountResult::FoundProgramAccount update for program pubkey"
4547                );
4548            }
4549        }
4550
4551        // If we insert the program data account ahead of time, then have a GetAccountResult::FoundProgramAccount with just the program data account,
4552        // we should get one insert
4553        {
4554            let (program_address, program_account, program_data_address, program_data_account) =
4555                create_program_accounts();
4556
4557            let index_before = svm.get_all_accounts().unwrap();
4558            let found_update = GetAccountResult::FoundAccount(
4559                program_data_address,
4560                program_data_account.clone(),
4561                true,
4562            );
4563            svm.write_account_update(found_update);
4564            assert_eq!(
4565                svm.get_all_accounts().unwrap().len(),
4566                index_before.len() + 1
4567            );
4568            if !expect_account_update_event(
4569                &events_rx,
4570                &svm,
4571                &program_data_address,
4572                &program_data_account,
4573            ) {
4574                panic!(
4575                    "Expected account update event not received after GetAccountResult::FoundAccount update"
4576                );
4577            }
4578
4579            let index_before = svm.get_all_accounts().unwrap();
4580            let program_account_found_update = GetAccountResult::FoundProgramAccount(
4581                (program_address, program_account.clone()),
4582                (program_data_address, None),
4583            );
4584            svm.write_account_update(program_account_found_update);
4585            assert_eq!(
4586                svm.get_all_accounts().unwrap().len(),
4587                index_before.len() + 1
4588            );
4589            if !expect_account_update_event(&events_rx, &svm, &program_address, &program_account) {
4590                panic!(
4591                    "Expected account update event not received after GetAccountResult::FoundAccount update"
4592                );
4593            }
4594        }
4595    }
4596
4597    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4598    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4599    #[test_case(TestType::no_db(); "with no db")]
4600    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4601    fn test_encode_ui_account(test_type: TestType) {
4602        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4603
4604        let idl_v1: Idl =
4605            serde_json::from_slice(&include_bytes!("../tests/assets/idl_v1.json").to_vec())
4606                .unwrap();
4607
4608        svm.register_idl(idl_v1.clone(), Some(0)).unwrap();
4609
4610        let account_pubkey = Pubkey::new_unique();
4611
4612        #[derive(borsh::BorshSerialize)]
4613        pub struct CustomAccount {
4614            pub my_custom_data: u64,
4615            pub another_field: String,
4616            pub bool: bool,
4617            pub pubkey: Pubkey,
4618        }
4619
4620        // Account data not matching IDL schema should use default encoding
4621        {
4622            let account_data = vec![0; 100];
4623            let base64_data = general_purpose::STANDARD.encode(&account_data);
4624            let expected_data = UiAccountData::Binary(base64_data, UiAccountEncoding::Base64);
4625            let account = Account {
4626                lamports: 1000,
4627                data: account_data,
4628                owner: idl_v1.address.parse().unwrap(),
4629                executable: false,
4630                rent_epoch: 0,
4631            };
4632
4633            let ui_account = svm.encode_ui_account(
4634                &account_pubkey,
4635                &account,
4636                UiAccountEncoding::JsonParsed,
4637                None,
4638                None,
4639            );
4640            let expected_account = UiAccount {
4641                lamports: 1000,
4642                data: expected_data,
4643                owner: idl_v1.address.clone(),
4644                executable: false,
4645                rent_epoch: 0,
4646                space: Some(account.data.len() as u64),
4647            };
4648            assert_eq!(ui_account, expected_account);
4649        }
4650
4651        // valid account data matching IDL schema should be parsed
4652        {
4653            let mut account_data = idl_v1.accounts[0].discriminator.clone();
4654            let pubkey = Pubkey::new_unique();
4655            CustomAccount {
4656                my_custom_data: 42,
4657                another_field: "test".to_string(),
4658                bool: true,
4659                pubkey,
4660            }
4661            .serialize(&mut account_data)
4662            .unwrap();
4663
4664            let account = Account {
4665                lamports: 1000,
4666                data: account_data,
4667                owner: idl_v1.address.parse().unwrap(),
4668                executable: false,
4669                rent_epoch: 0,
4670            };
4671
4672            let ui_account = svm.encode_ui_account(
4673                &account_pubkey,
4674                &account,
4675                UiAccountEncoding::JsonParsed,
4676                None,
4677                None,
4678            );
4679            let expected_account = UiAccount {
4680                lamports: 1000,
4681                data: UiAccountData::Json(ParsedAccount {
4682                    program: format!("{}", idl_v1.metadata.name).to_case(convert_case::Case::Kebab),
4683                    parsed: serde_json::json!({
4684                        "my_custom_data": 42,
4685                        "another_field": "test",
4686                        "bool": true,
4687                        "pubkey": pubkey.to_string(),
4688                    }),
4689                    space: account.data.len() as u64,
4690                }),
4691                owner: idl_v1.address.clone(),
4692                executable: false,
4693                rent_epoch: 0,
4694                space: Some(account.data.len() as u64),
4695            };
4696            assert_eq!(ui_account, expected_account);
4697        }
4698
4699        let idl_v2: Idl =
4700            serde_json::from_slice(&include_bytes!("../tests/assets/idl_v2.json").to_vec())
4701                .unwrap();
4702
4703        svm.register_idl(idl_v2.clone(), Some(100)).unwrap();
4704
4705        // even though we have a new IDL that is more recent, we should be able to match with the old IDL
4706        {
4707            let mut account_data = idl_v1.accounts[0].discriminator.clone();
4708            let pubkey = Pubkey::new_unique();
4709            CustomAccount {
4710                my_custom_data: 42,
4711                another_field: "test".to_string(),
4712                bool: true,
4713                pubkey,
4714            }
4715            .serialize(&mut account_data)
4716            .unwrap();
4717
4718            let account = Account {
4719                lamports: 1000,
4720                data: account_data,
4721                owner: idl_v1.address.parse().unwrap(),
4722                executable: false,
4723                rent_epoch: 0,
4724            };
4725
4726            let ui_account = svm.encode_ui_account(
4727                &account_pubkey,
4728                &account,
4729                UiAccountEncoding::JsonParsed,
4730                None,
4731                None,
4732            );
4733            let expected_account = UiAccount {
4734                lamports: 1000,
4735                data: UiAccountData::Json(ParsedAccount {
4736                    program: format!("{}", idl_v1.metadata.name).to_case(convert_case::Case::Kebab),
4737                    parsed: serde_json::json!({
4738                        "my_custom_data": 42,
4739                        "another_field": "test",
4740                        "bool": true,
4741                        "pubkey": pubkey.to_string(),
4742                    }),
4743                    space: account.data.len() as u64,
4744                }),
4745                owner: idl_v1.address.clone(),
4746                executable: false,
4747                rent_epoch: 0,
4748                space: Some(account.data.len() as u64),
4749            };
4750            assert_eq!(ui_account, expected_account);
4751        }
4752
4753        // valid account data matching IDL v2 schema should be parsed, if svm slot reaches IDL registration slot
4754        {
4755            // use the v2 shape of the custom account
4756            #[derive(borsh::BorshSerialize)]
4757            pub struct CustomAccount {
4758                pub my_custom_data: u64,
4759                pub another_field: String,
4760                pub pubkey: Pubkey,
4761            }
4762            let mut account_data = idl_v1.accounts[0].discriminator.clone();
4763            let pubkey = Pubkey::new_unique();
4764            CustomAccount {
4765                my_custom_data: 42,
4766                another_field: "test".to_string(),
4767                pubkey,
4768            }
4769            .serialize(&mut account_data)
4770            .unwrap();
4771
4772            let account = Account {
4773                lamports: 1000,
4774                data: account_data.clone(),
4775                owner: idl_v1.address.parse().unwrap(),
4776                executable: false,
4777                rent_epoch: 0,
4778            };
4779
4780            let ui_account = svm.encode_ui_account(
4781                &account_pubkey,
4782                &account,
4783                UiAccountEncoding::JsonParsed,
4784                None,
4785                None,
4786            );
4787            let base64_data = general_purpose::STANDARD.encode(&account_data);
4788            let expected_data = UiAccountData::Binary(base64_data, UiAccountEncoding::Base64);
4789            let expected_account = UiAccount {
4790                lamports: 1000,
4791                data: expected_data,
4792                owner: idl_v1.address.clone(),
4793                executable: false,
4794                rent_epoch: 0,
4795                space: Some(account.data.len() as u64),
4796            };
4797            assert_eq!(ui_account, expected_account);
4798
4799            svm.latest_epoch_info.absolute_slot = 100; // simulate reaching the slot where IDL v2 was registered
4800
4801            let ui_account = svm.encode_ui_account(
4802                &account_pubkey,
4803                &account,
4804                UiAccountEncoding::JsonParsed,
4805                None,
4806                None,
4807            );
4808            let expected_account = UiAccount {
4809                lamports: 1000,
4810                data: UiAccountData::Json(ParsedAccount {
4811                    program: format!("{}", idl_v1.metadata.name).to_case(convert_case::Case::Kebab),
4812                    parsed: serde_json::json!({
4813                        "my_custom_data": 42,
4814                        "another_field": "test",
4815                        "pubkey": pubkey.to_string(),
4816                    }),
4817                    space: account.data.len() as u64,
4818                }),
4819                owner: idl_v1.address.clone(),
4820                executable: false,
4821                rent_epoch: 0,
4822                space: Some(account.data.len() as u64),
4823            };
4824            assert_eq!(ui_account, expected_account);
4825        }
4826    }
4827
4828    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4829    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4830    #[test_case(TestType::no_db(); "with no db")]
4831    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4832    fn test_profiling_map_capacity_default(test_type: TestType) {
4833        let (svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4834        assert_eq!(svm.max_profiles, DEFAULT_PROFILING_MAP_CAPACITY);
4835    }
4836
4837    #[test]
4838    fn test_default_uses_no_db_storage() {
4839        let (svm, _events_rx, _geyser_rx) = SurfnetSvm::default();
4840        assert!(svm.inner.db.is_none());
4841    }
4842
4843    #[cfg(feature = "sqlite")]
4844    #[test]
4845    fn test_new_with_db_uses_sqlite_storage() {
4846        let (svm, _events_rx, _geyser_rx) =
4847            SurfnetSvm::new_with_db(Some(":memory:"), SurfnetSvmConfig::default()).unwrap();
4848        assert!(svm.inner.db.is_some());
4849    }
4850
4851    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4852    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4853    fn test_new_with_db_restores_slot_checkpoint(test_type: TestType) {
4854        let (database_url, surfnet_id) = match &test_type {
4855            TestType::OnDiskSqlite(db_path) => {
4856                (db_path.as_str(), "slot-checkpoint-recovery".to_string())
4857            }
4858            #[cfg(feature = "postgres")]
4859            TestType::Postgres { url, surfnet_id } => (url.as_str(), surfnet_id.clone()),
4860            _ => unreachable!("test case must provide persistent database storage"),
4861        };
4862        let config = SurfnetSvmConfig {
4863            surfnet_id,
4864            ..SurfnetSvmConfig::default()
4865        };
4866        let target_slot = (*CHECKPOINT_INTERVAL_SLOTS).max(FINALIZATION_SLOT_THRESHOLD);
4867
4868        let checkpoint_slot = {
4869            let (mut svm, _events_rx, _geyser_rx) =
4870                SurfnetSvm::new_with_db(Some(database_url), config.clone()).unwrap();
4871            while svm.get_latest_absolute_slot() <= target_slot {
4872                svm.confirm_current_block().unwrap();
4873            }
4874
4875            let checkpoint_slot = svm
4876                .slot_checkpoint
4877                .get(&"latest_slot".to_string())
4878                .unwrap()
4879                .expect("checkpoint slot should be persisted");
4880            svm.shutdown();
4881            checkpoint_slot
4882        };
4883
4884        let (mut svm, _events_rx, _geyser_rx) =
4885            SurfnetSvm::new_with_db(Some(database_url), config).unwrap();
4886        assert_eq!(svm.get_latest_absolute_slot(), checkpoint_slot);
4887        assert_eq!(svm.latest_epoch_info.block_height, checkpoint_slot);
4888        assert_eq!(svm.chain_tip.index, checkpoint_slot);
4889        assert_eq!(
4890            svm.chain_tip.hash,
4891            SyntheticBlockhash::new(checkpoint_slot - 1).to_string()
4892        );
4893
4894        let clock = svm.inner.get_sysvar::<Clock>();
4895        assert_eq!(clock.slot, checkpoint_slot);
4896
4897        let recovered_blockhash = svm.chain_tip.hash.clone();
4898        svm.confirm_current_block().unwrap();
4899        assert_eq!(
4900            svm.chain_tip.hash,
4901            SyntheticBlockhash::new(checkpoint_slot).to_string()
4902        );
4903        assert_ne!(svm.chain_tip.hash, recovered_blockhash);
4904    }
4905
4906    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4907    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4908    fn test_new_with_db_restores_last_checkpoint_slot_from_block_only_recovery(
4909        test_type: TestType,
4910    ) {
4911        let (database_url, surfnet_id) = match &test_type {
4912            TestType::OnDiskSqlite(db_path) => {
4913                (db_path.as_str(), "block-only-recovery".to_string())
4914            }
4915            #[cfg(feature = "postgres")]
4916            TestType::Postgres { url, surfnet_id } => (url.as_str(), surfnet_id.clone()),
4917            _ => unreachable!("test case must provide persistent database storage"),
4918        };
4919        let config = SurfnetSvmConfig {
4920            surfnet_id,
4921            ..SurfnetSvmConfig::default()
4922        };
4923        let block_slot = (*CHECKPOINT_INTERVAL_SLOTS)
4924            .max(FINALIZATION_SLOT_THRESHOLD)
4925            .saturating_add(7);
4926
4927        {
4928            let (mut svm, _events_rx, _geyser_rx) =
4929                SurfnetSvm::new_with_db(Some(database_url), config.clone()).unwrap();
4930            svm.blocks
4931                .store(
4932                    block_slot,
4933                    BlockHeader {
4934                        hash: SyntheticBlockhash::new(block_slot - 1).to_string(),
4935                        previous_blockhash: SyntheticBlockhash::new(block_slot - 2).to_string(),
4936                        parent_slot: block_slot - 1,
4937                        block_time: 0,
4938                        block_height: block_slot,
4939                        signatures: vec![Signature::new_unique()],
4940                    },
4941                )
4942                .unwrap();
4943            assert!(
4944                svm.slot_checkpoint
4945                    .get(&"latest_slot".to_string())
4946                    .unwrap()
4947                    .is_none()
4948            );
4949            svm.shutdown();
4950        }
4951
4952        let (svm, _events_rx, _geyser_rx) =
4953            SurfnetSvm::new_with_db(Some(database_url), config).unwrap();
4954        assert_eq!(svm.get_latest_absolute_slot(), block_slot);
4955        assert_eq!(svm.latest_epoch_info.block_height, block_slot);
4956        assert_eq!(svm.last_checkpoint_slot, block_slot);
4957    }
4958
4959    #[test]
4960    fn test_constructor_applies_startup_config() {
4961        let config = SurfnetSvmConfig {
4962            surfnet_id: "constructor-test".to_string(),
4963            feature_config: SvmFeatureConfig::new().disable(disable_fees_sysvar::id()),
4964            slot_time: 123,
4965            instruction_profiling_enabled: false,
4966            max_profiles: 17,
4967            log_bytes_limit: None,
4968            skip_blockhash_check: true,
4969        };
4970        let (svm, _events_rx, _geyser_rx) = SurfnetSvm::new(config).unwrap();
4971
4972        assert_eq!(svm.slot_time, 123);
4973        assert!(!svm.instruction_profiling_enabled);
4974        assert_eq!(svm.max_profiles, 17);
4975        assert_eq!(
4976            svm.latest_epoch_info.absolute_slot,
4977            FINALIZATION_SLOT_THRESHOLD
4978        );
4979        assert_eq!(svm.genesis_slot, FINALIZATION_SLOT_THRESHOLD);
4980        assert!(!svm.feature_set.is_active(&disable_fees_sysvar::id()));
4981
4982        let epoch_schedule = svm.inner.get_sysvar::<EpochSchedule>();
4983        assert!(!epoch_schedule.warmup);
4984
4985        let registry = TemplateRegistry::new();
4986        for (_, template) in registry.templates {
4987            let program_id = template.idl.address.clone();
4988            assert!(svm.registered_idls.get(&program_id).unwrap().is_some());
4989        }
4990        assert!(svm.skip_blockhash_check);
4991    }
4992
4993    #[test]
4994    fn test_initialize_only_updates_remote_state() {
4995        let config = SurfnetSvmConfig {
4996            surfnet_id: "remote-init-test".to_string(),
4997            feature_config: SvmFeatureConfig::new().disable(disable_fees_sysvar::id()),
4998            slot_time: 321,
4999            instruction_profiling_enabled: false,
5000            max_profiles: 23,
5001            log_bytes_limit: None,
5002            skip_blockhash_check: false,
5003        };
5004        let (mut svm, _events_rx, _geyser_rx) = SurfnetSvm::new(config).unwrap();
5005        let epoch_info = EpochInfo {
5006            epoch: 7,
5007            slot_index: 4,
5008            slots_in_epoch: crate::surfnet::SLOTS_PER_EPOCH,
5009            absolute_slot: 777,
5010            block_height: 777,
5011            transaction_count: None,
5012        };
5013
5014        svm.initialize(epoch_info.clone(), EpochSchedule::without_warmup());
5015
5016        assert_eq!(svm.slot_time, 321);
5017        assert!(!svm.instruction_profiling_enabled);
5018        assert_eq!(svm.max_profiles, 23);
5019        assert!(!svm.feature_set.is_active(&disable_fees_sysvar::id()));
5020        assert_eq!(svm.latest_epoch_info, epoch_info);
5021        assert_eq!(svm.genesis_slot, 777);
5022    }
5023
5024    #[test]
5025    fn test_clone_for_profiling_preserves_skip_blockhash_check() {
5026        let (mut svm, _events_rx, _geyser_rx) = SurfnetSvm::default();
5027        svm.skip_blockhash_check = true;
5028
5029        let profiling_clone = svm.clone_for_profiling();
5030        assert!(profiling_clone.skip_blockhash_check);
5031    }
5032
5033    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
5034    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
5035    #[test_case(TestType::no_db(); "with no db")]
5036    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
5037    fn test_send_transaction_rejects_invalid_blockhash_by_default(test_type: TestType) {
5038        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
5039        let payer = Keypair::new();
5040        let recipient = Pubkey::new_unique();
5041        let lamports = 1_000_000_000;
5042        let invalid_blockhash = Hash::new_unique();
5043
5044        svm.airdrop(&payer.pubkey(), 2 * lamports).unwrap().unwrap();
5045        assert!(!svm.check_blockhash_is_recent(&invalid_blockhash));
5046
5047        let tx = build_transfer_transaction(&payer, &recipient, lamports, invalid_blockhash);
5048        let err = svm.send_transaction(tx, false, false).unwrap_err().err;
5049
5050        assert_eq!(err, TransactionError::BlockhashNotFound);
5051    }
5052
5053    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
5054    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
5055    #[test_case(TestType::no_db(); "with no db")]
5056    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
5057    fn test_skip_blockhash_check_bypasses_send_simulate_and_estimate(test_type: TestType) {
5058        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
5059        let payer = Keypair::new();
5060        let recipient = Pubkey::new_unique();
5061        let lamports = 1_000_000_000;
5062        let invalid_blockhash = Hash::new_unique();
5063
5064        svm.skip_blockhash_check = true;
5065        svm.airdrop(&payer.pubkey(), 2 * lamports).unwrap().unwrap();
5066        assert!(!svm.check_blockhash_is_recent(&invalid_blockhash));
5067
5068        let tx = build_transfer_transaction(&payer, &recipient, lamports, invalid_blockhash);
5069
5070        let estimate = svm.estimate_compute_units(&tx);
5071        assert!(
5072            estimate.success,
5073            "estimate should succeed when skip_blockhash_check is enabled: {:?}",
5074            estimate.error_message
5075        );
5076
5077        let simulation = svm.simulate_transaction(tx.clone(), false);
5078        assert!(
5079            simulation.is_ok(),
5080            "simulation should succeed when skip_blockhash_check is enabled: {:?}",
5081            simulation.err()
5082        );
5083
5084        let send_result = svm.send_transaction(tx, false, false);
5085        assert!(
5086            send_result.is_ok(),
5087            "send should succeed when skip_blockhash_check is enabled: {:?}",
5088            send_result.err().map(|err| err.err)
5089        );
5090    }
5091
5092    // Feature configuration tests
5093    //
5094    // Feature sets are now fully determined at SVM construction time (see
5095    // `SurfnetSvm::build` and `compose_feature_set`). These tests therefore
5096    // construct an SVM with the desired `SvmFeatureConfig` and assert on the
5097    // resulting state, rather than mutating an existing SVM.
5098
5099    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
5100    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
5101    #[test_case(TestType::no_db(); "with no db")]
5102    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
5103    fn test_feature_set_empty_config_uses_mainnet_baseline(test_type: TestType) {
5104        // An empty `SvmFeatureConfig` should yield exactly the mainnet baseline:
5105        // features active on mainnet are active; features not active on mainnet
5106        // are inactive.
5107        let (svm, _events_rx, _geyser_rx) =
5108            test_type.initialize_svm_with_features(SvmFeatureConfig::new());
5109
5110        assert!(svm.feature_set.is_active(&disable_fees_sysvar::id()));
5111        assert!(!svm.feature_set.is_active(&enable_loader_v4::id()));
5112    }
5113
5114    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
5115    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
5116    #[test_case(TestType::no_db(); "with no db")]
5117    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
5118    fn test_feature_set_enable_feature(test_type: TestType) {
5119        // `enable_loader_v4` is not active on the mainnet baseline; an explicit
5120        // enable in the config should turn it on.
5121        let feature_id = enable_loader_v4::id();
5122        let (svm, _events_rx, _geyser_rx) =
5123            test_type.initialize_svm_with_features(SvmFeatureConfig::new().enable(feature_id));
5124
5125        assert!(svm.feature_set.is_active(&feature_id));
5126    }
5127
5128    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
5129    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
5130    #[test_case(TestType::no_db(); "with no db")]
5131    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
5132    fn test_feature_set_disable_feature(test_type: TestType) {
5133        // `disable_fees_sysvar` is active on the mainnet baseline; an explicit
5134        // disable in the config should turn it off.
5135        let feature_id = disable_fees_sysvar::id();
5136        let (svm, _events_rx, _geyser_rx) =
5137            test_type.initialize_svm_with_features(SvmFeatureConfig::new().disable(feature_id));
5138
5139        assert!(!svm.feature_set.is_active(&feature_id));
5140    }
5141
5142    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
5143    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
5144    #[test_case(TestType::no_db(); "with no db")]
5145    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
5146    fn test_feature_set_mainnet_baseline(test_type: TestType) {
5147        // Default config → exact mainnet baseline. Spot-check a representative
5148        // sample of features on each side of the mainnet activation line.
5149        let (svm, _events_rx, _geyser_rx) =
5150            test_type.initialize_svm_with_features(SvmFeatureConfig::default());
5151
5152        // Not active on mainnet → inactive.
5153        assert!(!svm.feature_set.is_active(&enable_loader_v4::id()));
5154        assert!(
5155            !svm.feature_set
5156                .is_active(&enable_extend_program_checked::id())
5157        );
5158        assert!(!svm.feature_set.is_active(&blake3_syscall_enabled::id()));
5159        assert!(
5160            !svm.feature_set
5161                .is_active(&raise_cpi_nesting_limit_to_8::id())
5162        );
5163        assert!(
5164            !svm.feature_set
5165                .is_active(&stake_raise_minimum_delegation_to_1_sol::id())
5166        );
5167
5168        // Active on mainnet → active.
5169        assert!(svm.feature_set.is_active(&disable_fees_sysvar::id()));
5170        assert!(svm.feature_set.is_active(&curve25519_syscall_enabled::id()));
5171        assert!(
5172            svm.feature_set
5173                .is_active(&enable_sbpf_v1_deployment_and_execution::id())
5174        );
5175        assert!(
5176            svm.feature_set
5177                .is_active(&enable_sbpf_v2_deployment_and_execution::id())
5178        );
5179        assert!(
5180            svm.feature_set
5181                .is_active(&enable_sbpf_v3_deployment_and_execution::id())
5182        );
5183        assert!(
5184            svm.feature_set
5185                .is_active(&formalize_loaded_transaction_data_size::id())
5186        );
5187        assert!(
5188            svm.feature_set
5189                .is_active(&move_precompile_verification_to_svm::id())
5190        );
5191    }
5192
5193    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
5194    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
5195    #[test_case(TestType::no_db(); "with no db")]
5196    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
5197    fn test_feature_set_mainnet_with_override(test_type: TestType) {
5198        // Mainnet baseline + an explicit enable: the enabled feature should be
5199        // active while the rest of the mainnet baseline remains intact.
5200        let config = SvmFeatureConfig::default().enable(enable_loader_v4::id());
5201        let (svm, _events_rx, _geyser_rx) = test_type.initialize_svm_with_features(config);
5202
5203        assert!(svm.feature_set.is_active(&enable_loader_v4::id()));
5204        assert!(!svm.feature_set.is_active(&blake3_syscall_enabled::id()));
5205        assert!(
5206            !svm.feature_set
5207                .is_active(&enable_extend_program_checked::id())
5208        );
5209    }
5210
5211    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
5212    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
5213    #[test_case(TestType::no_db(); "with no db")]
5214    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
5215    fn test_feature_set_multiple_changes(test_type: TestType) {
5216        let config = SvmFeatureConfig::new()
5217            .enable(enable_loader_v4::id())
5218            .enable(enable_sbpf_v2_deployment_and_execution::id())
5219            .disable(disable_fees_sysvar::id())
5220            .disable(blake3_syscall_enabled::id());
5221
5222        let (svm, _events_rx, _geyser_rx) = test_type.initialize_svm_with_features(config);
5223
5224        assert!(svm.feature_set.is_active(&enable_loader_v4::id()));
5225        assert!(
5226            svm.feature_set
5227                .is_active(&enable_sbpf_v2_deployment_and_execution::id())
5228        );
5229        assert!(!svm.feature_set.is_active(&disable_fees_sysvar::id()));
5230        assert!(!svm.feature_set.is_active(&blake3_syscall_enabled::id()));
5231    }
5232
5233    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
5234    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
5235    #[test_case(TestType::no_db(); "with no db")]
5236    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
5237    fn test_feature_set_native_mint_present(test_type: TestType) {
5238        // Native mint must exist on a freshly constructed SVM regardless of
5239        // whether the user supplied feature overrides.
5240        let config = SvmFeatureConfig::new().disable(disable_fees_sysvar::id());
5241        let (svm, _events_rx, _geyser_rx) = test_type.initialize_svm_with_features(config);
5242
5243        assert!(
5244            svm.inner
5245                .get_account(&spl_token_interface::native_mint::ID)
5246                .unwrap()
5247                .is_some()
5248        );
5249    }
5250
5251    // Garbage collection tests
5252
5253    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
5254    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
5255    #[test_case(TestType::no_db(); "with no db")]
5256    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
5257    fn test_garbage_collected_account_tracking(test_type: TestType) {
5258        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
5259
5260        let owner = Pubkey::new_unique();
5261        let account_pubkey = Pubkey::new_unique();
5262
5263        let account = Account {
5264            lamports: 1000000,
5265            data: vec![1, 2, 3, 4, 5],
5266            owner,
5267            executable: false,
5268            rent_epoch: 0,
5269        };
5270
5271        svm.set_account(&account_pubkey, account.clone()).unwrap();
5272
5273        assert!(svm.get_account(&account_pubkey).unwrap().is_some());
5274        assert!(
5275            !svm.offline_accounts
5276                .contains_key(&account_pubkey.to_string())
5277                .unwrap()
5278        );
5279        assert_eq!(svm.get_account_owned_by(&owner).unwrap().len(), 1);
5280
5281        let empty_account = Account::default();
5282        svm.update_account_registries(&account_pubkey, &empty_account)
5283            .unwrap();
5284
5285        assert!(
5286            svm.offline_accounts
5287                .contains_key(&account_pubkey.to_string())
5288                .unwrap()
5289        );
5290
5291        assert_eq!(svm.get_account_owned_by(&owner).unwrap().len(), 0);
5292
5293        let owned_accounts = svm.get_account_owned_by(&owner).unwrap();
5294        assert!(!owned_accounts.iter().any(|(pk, _)| *pk == account_pubkey));
5295    }
5296
5297    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
5298    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
5299    #[test_case(TestType::no_db(); "with no db")]
5300    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
5301    fn test_garbage_collected_token_account_cleanup(test_type: TestType) {
5302        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
5303
5304        let token_owner = Pubkey::new_unique();
5305        let delegate = Pubkey::new_unique();
5306        let mint = Pubkey::new_unique();
5307        let token_account_pubkey = Pubkey::new_unique();
5308
5309        let mut token_account_data = [0u8; TokenAccount::LEN];
5310        let token_account = TokenAccount {
5311            mint,
5312            owner: token_owner,
5313            amount: 1000,
5314            delegate: COption::Some(delegate),
5315            state: AccountState::Initialized,
5316            is_native: COption::None,
5317            delegated_amount: 500,
5318            close_authority: COption::None,
5319        };
5320        token_account.pack_into_slice(&mut token_account_data);
5321
5322        let account = Account {
5323            lamports: 2000000,
5324            data: token_account_data.to_vec(),
5325            owner: spl_token_interface::id(),
5326            executable: false,
5327            rent_epoch: 0,
5328        };
5329
5330        svm.set_account(&token_account_pubkey, account).unwrap();
5331
5332        assert_eq!(
5333            svm.get_token_accounts_by_owner(&token_owner).unwrap().len(),
5334            1
5335        );
5336        assert_eq!(svm.get_token_accounts_by_delegate(&delegate).len(), 1);
5337        assert!(
5338            !svm.offline_accounts
5339                .contains_key(&token_account_pubkey.to_string())
5340                .unwrap()
5341        );
5342
5343        let empty_account = Account::default();
5344        svm.update_account_registries(&token_account_pubkey, &empty_account)
5345            .unwrap();
5346
5347        assert!(
5348            svm.offline_accounts
5349                .contains_key(&token_account_pubkey.to_string())
5350                .unwrap()
5351        );
5352
5353        assert_eq!(
5354            svm.get_token_accounts_by_owner(&token_owner).unwrap().len(),
5355            0
5356        );
5357        assert_eq!(svm.get_token_accounts_by_delegate(&delegate).len(), 0);
5358        assert!(
5359            svm.token_accounts
5360                .get(&token_account_pubkey.to_string())
5361                .unwrap()
5362                .is_none()
5363        );
5364    }
5365
5366    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
5367    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
5368    #[test_case(TestType::no_db(); "with no db")]
5369    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
5370    fn test_is_slot_in_valid_range(test_type: TestType) {
5371        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
5372
5373        // Set up: genesis_slot = 100, latest absolute slot = 110
5374        svm.genesis_slot = 100;
5375        svm.latest_epoch_info.absolute_slot = 110;
5376
5377        // Test slots within valid range
5378        assert!(
5379            svm.is_slot_in_valid_range(100),
5380            "genesis_slot should be valid"
5381        );
5382        assert!(
5383            svm.is_slot_in_valid_range(105),
5384            "middle slot should be valid"
5385        );
5386        assert!(
5387            svm.is_slot_in_valid_range(110),
5388            "latest slot should be valid"
5389        );
5390
5391        // Test slots outside valid range
5392        assert!(
5393            !svm.is_slot_in_valid_range(99),
5394            "slot before genesis should be invalid"
5395        );
5396        assert!(
5397            !svm.is_slot_in_valid_range(111),
5398            "slot after latest should be invalid"
5399        );
5400        assert!(
5401            !svm.is_slot_in_valid_range(0),
5402            "slot 0 should be invalid when genesis > 0"
5403        );
5404        assert!(
5405            !svm.is_slot_in_valid_range(1000),
5406            "far future slot should be invalid"
5407        );
5408    }
5409
5410    #[test]
5411    fn test_is_slot_in_valid_range_genesis_zero() {
5412        let (mut svm, _events_rx, _geyser_rx) = SurfnetSvm::default();
5413
5414        // Set up: genesis_slot = 0, latest absolute slot = 50
5415        svm.genesis_slot = 0;
5416        svm.latest_epoch_info.absolute_slot = 50;
5417
5418        // Test boundary conditions with genesis at 0
5419        assert!(
5420            svm.is_slot_in_valid_range(0),
5421            "slot 0 should be valid when genesis = 0"
5422        );
5423        assert!(
5424            svm.is_slot_in_valid_range(25),
5425            "middle slot should be valid"
5426        );
5427        assert!(
5428            svm.is_slot_in_valid_range(50),
5429            "latest slot should be valid"
5430        );
5431        assert!(
5432            !svm.is_slot_in_valid_range(51),
5433            "slot after latest should be invalid"
5434        );
5435    }
5436
5437    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
5438    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
5439    #[test_case(TestType::no_db(); "with no db")]
5440    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
5441    fn test_get_block_or_reconstruct_stored_block(test_type: TestType) {
5442        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
5443
5444        // Set up: genesis_slot = 0, latest absolute slot = 100
5445        svm.genesis_slot = 0;
5446        svm.latest_epoch_info.absolute_slot = 100;
5447
5448        // Store a block with transactions
5449        let stored_block = BlockHeader {
5450            hash: "stored_block_hash".to_string(),
5451            previous_blockhash: "prev_hash".to_string(),
5452            parent_slot: 49,
5453            block_time: 1234567890,
5454            block_height: 50,
5455            signatures: vec![Signature::new_unique()],
5456        };
5457        svm.blocks.store(50, stored_block.clone()).unwrap();
5458
5459        // Retrieve the stored block
5460        let result = svm.get_block_or_reconstruct(50).unwrap();
5461        assert!(result.is_some(), "should return stored block");
5462        let block = result.unwrap();
5463        assert_eq!(block.hash, "stored_block_hash");
5464        assert_eq!(block.signatures.len(), 1);
5465    }
5466
5467    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
5468    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
5469    #[test_case(TestType::no_db(); "with no db")]
5470    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
5471    fn test_get_block_or_reconstruct_empty_block(test_type: TestType) {
5472        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
5473
5474        // Set up: genesis_slot = 0, latest absolute slot = 100
5475        svm.genesis_slot = 0;
5476        svm.latest_epoch_info.absolute_slot = 100;
5477        svm.genesis_updated_at = 1000000; // 1 second in ms
5478        svm.slot_time = 400; // 400ms per slot
5479
5480        // Request a slot that wasn't stored (no block stored at slot 50)
5481        let result = svm.get_block_or_reconstruct(50).unwrap();
5482        assert!(
5483            result.is_some(),
5484            "should reconstruct empty block for valid slot"
5485        );
5486
5487        let block = result.unwrap();
5488        // Verify it's a reconstructed empty block
5489        assert!(
5490            block.signatures.is_empty(),
5491            "reconstructed block should have no signatures"
5492        );
5493        assert_eq!(block.block_height, 50);
5494        assert_eq!(block.parent_slot, 49);
5495
5496        // Verify the block time is calculated correctly
5497        // genesis_updated_at (1000000ms) + (50 slots * 400ms) = 1020000ms = 1020 seconds
5498        assert_eq!(block.block_time, 1020);
5499    }
5500
5501    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
5502    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
5503    #[test_case(TestType::no_db(); "with no db")]
5504    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
5505    fn test_get_block_or_reconstruct_out_of_range(test_type: TestType) {
5506        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
5507
5508        // Set up: genesis_slot = 100, latest absolute slot = 110
5509        svm.genesis_slot = 100;
5510        svm.latest_epoch_info.absolute_slot = 110;
5511
5512        // Request slot before genesis
5513        let result = svm.get_block_or_reconstruct(50).unwrap();
5514        assert!(
5515            result.is_none(),
5516            "should return None for slot before genesis"
5517        );
5518
5519        // Request slot after latest
5520        let result = svm.get_block_or_reconstruct(200).unwrap();
5521        assert!(result.is_none(), "should return None for slot after latest");
5522    }
5523
5524    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
5525    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
5526    #[test_case(TestType::no_db(); "with no db")]
5527    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
5528    #[allow(deprecated)]
5529    fn test_reconstruct_sysvars_recent_blockhashes(test_type: TestType) {
5530        use solana_sysvar::recent_blockhashes::RecentBlockhashes;
5531
5532        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
5533
5534        // Set up: chain_tip.index = 10, genesis_slot = 0
5535        svm.chain_tip = BlockIdentifier::new(10, "test_hash");
5536        svm.genesis_slot = 0;
5537        svm.latest_epoch_info.absolute_slot = 10;
5538
5539        svm.reconstruct_sysvars();
5540
5541        // Verify RecentBlockhashes sysvar
5542        let recent_blockhashes = svm.inner.get_sysvar::<RecentBlockhashes>();
5543
5544        // Should have 11 entries (indices 0 through 10)
5545        assert_eq!(recent_blockhashes.len(), 11);
5546
5547        // First entry should be the hash for chain_tip.index (10)
5548        let expected_hash = SyntheticBlockhash::new(10);
5549        assert_eq!(
5550            recent_blockhashes.first().unwrap().blockhash,
5551            *expected_hash.hash(),
5552            "First blockhash should match SyntheticBlockhash for chain_tip.index"
5553        );
5554
5555        // Last entry should be the hash for index 0
5556        let expected_last_hash = SyntheticBlockhash::new(0);
5557        assert_eq!(
5558            recent_blockhashes.last().unwrap().blockhash,
5559            *expected_last_hash.hash(),
5560            "Last blockhash should match SyntheticBlockhash for index 0"
5561        );
5562    }
5563
5564    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
5565    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
5566    #[test_case(TestType::no_db(); "with no db")]
5567    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
5568    #[allow(deprecated)]
5569    fn test_reconstruct_sysvars_slot_hashes(test_type: TestType) {
5570        use solana_slot_hashes::SlotHashes;
5571
5572        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
5573
5574        // Set up: chain_tip.index = 5, genesis_slot = 100 (absolute slot = 105)
5575        svm.chain_tip = BlockIdentifier::new(5, "test_hash");
5576        svm.genesis_slot = 100;
5577        svm.latest_epoch_info.absolute_slot = 105;
5578
5579        svm.reconstruct_sysvars();
5580
5581        // Verify SlotHashes sysvar
5582        let slot_hashes = svm.inner.get_sysvar::<SlotHashes>();
5583
5584        // Should include the finalized warmup window even though slots 74-99
5585        // are before the local genesis slot.
5586        assert_eq!(slot_hashes.len(), 32);
5587        assert!(
5588            slot_hashes.get(&74).is_some(),
5589            "SlotHashes should contain the finalized warmup floor"
5590        );
5591
5592        // Check that slot 105 maps to hash for index 5
5593        let expected_hash_105 = SyntheticBlockhash::new(5);
5594        let hash_for_105 = slot_hashes.get(&105);
5595        assert!(hash_for_105.is_some(), "SlotHashes should contain slot 105");
5596        assert_eq!(
5597            hash_for_105.unwrap(),
5598            expected_hash_105.hash(),
5599            "Hash for slot 105 should match SyntheticBlockhash for index 5"
5600        );
5601
5602        // Check that slot 100 maps to hash for index 0
5603        let expected_hash_100 = SyntheticBlockhash::new(0);
5604        let hash_for_100 = slot_hashes.get(&100);
5605        assert!(hash_for_100.is_some(), "SlotHashes should contain slot 100");
5606        assert_eq!(
5607            hash_for_100.unwrap(),
5608            expected_hash_100.hash(),
5609            "Hash for slot 100 should match SyntheticBlockhash for index 0"
5610        );
5611    }
5612
5613    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
5614    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
5615    #[test_case(TestType::no_db(); "with no db")]
5616    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
5617    fn test_reconstruct_sysvars_slot_hashes_cover_finalized_warmup(test_type: TestType) {
5618        use solana_slot_hashes::SlotHashes;
5619
5620        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
5621
5622        svm.chain_tip = BlockIdentifier::new(9, "test_hash");
5623        svm.genesis_slot = FINALIZATION_SLOT_THRESHOLD;
5624        svm.latest_epoch_info.absolute_slot = 40;
5625
5626        svm.reconstruct_sysvars();
5627
5628        let slot_hashes = svm.inner.get_sysvar::<SlotHashes>();
5629
5630        assert_eq!(
5631            slot_hashes.len(),
5632            (FINALIZATION_SLOT_THRESHOLD + 1) as usize
5633        );
5634        assert!(
5635            slot_hashes.get(&9).is_some(),
5636            "SlotHashes should include finalized slot 9 during warmup"
5637        );
5638        assert!(
5639            slot_hashes.get(&40).is_some(),
5640            "SlotHashes should include the processed slot"
5641        );
5642    }
5643
5644    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
5645    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
5646    #[test_case(TestType::no_db(); "with no db")]
5647    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
5648    fn test_reconstruct_sysvars_clock(test_type: TestType) {
5649        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
5650
5651        // Set up: chain_tip.index = 50, genesis_slot = 1000 (absolute slot = 1050)
5652        svm.chain_tip = BlockIdentifier::new(50, "test_hash");
5653        svm.genesis_slot = 1000;
5654        svm.latest_epoch_info.absolute_slot = 1050;
5655        svm.latest_epoch_info.epoch = 5;
5656        svm.genesis_updated_at = 2_000_000; // 2 seconds in ms
5657        svm.slot_time = 400; // 400ms per slot
5658
5659        svm.reconstruct_sysvars();
5660
5661        // Verify Clock sysvar
5662        let clock = svm.inner.get_sysvar::<Clock>();
5663
5664        assert_eq!(clock.slot, 1050, "Clock slot should be absolute slot");
5665        assert_eq!(clock.epoch, 5, "Clock epoch should match latest_epoch_info");
5666
5667        // Expected timestamp: genesis_updated_at + (50 slots * 400ms) = 2_000_000 + 20_000 = 2_020_000ms = 2020 seconds
5668        assert_eq!(
5669            clock.unix_timestamp, 2020,
5670            "Clock unix_timestamp should be calculated correctly"
5671        );
5672    }
5673
5674    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
5675    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
5676    #[test_case(TestType::no_db(); "with no db")]
5677    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
5678    #[allow(deprecated)]
5679    fn test_reconstruct_sysvars_max_blockhashes(test_type: TestType) {
5680        use solana_slot_hashes::SlotHashes;
5681        use solana_sysvar::recent_blockhashes::RecentBlockhashes;
5682
5683        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
5684
5685        // Set up: chain_tip.index = 600 (more than MAX_RECENT_BLOCKHASHES_STANDARD)
5686        svm.chain_tip = BlockIdentifier::new(600, "test_hash");
5687        svm.genesis_slot = 0;
5688        svm.latest_epoch_info.absolute_slot = 600;
5689
5690        svm.reconstruct_sysvars();
5691
5692        // Verify RecentBlockhashes sysvar is capped at MAX_RECENT_BLOCKHASHES_STANDARD
5693        let recent_blockhashes = svm.inner.get_sysvar::<RecentBlockhashes>();
5694
5695        assert_eq!(
5696            recent_blockhashes.len(),
5697            MAX_RECENT_BLOCKHASHES_STANDARD,
5698            "RecentBlockhashes should be capped at MAX_RECENT_BLOCKHASHES_STANDARD"
5699        );
5700
5701        let slot_hashes = svm.inner.get_sysvar::<SlotHashes>();
5702        assert_eq!(
5703            slot_hashes.len(),
5704            MAX_SLOT_HASHES_ENTRIES,
5705            "SlotHashes should be capped at MAX_SLOT_HASHES_ENTRIES"
5706        );
5707        assert!(
5708            slot_hashes.get(&89).is_some(),
5709            "SlotHashes should retain the 512-slot floor"
5710        );
5711        assert!(
5712            slot_hashes.get(&88).is_none(),
5713            "SlotHashes should evict slots older than the 512-slot floor"
5714        );
5715
5716        // First entry should still be for chain_tip.index (600)
5717        let expected_hash = SyntheticBlockhash::new(600);
5718        assert_eq!(
5719            recent_blockhashes.first().unwrap().blockhash,
5720            *expected_hash.hash(),
5721            "First blockhash should match SyntheticBlockhash for chain_tip.index"
5722        );
5723
5724        // Last RecentBlockhashes entry should use its own cap.
5725        let expected_last_hash =
5726            SyntheticBlockhash::new(600 - MAX_RECENT_BLOCKHASHES_STANDARD as u64 + 1);
5727        assert_eq!(
5728            recent_blockhashes.last().unwrap().blockhash,
5729            *expected_last_hash.hash(),
5730            "Last blockhash should match SyntheticBlockhash for start_index"
5731        );
5732    }
5733
5734    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
5735    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
5736    #[test_case(TestType::no_db(); "with no db")]
5737    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
5738    #[allow(deprecated)]
5739    fn test_reconstruct_sysvars_deterministic(test_type: TestType) {
5740        use solana_slot_hashes::SlotHashes;
5741        use solana_sysvar::recent_blockhashes::RecentBlockhashes;
5742
5743        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
5744
5745        // Set up initial state
5746        svm.chain_tip = BlockIdentifier::new(25, "test_hash");
5747        svm.genesis_slot = 50;
5748        svm.latest_epoch_info.absolute_slot = 75;
5749        svm.latest_epoch_info.epoch = 2;
5750        svm.genesis_updated_at = 1_000_000;
5751        svm.slot_time = 400;
5752
5753        // First reconstruction
5754        svm.reconstruct_sysvars();
5755        let blockhashes_1 = svm.inner.get_sysvar::<RecentBlockhashes>();
5756        let slot_hashes_1 = svm.inner.get_sysvar::<SlotHashes>();
5757        let clock_1 = svm.inner.get_sysvar::<Clock>();
5758
5759        // Second reconstruction with same state
5760        svm.reconstruct_sysvars();
5761        let blockhashes_2 = svm.inner.get_sysvar::<RecentBlockhashes>();
5762        let slot_hashes_2 = svm.inner.get_sysvar::<SlotHashes>();
5763        let clock_2 = svm.inner.get_sysvar::<Clock>();
5764
5765        // Verify determinism - results should be identical
5766        assert_eq!(blockhashes_1.len(), blockhashes_2.len());
5767        for (b1, b2) in blockhashes_1.iter().zip(blockhashes_2.iter()) {
5768            assert_eq!(
5769                b1.blockhash, b2.blockhash,
5770                "RecentBlockhashes should be deterministic"
5771            );
5772        }
5773
5774        assert_eq!(slot_hashes_1.len(), slot_hashes_2.len());
5775        assert_eq!(clock_1.slot, clock_2.slot);
5776        assert_eq!(clock_1.epoch, clock_2.epoch);
5777        assert_eq!(clock_1.unix_timestamp, clock_2.unix_timestamp);
5778    }
5779
5780    fn pick_known_feature_gate() -> Pubkey {
5781        *agave_feature_set::FEATURE_NAMES
5782            .keys()
5783            .next()
5784            .expect("agave_feature_set::FEATURE_NAMES is non-empty")
5785    }
5786
5787    fn seed_filterable_accounts(svm: &mut SurfnetSvm) -> (Pubkey, Pubkey, Pubkey) {
5788        let user_pubkey = Pubkey::new_unique();
5789        let user_account = Account {
5790            lamports: 1_000_000,
5791            data: vec![],
5792            owner: system_program::id(),
5793            executable: false,
5794            rent_epoch: 0,
5795        };
5796        svm.set_account(&user_pubkey, user_account).unwrap();
5797
5798        let sysvar_pubkey = Pubkey::new_unique();
5799        let sysvar_account = Account {
5800            lamports: 1,
5801            data: vec![1, 2, 3, 4],
5802            owner: solana_sdk_ids::sysvar::id(),
5803            executable: false,
5804            rent_epoch: 0,
5805        };
5806        svm.set_account(&sysvar_pubkey, sysvar_account).unwrap();
5807
5808        let feature_pubkey = pick_known_feature_gate();
5809        let feature_account = Account {
5810            lamports: 1,
5811            data: vec![],
5812            owner: solana_sdk_ids::feature::id(),
5813            executable: false,
5814            rent_epoch: 0,
5815        };
5816        svm.set_account(&feature_pubkey, feature_account).unwrap();
5817
5818        (user_pubkey, sysvar_pubkey, feature_pubkey)
5819    }
5820
5821    #[test]
5822    fn test_export_snapshot_default_includes_sysvars_and_feature_gates() {
5823        let (mut svm, _events_rx, _geyser_rx) = SurfnetSvm::default();
5824        let (user, sysvar, feature) = seed_filterable_accounts(&mut svm);
5825
5826        let snapshot = svm
5827            .export_snapshot(ExportSnapshotConfig::default())
5828            .unwrap();
5829
5830        assert!(snapshot.contains_key(&user.to_string()));
5831        assert!(snapshot.contains_key(&sysvar.to_string()));
5832        assert!(snapshot.contains_key(&feature.to_string()));
5833    }
5834
5835    #[test]
5836    fn test_export_snapshot_exclude_sysvars() {
5837        let (mut svm, _events_rx, _geyser_rx) = SurfnetSvm::default();
5838        let (user, sysvar, feature) = seed_filterable_accounts(&mut svm);
5839
5840        let snapshot = svm
5841            .export_snapshot(ExportSnapshotConfig {
5842                filter: Some(ExportSnapshotFilter {
5843                    exclude_sysvars: Some(true),
5844                    ..Default::default()
5845                }),
5846                ..Default::default()
5847            })
5848            .unwrap();
5849
5850        assert!(snapshot.contains_key(&user.to_string()));
5851        assert!(!snapshot.contains_key(&sysvar.to_string()));
5852        assert!(snapshot.contains_key(&feature.to_string()));
5853    }
5854
5855    #[test]
5856    fn test_export_snapshot_exclude_feature_gates() {
5857        let (mut svm, _events_rx, _geyser_rx) = SurfnetSvm::default();
5858        let (user, sysvar, feature) = seed_filterable_accounts(&mut svm);
5859
5860        let snapshot = svm
5861            .export_snapshot(ExportSnapshotConfig {
5862                filter: Some(ExportSnapshotFilter {
5863                    exclude_feature_gates: Some(true),
5864                    ..Default::default()
5865                }),
5866                ..Default::default()
5867            })
5868            .unwrap();
5869
5870        assert!(snapshot.contains_key(&user.to_string()));
5871        assert!(snapshot.contains_key(&sysvar.to_string()));
5872        assert!(!snapshot.contains_key(&feature.to_string()));
5873    }
5874
5875    #[test]
5876    fn test_export_snapshot_exclude_sysvars_and_feature_gates() {
5877        let (mut svm, _events_rx, _geyser_rx) = SurfnetSvm::default();
5878        let (user, sysvar, feature) = seed_filterable_accounts(&mut svm);
5879
5880        let snapshot = svm
5881            .export_snapshot(ExportSnapshotConfig {
5882                filter: Some(ExportSnapshotFilter {
5883                    exclude_sysvars: Some(true),
5884                    exclude_feature_gates: Some(true),
5885                    ..Default::default()
5886                }),
5887                ..Default::default()
5888            })
5889            .unwrap();
5890
5891        assert!(snapshot.contains_key(&user.to_string()));
5892        assert!(!snapshot.contains_key(&sysvar.to_string()));
5893        assert!(!snapshot.contains_key(&feature.to_string()));
5894    }
5895
5896    #[test]
5897    fn test_export_snapshot_include_accounts_overrides_exclusions() {
5898        let (mut svm, _events_rx, _geyser_rx) = SurfnetSvm::default();
5899        let (_user, sysvar, feature) = seed_filterable_accounts(&mut svm);
5900
5901        let snapshot = svm
5902            .export_snapshot(ExportSnapshotConfig {
5903                filter: Some(ExportSnapshotFilter {
5904                    exclude_sysvars: Some(true),
5905                    exclude_feature_gates: Some(true),
5906                    include_accounts: Some(vec![sysvar.to_string(), feature.to_string()]),
5907                    ..Default::default()
5908                }),
5909                ..Default::default()
5910            })
5911            .unwrap();
5912
5913        assert!(snapshot.contains_key(&sysvar.to_string()));
5914        assert!(snapshot.contains_key(&feature.to_string()));
5915    }
5916
5917    // ==========================================
5918    // PDA Derivation Tests
5919    // ==========================================
5920    // These tests verify the AccountAddress::resolve() method from surfpool_types
5921
5922    #[test]
5923    fn test_pda_derivation_with_string_seed() {
5924        // Test PDA derivation with a simple string seed
5925        let program_id = "11111111111111111111111111111111"; // System program
5926        let address = surfpool_types::AccountAddress::Pda {
5927            program_id: program_id.to_string(),
5928            seeds: vec![surfpool_types::PdaSeed::String("test_seed".to_string())],
5929        };
5930
5931        let result = address.resolve_simple();
5932        assert!(result.is_some(), "Should derive PDA with string seed");
5933
5934        // Verify it matches direct derivation
5935        let program_pubkey = Pubkey::from_str(program_id).unwrap();
5936        let (expected_pda, _) = Pubkey::find_program_address(&[b"test_seed"], &program_pubkey);
5937        assert_eq!(result.unwrap(), expected_pda);
5938    }
5939
5940    #[test]
5941    fn test_pda_derivation_with_pubkey_seed() {
5942        // Test PDA derivation with a pubkey seed
5943        let program_id = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
5944        let seed_pubkey = "So11111111111111111111111111111111111111112"; // Wrapped SOL
5945
5946        let address = surfpool_types::AccountAddress::Pda {
5947            program_id: program_id.to_string(),
5948            seeds: vec![surfpool_types::PdaSeed::Pubkey(seed_pubkey.to_string())],
5949        };
5950
5951        let result = address.resolve_simple();
5952        assert!(result.is_some(), "Should derive PDA with pubkey seed");
5953
5954        // Verify it matches direct derivation
5955        let program_pubkey = Pubkey::from_str(program_id).unwrap();
5956        let seed_pk = Pubkey::from_str(seed_pubkey).unwrap();
5957        let (expected_pda, _) = Pubkey::find_program_address(&[seed_pk.as_ref()], &program_pubkey);
5958        assert_eq!(result.unwrap(), expected_pda);
5959    }
5960
5961    #[test]
5962    fn test_pda_derivation_with_multiple_seeds() {
5963        // Test PDA derivation with multiple seeds of different types
5964        let program_id = "KLend2g3cP87fffoy8q1mQqGKjrxjC8boSyAYavgmjD"; // Kamino
5965        let lending_market = "ByYiZxp8QrdN9qbdtaAiePN8AAr3qvTPppNJDpf5DVJ5";
5966        let owner = "81BgcfZuZf9bESLvw3zDkh7cZmMtDwTPgkCvYu7zx26o";
5967
5968        let address = surfpool_types::AccountAddress::Pda {
5969            program_id: program_id.to_string(),
5970            seeds: vec![
5971                surfpool_types::PdaSeed::String("obligation".to_string()),
5972                surfpool_types::PdaSeed::Pubkey(lending_market.to_string()),
5973                surfpool_types::PdaSeed::Pubkey(owner.to_string()),
5974            ],
5975        };
5976
5977        let result = address.resolve_simple();
5978        assert!(result.is_some(), "Should derive PDA with multiple seeds");
5979
5980        // Verify it matches direct derivation
5981        let program_pubkey = Pubkey::from_str(program_id).unwrap();
5982        let market_pk = Pubkey::from_str(lending_market).unwrap();
5983        let owner_pk = Pubkey::from_str(owner).unwrap();
5984        let (expected_pda, _) = Pubkey::find_program_address(
5985            &[b"obligation", market_pk.as_ref(), owner_pk.as_ref()],
5986            &program_pubkey,
5987        );
5988        assert_eq!(result.unwrap(), expected_pda);
5989    }
5990
5991    #[test]
5992    fn test_pda_derivation_with_bytes_seed() {
5993        // Test PDA derivation with raw bytes seed
5994        let program_id = "11111111111111111111111111111111";
5995        let bytes_seed = vec![1u8, 2, 3, 4, 5, 6, 7, 8];
5996
5997        let address = surfpool_types::AccountAddress::Pda {
5998            program_id: program_id.to_string(),
5999            seeds: vec![surfpool_types::PdaSeed::Bytes(bytes_seed.clone())],
6000        };
6001
6002        let result = address.resolve_simple();
6003        assert!(result.is_some(), "Should derive PDA with bytes seed");
6004
6005        // Verify it matches direct derivation
6006        let program_pubkey = Pubkey::from_str(program_id).unwrap();
6007        let (expected_pda, _) = Pubkey::find_program_address(&[&bytes_seed], &program_pubkey);
6008        assert_eq!(result.unwrap(), expected_pda);
6009    }
6010
6011    #[test]
6012    fn test_pda_derivation_with_property_ref_pubkey() {
6013        // Test PDA derivation with PropertyRef that resolves to a pubkey
6014        let program_id = "11111111111111111111111111111111";
6015        let ref_pubkey = "So11111111111111111111111111111111111111112";
6016
6017        let address = surfpool_types::AccountAddress::Pda {
6018            program_id: program_id.to_string(),
6019            seeds: vec![surfpool_types::PdaSeed::PropertyRef(
6020                "my_pubkey".to_string(),
6021            )],
6022        };
6023
6024        let mut values = HashMap::new();
6025        values.insert(
6026            "my_pubkey".to_string(),
6027            serde_json::Value::String(ref_pubkey.to_string()),
6028        );
6029
6030        let result = address.resolve(Some(&values));
6031        assert!(
6032            result.is_some(),
6033            "Should derive PDA with property ref pubkey"
6034        );
6035
6036        // Verify it matches direct derivation
6037        let program_pubkey = Pubkey::from_str(program_id).unwrap();
6038        let seed_pk = Pubkey::from_str(ref_pubkey).unwrap();
6039        let (expected_pda, _) = Pubkey::find_program_address(&[seed_pk.as_ref()], &program_pubkey);
6040        assert_eq!(result.unwrap(), expected_pda);
6041    }
6042
6043    #[test]
6044    fn test_pda_derivation_with_property_ref_u64() {
6045        // Test PDA derivation with PropertyRef that resolves to a u64
6046        let program_id = "11111111111111111111111111111111";
6047        let ref_value: u64 = 12345;
6048
6049        let address = surfpool_types::AccountAddress::Pda {
6050            program_id: program_id.to_string(),
6051            seeds: vec![surfpool_types::PdaSeed::PropertyRef(
6052                "my_number".to_string(),
6053            )],
6054        };
6055
6056        let mut values = HashMap::new();
6057        values.insert(
6058            "my_number".to_string(),
6059            serde_json::Value::Number(ref_value.into()),
6060        );
6061
6062        let result = address.resolve(Some(&values));
6063        assert!(result.is_some(), "Should derive PDA with property ref u64");
6064
6065        // Verify it matches direct derivation
6066        let program_pubkey = Pubkey::from_str(program_id).unwrap();
6067        let (expected_pda, _) =
6068            Pubkey::find_program_address(&[&ref_value.to_le_bytes()], &program_pubkey);
6069        assert_eq!(result.unwrap(), expected_pda);
6070    }
6071
6072    #[test]
6073    fn test_pda_derivation_invalid_program_id() {
6074        // Test PDA derivation with invalid program ID
6075        let address = surfpool_types::AccountAddress::Pda {
6076            program_id: "invalid_program_id".to_string(),
6077            seeds: vec![surfpool_types::PdaSeed::String("test".to_string())],
6078        };
6079
6080        let result = address.resolve_simple();
6081        assert!(result.is_none(), "Should fail with invalid program ID");
6082    }
6083
6084    #[test]
6085    fn test_pda_derivation_invalid_pubkey_seed() {
6086        // Test PDA derivation with invalid pubkey in seed
6087        let program_id = "11111111111111111111111111111111";
6088        let address = surfpool_types::AccountAddress::Pda {
6089            program_id: program_id.to_string(),
6090            seeds: vec![surfpool_types::PdaSeed::Pubkey(
6091                "not_a_valid_pubkey".to_string(),
6092            )],
6093        };
6094
6095        let result = address.resolve_simple();
6096        assert!(result.is_none(), "Should fail with invalid pubkey seed");
6097    }
6098
6099    #[test]
6100    fn test_pda_derivation_missing_property_ref() {
6101        // Test PDA derivation with missing PropertyRef
6102        let program_id = "11111111111111111111111111111111";
6103        let address = surfpool_types::AccountAddress::Pda {
6104            program_id: program_id.to_string(),
6105            seeds: vec![surfpool_types::PdaSeed::PropertyRef(
6106                "nonexistent_property".to_string(),
6107            )],
6108        };
6109
6110        let result = address.resolve_simple(); // No values provided
6111        assert!(result.is_none(), "Should fail with missing property ref");
6112    }
6113
6114    #[test]
6115    fn test_pubkey_address_resolution() {
6116        // Test simple pubkey address resolution
6117        let pubkey_str = "So11111111111111111111111111111111111111112";
6118        let address = surfpool_types::AccountAddress::Pubkey(pubkey_str.to_string());
6119
6120        let result = address.resolve_simple();
6121        assert!(result.is_some(), "Should resolve pubkey address");
6122        assert_eq!(result.unwrap(), Pubkey::from_str(pubkey_str).unwrap());
6123    }
6124
6125    #[test]
6126    fn test_pda_deterministic() {
6127        // Test that PDA derivation is deterministic
6128        let program_id = "KLend2g3cP87fffoy8q1mQqGKjrxjC8boSyAYavgmjD";
6129        let address = surfpool_types::AccountAddress::Pda {
6130            program_id: program_id.to_string(),
6131            seeds: vec![
6132                surfpool_types::PdaSeed::String("reserve".to_string()),
6133                surfpool_types::PdaSeed::Pubkey(
6134                    "ByYiZxp8QrdN9qbdtaAiePN8AAr3qvTPppNJDpf5DVJ5".to_string(),
6135                ),
6136                surfpool_types::PdaSeed::Pubkey(
6137                    "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
6138                ),
6139            ],
6140        };
6141
6142        // Derive multiple times
6143        let result1 = address.resolve_simple();
6144        let result2 = address.resolve_simple();
6145        let result3 = address.resolve_simple();
6146
6147        assert!(result1.is_some());
6148        assert_eq!(result1, result2, "PDA derivation should be deterministic");
6149        assert_eq!(result2, result3, "PDA derivation should be deterministic");
6150    }
6151
6152    // ==========================================
6153    // Raydium CLMM Pool PDA Tests
6154    // ==========================================
6155    // These tests verify that we can correctly derive Raydium CLMM pool addresses
6156    // using the seeds: ["pool", amm_config, token_mint_0, token_mint_1]
6157    // Reference: https://github.com/raydium-io/raydium-clmm/blob/master/programs/amm/src/states/pool.rs
6158
6159    #[test]
6160    fn test_raydium_clmm_sol_usdc_pool_derivation() {
6161        // Test deriving the well-known SOL/USDC CLMM pool address
6162        // Pool address: 3ucNos4NbumPLZNWztqGHNFFgkHeRMBQAVemeeomsUxv
6163        // Source: Fetched from mainnet Raydium CLMM pool account data
6164
6165        let raydium_clmm_program = "CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK";
6166
6167        // The actual AMM config used by this pool (fetched from on-chain data)
6168        // This is NOT the standard 25bps config, it's a different one
6169        let amm_config = "3h2e43PunVA5K34vwKCLHWhZF4aZpyaC9RmxvshGAQpL";
6170
6171        // Token mints as stored in the pool (from on-chain data)
6172        // Note: The order depends on how the pool was created, not just alphabetical sorting
6173        let token_mint_0 = "So11111111111111111111111111111111111111112"; // SOL
6174        let token_mint_1 = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; // USDC
6175
6176        let address = surfpool_types::AccountAddress::Pda {
6177            program_id: raydium_clmm_program.to_string(),
6178            seeds: vec![
6179                surfpool_types::PdaSeed::String("pool".to_string()),
6180                surfpool_types::PdaSeed::Pubkey(amm_config.to_string()),
6181                surfpool_types::PdaSeed::Pubkey(token_mint_0.to_string()),
6182                surfpool_types::PdaSeed::Pubkey(token_mint_1.to_string()),
6183            ],
6184        };
6185
6186        let result = address.resolve_simple();
6187        assert!(result.is_some(), "Should derive Raydium CLMM pool PDA");
6188
6189        // Verify it matches the known pool address
6190        let expected_pool =
6191            Pubkey::from_str("3ucNos4NbumPLZNWztqGHNFFgkHeRMBQAVemeeomsUxv").unwrap();
6192        assert_eq!(
6193            result.unwrap(),
6194            expected_pool,
6195            "Derived pool address should match the known SOL/USDC CLMM pool"
6196        );
6197    }
6198
6199    #[test]
6200    fn test_raydium_clmm_pool_with_property_refs() {
6201        // Test deriving a pool PDA using PropertyRef (simulating how the YAML template works)
6202        let raydium_clmm_program = "CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK";
6203
6204        let address = surfpool_types::AccountAddress::Pda {
6205            program_id: raydium_clmm_program.to_string(),
6206            seeds: vec![
6207                surfpool_types::PdaSeed::String("pool".to_string()),
6208                surfpool_types::PdaSeed::PropertyRef("amm_config".to_string()),
6209                surfpool_types::PdaSeed::PropertyRef("token_mint_0".to_string()),
6210                surfpool_types::PdaSeed::PropertyRef("token_mint_1".to_string()),
6211            ],
6212        };
6213
6214        // Provide values via the HashMap (simulating user input)
6215        // Using the actual values from the mainnet SOL/USDC pool
6216        let mut values = HashMap::new();
6217        values.insert(
6218            "amm_config".to_string(),
6219            serde_json::Value::String("3h2e43PunVA5K34vwKCLHWhZF4aZpyaC9RmxvshGAQpL".to_string()),
6220        );
6221        values.insert(
6222            "token_mint_0".to_string(),
6223            serde_json::Value::String("So11111111111111111111111111111111111111112".to_string()),
6224        );
6225        values.insert(
6226            "token_mint_1".to_string(),
6227            serde_json::Value::String("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string()),
6228        );
6229
6230        let result = address.resolve(Some(&values));
6231        assert!(
6232            result.is_some(),
6233            "Should derive pool PDA with property refs"
6234        );
6235
6236        let expected_pool =
6237            Pubkey::from_str("3ucNos4NbumPLZNWztqGHNFFgkHeRMBQAVemeeomsUxv").unwrap();
6238        assert_eq!(
6239            result.unwrap(),
6240            expected_pool,
6241            "Property ref derived pool should match known pool"
6242        );
6243    }
6244
6245    #[test]
6246    fn test_raydium_clmm_different_fee_tiers() {
6247        // Test that different fee tiers produce different pool addresses for the same token pair
6248        let raydium_clmm_program = "CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK";
6249
6250        // Same token pair (SOL/USDC)
6251        let token_mint_0 = "So11111111111111111111111111111111111111112";
6252        let token_mint_1 = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
6253
6254        // Different fee tiers from our constants
6255        let fee_tiers = [
6256            (
6257                "ultra_tight",
6258                "4BLNHtVe942GSs4teSZqGX24xwKNkqU7bGgNn3iUiUpw",
6259            ), // 1 bps
6260            ("tight", "HfERMT5DRA6C1TAqecrJQFpmkf3wsWTMncqnj3RDg5aw"), // 5 bps
6261            ("standard", "E64NGkDLLCdQ2yFNPcavaKptrEgmiQaNykUuLC1Qgwyp"), // 25 bps
6262            ("wide", "A1BBtTYJd4i3xU8D6Tc2FzU6ZN4oXZWXKZnCxwbHXr8x"),  // 100 bps
6263        ];
6264
6265        let mut derived_pools = Vec::new();
6266
6267        for (tier_name, amm_config) in &fee_tiers {
6268            let address = surfpool_types::AccountAddress::Pda {
6269                program_id: raydium_clmm_program.to_string(),
6270                seeds: vec![
6271                    surfpool_types::PdaSeed::String("pool".to_string()),
6272                    surfpool_types::PdaSeed::Pubkey(amm_config.to_string()),
6273                    surfpool_types::PdaSeed::Pubkey(token_mint_0.to_string()),
6274                    surfpool_types::PdaSeed::Pubkey(token_mint_1.to_string()),
6275                ],
6276            };
6277
6278            let result = address.resolve_simple();
6279            assert!(
6280                result.is_some(),
6281                "Should derive pool for {} fee tier",
6282                tier_name
6283            );
6284            derived_pools.push((tier_name, result.unwrap()));
6285        }
6286
6287        // Verify all pools are different (different fee tiers = different pools)
6288        for i in 0..derived_pools.len() {
6289            for j in (i + 1)..derived_pools.len() {
6290                assert_ne!(
6291                    derived_pools[i].1, derived_pools[j].1,
6292                    "Pool for {} should differ from pool for {}",
6293                    derived_pools[i].0, derived_pools[j].0
6294                );
6295            }
6296        }
6297
6298        // Verify PDA derivation is deterministic
6299        for (tier_name, amm_config) in &fee_tiers {
6300            let address = surfpool_types::AccountAddress::Pda {
6301                program_id: raydium_clmm_program.to_string(),
6302                seeds: vec![
6303                    surfpool_types::PdaSeed::String("pool".to_string()),
6304                    surfpool_types::PdaSeed::Pubkey(amm_config.to_string()),
6305                    surfpool_types::PdaSeed::Pubkey(token_mint_0.to_string()),
6306                    surfpool_types::PdaSeed::Pubkey(token_mint_1.to_string()),
6307                ],
6308            };
6309            let result2 = address.resolve_simple().unwrap();
6310            let original = derived_pools
6311                .iter()
6312                .find(|(name, _)| *name == tier_name)
6313                .unwrap();
6314            assert_eq!(
6315                original.1, result2,
6316                "PDA derivation should be deterministic for {} tier",
6317                tier_name
6318            );
6319        }
6320    }
6321
6322    #[test]
6323    fn test_raydium_clmm_token_order_matters() {
6324        // Test that token mint order matters for PDA derivation
6325        // The pool PDA is derived from the exact token order used during pool creation
6326        let raydium_clmm_program = "CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK";
6327        // Using the actual AMM config from the mainnet SOL/USDC pool
6328        let amm_config = "3h2e43PunVA5K34vwKCLHWhZF4aZpyaC9RmxvshGAQpL";
6329
6330        let usdc = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
6331        let sol = "So11111111111111111111111111111111111111112";
6332
6333        // Order as it appears in the actual pool (SOL first, USDC second)
6334        let address_sol_usdc = surfpool_types::AccountAddress::Pda {
6335            program_id: raydium_clmm_program.to_string(),
6336            seeds: vec![
6337                surfpool_types::PdaSeed::String("pool".to_string()),
6338                surfpool_types::PdaSeed::Pubkey(amm_config.to_string()),
6339                surfpool_types::PdaSeed::Pubkey(sol.to_string()),
6340                surfpool_types::PdaSeed::Pubkey(usdc.to_string()),
6341            ],
6342        };
6343
6344        // Swapped order (USDC first, SOL second)
6345        let address_usdc_sol = surfpool_types::AccountAddress::Pda {
6346            program_id: raydium_clmm_program.to_string(),
6347            seeds: vec![
6348                surfpool_types::PdaSeed::String("pool".to_string()),
6349                surfpool_types::PdaSeed::Pubkey(amm_config.to_string()),
6350                surfpool_types::PdaSeed::Pubkey(usdc.to_string()),
6351                surfpool_types::PdaSeed::Pubkey(sol.to_string()),
6352            ],
6353        };
6354
6355        let result_sol_usdc = address_sol_usdc.resolve_simple().unwrap();
6356        let result_usdc_sol = address_usdc_sol.resolve_simple().unwrap();
6357
6358        // They should produce different PDAs
6359        assert_ne!(
6360            result_sol_usdc, result_usdc_sol,
6361            "Different token order should produce different PDA"
6362        );
6363
6364        // Only the correct order (SOL, USDC) matches the known pool
6365        let expected_pool =
6366            Pubkey::from_str("3ucNos4NbumPLZNWztqGHNFFgkHeRMBQAVemeeomsUxv").unwrap();
6367        assert_eq!(
6368            result_sol_usdc, expected_pool,
6369            "SOL/USDC order should match known pool"
6370        );
6371        assert_ne!(
6372            result_usdc_sol, expected_pool,
6373            "USDC/SOL order should NOT match known pool"
6374        );
6375    }
6376
6377    #[test]
6378    fn test_raydium_clmm_amm_config_derivation() {
6379        // Test that we can derive AMM config addresses from index
6380        // Seeds: ["amm_config", index.to_be_bytes()]
6381        let raydium_clmm_program = "CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK";
6382
6383        // Test known indices and their expected addresses
6384        let test_cases = [
6385            (0u16, "4BLNHtVe942GSs4teSZqGX24xwKNkqU7bGgNn3iUiUpw"), // ultra_tight
6386            (1u16, "E64NGkDLLCdQ2yFNPcavaKptrEgmiQaNykUuLC1Qgwyp"), // standard
6387            (2u16, "HfERMT5DRA6C1TAqecrJQFpmkf3wsWTMncqnj3RDg5aw"), // tight
6388            (3u16, "A1BBtTYJd4i3xU8D6Tc2FzU6ZN4oXZWXKZnCxwbHXr8x"), // wide
6389            (8u16, "3h2e43PunVA5K34vwKCLHWhZF4aZpyaC9RmxvshGAQpL"), // SOL/USDC main pool config
6390        ];
6391
6392        for (index, expected_address) in &test_cases {
6393            let address = surfpool_types::AccountAddress::Pda {
6394                program_id: raydium_clmm_program.to_string(),
6395                seeds: vec![
6396                    surfpool_types::PdaSeed::String("amm_config".to_string()),
6397                    surfpool_types::PdaSeed::U16Be(*index),
6398                ],
6399            };
6400
6401            let result = address.resolve_simple();
6402            assert!(
6403                result.is_some(),
6404                "Should derive AMM config for index {}",
6405                index
6406            );
6407
6408            let expected = Pubkey::from_str(expected_address).unwrap();
6409            assert_eq!(
6410                result.unwrap(),
6411                expected,
6412                "AMM config for index {} should match",
6413                index
6414            );
6415        }
6416    }
6417
6418    #[test]
6419    fn test_raydium_clmm_dynamic_pool_derivation() {
6420        // Test fully dynamic pool derivation:
6421        // 1. Derive AMM config from index using nested PDA
6422        // 2. Use that to derive pool address
6423        let raydium_clmm_program = "CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK";
6424
6425        // The SOL/USDC main pool uses config index 8
6426        let config_index = 8u16;
6427        let sol = "So11111111111111111111111111111111111111112";
6428        let usdc = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
6429
6430        // Using DerivedPda to dynamically derive the AMM config
6431        let address = surfpool_types::AccountAddress::Pda {
6432            program_id: raydium_clmm_program.to_string(),
6433            seeds: vec![
6434                surfpool_types::PdaSeed::String("pool".to_string()),
6435                // Nested PDA derivation for amm_config
6436                surfpool_types::PdaSeed::DerivedPda {
6437                    program_id: raydium_clmm_program.to_string(),
6438                    seeds: vec![
6439                        surfpool_types::PdaSeed::String("amm_config".to_string()),
6440                        surfpool_types::PdaSeed::U16Be(config_index),
6441                    ],
6442                },
6443                surfpool_types::PdaSeed::Pubkey(sol.to_string()),
6444                surfpool_types::PdaSeed::Pubkey(usdc.to_string()),
6445            ],
6446        };
6447
6448        let result = address.resolve_simple();
6449        assert!(result.is_some(), "Should derive pool with nested PDA");
6450
6451        // Should match the known SOL/USDC pool
6452        let expected_pool =
6453            Pubkey::from_str("3ucNos4NbumPLZNWztqGHNFFgkHeRMBQAVemeeomsUxv").unwrap();
6454        assert_eq!(
6455            result.unwrap(),
6456            expected_pool,
6457            "Dynamic derivation should match known SOL/USDC pool"
6458        );
6459    }
6460
6461    #[test]
6462    fn test_raydium_clmm_dynamic_pool_with_property_refs() {
6463        // Test dynamic pool derivation using property refs (simulating YAML template)
6464        let raydium_clmm_program = "CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK";
6465
6466        let address = surfpool_types::AccountAddress::Pda {
6467            program_id: raydium_clmm_program.to_string(),
6468            seeds: vec![
6469                surfpool_types::PdaSeed::String("pool".to_string()),
6470                // Nested PDA derivation for amm_config using U16BeRef
6471                surfpool_types::PdaSeed::DerivedPda {
6472                    program_id: raydium_clmm_program.to_string(),
6473                    seeds: vec![
6474                        surfpool_types::PdaSeed::String("amm_config".to_string()),
6475                        surfpool_types::PdaSeed::U16BeRef("config_index".to_string()),
6476                    ],
6477                },
6478                surfpool_types::PdaSeed::PropertyRef("token_mint_0".to_string()),
6479                surfpool_types::PdaSeed::PropertyRef("token_mint_1".to_string()),
6480            ],
6481        };
6482
6483        // Provide values (simulating user selecting from dropdowns)
6484        let mut values = HashMap::new();
6485        values.insert(
6486            "config_index".to_string(),
6487            serde_json::Value::Number(8.into()), // Index 8 for SOL/USDC main pool config
6488        );
6489        values.insert(
6490            "token_mint_0".to_string(),
6491            serde_json::Value::String("So11111111111111111111111111111111111111112".to_string()),
6492        );
6493        values.insert(
6494            "token_mint_1".to_string(),
6495            serde_json::Value::String("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string()),
6496        );
6497
6498        let result = address.resolve(Some(&values));
6499        assert!(result.is_some(), "Should derive pool with property refs");
6500
6501        let expected_pool =
6502            Pubkey::from_str("3ucNos4NbumPLZNWztqGHNFFgkHeRMBQAVemeeomsUxv").unwrap();
6503        assert_eq!(
6504            result.unwrap(),
6505            expected_pool,
6506            "Property ref dynamic derivation should match known SOL/USDC pool"
6507        );
6508    }
6509
6510    #[test]
6511    fn test_snapshot_export_restore_round_trip() {
6512        use std::{collections::HashMap, io::Write};
6513
6514        use surfpool_types::AccountSnapshot;
6515        use tempfile::NamedTempFile;
6516
6517        let (mut svm, _events_rx, _geyser_rx) =
6518            SurfnetSvm::new(SurfnetSvmConfig::default()).unwrap();
6519
6520        // Create test accounts with different characteristics
6521        let test_accounts: Vec<(Pubkey, Account)> = vec![
6522            // Simple account with SOL
6523            (
6524                Pubkey::new_unique(),
6525                Account {
6526                    lamports: 1_000_000_000,
6527                    data: vec![],
6528                    owner: solana_sdk_ids::system_program::id(),
6529                    executable: false,
6530                    rent_epoch: 0,
6531                },
6532            ),
6533            // Account with data
6534            (
6535                Pubkey::new_unique(),
6536                Account {
6537                    lamports: 500_000,
6538                    data: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
6539                    owner: Pubkey::new_unique(),
6540                    executable: false,
6541                    rent_epoch: 100,
6542                },
6543            ),
6544            // Account with larger data
6545            (
6546                Pubkey::new_unique(),
6547                Account {
6548                    lamports: 2_000_000,
6549                    data: (0..=255u8).collect::<Vec<u8>>(),
6550                    owner: Pubkey::new_unique(),
6551                    executable: false,
6552                    rent_epoch: 200,
6553                },
6554            ),
6555        ];
6556
6557        // Set all accounts in the SVM
6558        for (pubkey, account) in &test_accounts {
6559            svm.set_account(pubkey, account.clone())
6560                .expect("Failed to set account");
6561        }
6562
6563        // Verify accounts are set correctly
6564        for (pubkey, expected_account) in &test_accounts {
6565            let actual_account = svm
6566                .get_account(pubkey)
6567                .expect("get_account should not error")
6568                .expect("Account should exist");
6569            assert_eq!(
6570                actual_account.lamports, expected_account.lamports,
6571                "Lamports should match for {}",
6572                pubkey
6573            );
6574            assert_eq!(
6575                actual_account.data, expected_account.data,
6576                "Data should match for {}",
6577                pubkey
6578            );
6579            assert_eq!(
6580                actual_account.owner, expected_account.owner,
6581                "Owner should match for {}",
6582                pubkey
6583            );
6584        }
6585
6586        // Create a snapshot (simulating what export_snapshot does)
6587        let mut snapshot: HashMap<String, AccountSnapshot> = HashMap::new();
6588        for (pubkey, account) in &test_accounts {
6589            let account_snapshot = AccountSnapshot::new(
6590                account.lamports,
6591                account.owner.to_string(),
6592                account.executable,
6593                account.rent_epoch,
6594                general_purpose::STANDARD.encode(&account.data),
6595                None,
6596            );
6597            snapshot.insert(pubkey.to_string(), account_snapshot);
6598        }
6599
6600        // Serialize snapshot to JSON
6601        let snapshot_json =
6602            serde_json::to_string_pretty(&snapshot).expect("Failed to serialize snapshot");
6603
6604        // Write snapshot to a temp file
6605        let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
6606        temp_file
6607            .write_all(snapshot_json.as_bytes())
6608            .expect("Failed to write snapshot");
6609        let snapshot_path = temp_file.path().to_str().unwrap();
6610
6611        // Create a new SVM instance and restore the snapshot
6612        let (mut svm2, _events_rx2, _geyser_rx2) =
6613            SurfnetSvm::new(SurfnetSvmConfig::default()).unwrap();
6614
6615        // Verify accounts don't exist in new SVM
6616        for (pubkey, _) in &test_accounts {
6617            assert!(
6618                svm2.get_account(pubkey)
6619                    .expect("get_account should not error")
6620                    .is_none(),
6621                "Account {} should not exist in new SVM before restore",
6622                pubkey
6623            );
6624        }
6625
6626        // Restore from snapshot
6627        let restored_count = svm2
6628            .restore_from_snapshot(snapshot_path)
6629            .expect("Failed to restore from snapshot");
6630
6631        assert_eq!(
6632            restored_count,
6633            test_accounts.len(),
6634            "Should restore all accounts"
6635        );
6636
6637        // Verify all accounts were restored correctly
6638        for (pubkey, expected_account) in &test_accounts {
6639            let restored_account = svm2
6640                .get_account(pubkey)
6641                .expect("get_account should not error")
6642                .unwrap_or_else(|| panic!("Account {} should exist after restore", pubkey));
6643
6644            assert_eq!(
6645                restored_account.lamports, expected_account.lamports,
6646                "Lamports should match after restore for {}",
6647                pubkey
6648            );
6649            assert_eq!(
6650                restored_account.data, expected_account.data,
6651                "Data should match after restore for {}",
6652                pubkey
6653            );
6654            assert_eq!(
6655                restored_account.owner, expected_account.owner,
6656                "Owner should match after restore for {}",
6657                pubkey
6658            );
6659            assert_eq!(
6660                restored_account.executable, expected_account.executable,
6661                "Executable flag should match after restore for {}",
6662                pubkey
6663            );
6664            assert_eq!(
6665                restored_account.rent_epoch, expected_account.rent_epoch,
6666                "Rent epoch should match after restore for {}",
6667                pubkey
6668            );
6669        }
6670    }
6671
6672    #[test]
6673    fn test_snapshot_restore_invalid_file() {
6674        let (mut svm, _events_rx, _geyser_rx) =
6675            SurfnetSvm::new(SurfnetSvmConfig::default()).unwrap();
6676
6677        // Test with non-existent file
6678        let result = svm.restore_from_snapshot("/nonexistent/path/to/snapshot.json");
6679        assert!(result.is_err(), "Should fail with non-existent file");
6680
6681        // Test with invalid JSON
6682        use std::io::Write;
6683
6684        use tempfile::NamedTempFile;
6685
6686        let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
6687        temp_file
6688            .write_all(b"not valid json")
6689            .expect("Failed to write");
6690        let invalid_json_path = temp_file.path().to_str().unwrap();
6691
6692        let result = svm.restore_from_snapshot(invalid_json_path);
6693        assert!(result.is_err(), "Should fail with invalid JSON");
6694    }
6695
6696    #[test]
6697    fn test_snapshot_restore_partial_failure() {
6698        use std::{collections::HashMap, io::Write};
6699
6700        use surfpool_types::AccountSnapshot;
6701        use tempfile::NamedTempFile;
6702
6703        let (mut svm, _events_rx, _geyser_rx) =
6704            SurfnetSvm::new(SurfnetSvmConfig::default()).unwrap();
6705
6706        // Create a snapshot with one valid and one invalid account
6707        let mut snapshot: HashMap<String, AccountSnapshot> = HashMap::new();
6708
6709        // Valid account
6710        let valid_pubkey = Pubkey::new_unique();
6711        snapshot.insert(
6712            valid_pubkey.to_string(),
6713            AccountSnapshot::new(
6714                1_000_000,
6715                solana_sdk_ids::system_program::id().to_string(),
6716                false,
6717                0,
6718                general_purpose::STANDARD.encode(&[1, 2, 3]),
6719                None,
6720            ),
6721        );
6722
6723        // Account with invalid owner pubkey
6724        snapshot.insert(
6725            Pubkey::new_unique().to_string(),
6726            AccountSnapshot::new(
6727                500_000,
6728                "invalid_pubkey_string".to_string(), // Invalid owner
6729                false,
6730                0,
6731                general_purpose::STANDARD.encode(&[4, 5, 6]),
6732                None,
6733            ),
6734        );
6735
6736        // Write snapshot to temp file
6737        let snapshot_json =
6738            serde_json::to_string_pretty(&snapshot).expect("Failed to serialize snapshot");
6739        let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
6740        temp_file
6741            .write_all(snapshot_json.as_bytes())
6742            .expect("Failed to write snapshot");
6743        let snapshot_path = temp_file.path().to_str().unwrap();
6744
6745        // Restore should succeed but only restore the valid account
6746        let restored_count = svm
6747            .restore_from_snapshot(snapshot_path)
6748            .expect("Restore should succeed even with partial failures");
6749
6750        assert_eq!(restored_count, 1, "Should restore only the valid account");
6751
6752        // Verify the valid account was restored
6753        let restored_account = svm
6754            .get_account(&valid_pubkey)
6755            .expect("get_account should not error")
6756            .expect("Valid account should be restored");
6757        assert_eq!(restored_account.lamports, 1_000_000);
6758    }
6759}