Skip to main content

surfpool_core/surfnet/
locker.rs

1use std::{
2    collections::{BTreeMap, HashMap, HashSet},
3    sync::Arc,
4    time::SystemTime,
5};
6
7use bincode::serialized_size;
8use crossbeam_channel::{Receiver, Sender};
9use itertools::Itertools;
10use litesvm::types::{
11    FailedTransactionMetadata, SimulatedTransactionInfo, TransactionMetadata, TransactionResult,
12};
13use solana_account::{Account, ReadableAccount};
14use solana_account_decoder::{
15    UiAccount, UiAccountEncoding, UiDataSliceConfig,
16    parse_account_data::AccountAdditionalDataV3,
17    parse_bpf_loader::{BpfUpgradeableLoaderAccountType, UiProgram, parse_bpf_upgradeable_loader},
18    parse_token::UiTokenAmount,
19};
20use solana_address_lookup_table_interface::state::AddressLookupTable;
21use solana_client::{
22    rpc_client::SerializableTransaction,
23    rpc_config::{
24        RpcAccountInfoConfig, RpcBlockConfig, RpcLargestAccountsConfig, RpcLargestAccountsFilter,
25        RpcSignaturesForAddressConfig, RpcTransactionConfig, RpcTransactionLogsFilter,
26    },
27    rpc_filter::RpcFilterType,
28    rpc_request::TokenAccountsFilter,
29    rpc_response::{
30        RpcAccountBalance, RpcConfirmedTransactionStatusWithSignature, RpcKeyedAccount,
31        RpcLogsResponse, RpcTokenAccountBalance,
32    },
33};
34use solana_clock::{Clock, Slot, UnixTimestamp};
35use solana_commitment_config::{CommitmentConfig, CommitmentLevel};
36use solana_epoch_info::EpochInfo;
37use solana_hash::Hash;
38use solana_loader_v3_interface::{get_program_data_address, state::UpgradeableLoaderState};
39use solana_message::{
40    Message, SimpleAddressLoader, VersionedMessage,
41    compiled_instruction::CompiledInstruction,
42    v0::{LoadedAddresses, MessageAddressTableLookup},
43};
44use solana_pubkey::Pubkey;
45use solana_rpc_client_api::response::{SlotInfo, SlotUpdate};
46use solana_signature::Signature;
47use solana_transaction::{sanitized::SanitizedTransaction, versioned::VersionedTransaction};
48use solana_transaction_error::TransactionError;
49use solana_transaction_status::{
50    EncodedConfirmedTransactionWithStatusMeta,
51    TransactionConfirmationStatus as SolanaTransactionConfirmationStatus, UiConfirmedBlock,
52    UiTransactionEncoding, VersionedTransactionWithStatusMeta, extract_and_fmt_memos,
53};
54use surfpool_types::{
55    AccountSnapshot, ComputeUnitsEstimationResult, ExecutionCapture, ExportSnapshotConfig, Idl,
56    KeyedProfileResult, ProfileResult, RpcProfileResultConfig, RunbookExecutionStatusReport,
57    SimnetCommand, SimnetEvent, TransactionConfirmationStatus, TransactionStatusEvent,
58    UiKeyedProfileResult, UuidOrSignature, VersionedIdl,
59};
60use tokio::sync::RwLock;
61use txtx_addon_kit::indexmap::IndexSet;
62use uuid::Uuid;
63
64use super::{
65    AccountFactory, GetAccountResult, GetTransactionResult, GeyserEvent, SignatureSubscriptionType,
66    SurfnetSvm, remote::SurfnetRemoteClient,
67};
68use crate::{
69    error::{AirdropError, SurfpoolError, SurfpoolResult},
70    helpers::time_travel::calculate_time_travel_clock,
71    rpc::utils::{convert_transaction_metadata_from_canonical, verify_pubkey},
72    storage::StorageResult,
73    surfnet::FINALIZATION_SLOT_THRESHOLD,
74    types::{
75        GeyserAccountUpdate, OfflineAccountConfig, RemoteRpcResult, SurfnetTransactionStatus,
76        TimeTravelConfig, TokenAccount, TransactionLoadedAddresses, TransactionWithStatusMeta,
77    },
78};
79
80enum ProcessTransactionResult {
81    Success(TransactionMetadata),
82    SimulationFailure(FailedTransactionMetadata),
83    ExecutionFailure(FailedTransactionMetadata),
84}
85
86pub struct SvmAccessContext<T> {
87    pub slot: Slot,
88    pub latest_epoch_info: EpochInfo,
89    pub latest_blockhash: Hash,
90    pub inner: T,
91}
92
93impl<T> SvmAccessContext<T> {
94    pub fn new(slot: Slot, latest_epoch_info: EpochInfo, latest_blockhash: Hash, inner: T) -> Self {
95        Self {
96            slot,
97            latest_blockhash,
98            latest_epoch_info,
99            inner,
100        }
101    }
102
103    pub fn inner(&self) -> &T {
104        &self.inner
105    }
106
107    pub fn with_new_value<N>(self, inner: N) -> SvmAccessContext<N> {
108        SvmAccessContext {
109            slot: self.slot,
110            latest_blockhash: self.latest_blockhash,
111            latest_epoch_info: self.latest_epoch_info,
112            inner,
113        }
114    }
115}
116
117pub type SurfpoolContextualizedResult<T> = SurfpoolResult<SvmAccessContext<T>>;
118
119/// Determines the loading priority for accounts during snapshot loading. Accounts with lower
120/// returned values are loaded first. This function prioritizes non-executable accounts owned
121/// by the upgradeable loader (program data accounts) first, then all other accounts, and finally
122/// executable BPF-loader-upgradeable program accounts. This ordering ensures that program data
123/// accounts are available before any dependent program accounts are loaded, which would otherwise
124/// cause an error in LiteSVM.
125fn snapshot_load_priority(account: &Account) -> u8 {
126    if account.owner != solana_sdk_ids::bpf_loader_upgradeable::id() {
127        return 1;
128    }
129
130    let programdata_metadata_size = UpgradeableLoaderState::size_of_programdata_metadata();
131    if !account.executable
132        && account.data.len() >= programdata_metadata_size
133        && matches!(
134            bincode::deserialize::<UpgradeableLoaderState>(
135                &account.data[..programdata_metadata_size]
136            ),
137            Ok(UpgradeableLoaderState::ProgramData { .. })
138        )
139    {
140        return 0;
141    }
142
143    if account.executable
144        && matches!(
145            bincode::deserialize::<UpgradeableLoaderState>(&account.data),
146            Ok(UpgradeableLoaderState::Program { .. })
147        )
148    {
149        return 2;
150    }
151
152    1
153}
154
155/// Helper function to apply an override to a JSON value using dot notation path
156///
157/// # Arguments
158/// * `json` - The JSON value to modify
159/// * `path` - Dot-separated path to the field (e.g., "price_message.price")
160/// * `value` - The new value to set
161///
162/// # Returns
163/// Result indicating success or error
164pub struct SurfnetSvmLocker(pub Arc<RwLock<SurfnetSvm>>);
165
166impl Clone for SurfnetSvmLocker {
167    fn clone(&self) -> Self {
168        Self(self.0.clone())
169    }
170}
171
172/// Functions for reading and writing to the underlying SurfnetSvm instance
173impl SurfnetSvmLocker {
174    /// Explicitly shutdown the SVM, performing cleanup like WAL checkpoint for SQLite.
175    /// This should be called before the application exits to ensure data is persisted.
176    pub fn shutdown(&self) {
177        let read_lock = self.0.clone();
178        tokio::task::block_in_place(move || {
179            let read_guard = read_lock.blocking_read();
180            read_guard.shutdown();
181        });
182    }
183
184    /// Executes a read-only operation on the underlying `SurfnetSvm` by acquiring a blocking read lock.
185    /// Accepts a closure that receives a shared reference to `SurfnetSvm` and returns a value.
186    ///
187    /// # Returns
188    /// The result produced by the closure.
189    pub fn with_svm_reader<T, F>(&self, reader: F) -> T
190    where
191        F: FnOnce(&SurfnetSvm) -> T + Send + Sync,
192    {
193        let read_lock = self.0.clone();
194        tokio::task::block_in_place(move || {
195            let read_guard = read_lock.blocking_read();
196            reader(&read_guard)
197        })
198    }
199
200    /// Executes a read-only operation and wraps the result in `SvmAccessContext`, capturing
201    /// slot, epoch info, and blockhash along with the closure's result.
202    fn with_contextualized_svm_reader<T, F>(&self, reader: F) -> SvmAccessContext<T>
203    where
204        F: Fn(&SurfnetSvm) -> T + Send + Sync,
205        T: Send + 'static,
206    {
207        let read_lock = self.0.clone();
208        tokio::task::block_in_place(move || {
209            let read_guard = read_lock.blocking_read();
210            let res = reader(&read_guard);
211
212            SvmAccessContext::new(
213                read_guard.get_latest_absolute_slot(),
214                read_guard.latest_epoch_info(),
215                read_guard.latest_blockhash(),
216                res,
217            )
218        })
219    }
220
221    /// Executes a write operation on the underlying `SurfnetSvm` by acquiring a blocking write lock.
222    /// Accepts a closure that receives a mutable reference to `SurfnetSvm` and returns a value.
223    ///
224    /// # Returns
225    /// The result produced by the closure.
226    pub fn with_svm_writer<T, F>(&self, writer: F) -> T
227    where
228        F: FnOnce(&mut SurfnetSvm) -> T + Send + Sync,
229        T: Send + 'static,
230    {
231        let write_lock = self.0.clone();
232        tokio::task::block_in_place(move || {
233            let mut write_guard = write_lock.blocking_write();
234            writer(&mut write_guard)
235        })
236    }
237}
238
239/// Functions for creating and initializing the underlying SurfnetSvm instance
240impl SurfnetSvmLocker {
241    /// Constructs a new `SurfnetSvmLocker` wrapping the given `SurfnetSvm` instance.
242    pub fn new(svm: SurfnetSvm) -> Self {
243        Self(Arc::new(RwLock::new(svm)))
244    }
245
246    /// Initializes the locked `SurfnetSvm` with remote-derived startup state when available.
247    pub async fn initialize(&self, remote_ctx: &Option<SurfnetRemoteClient>) -> SurfpoolResult<()> {
248        let Some(remote_client) = remote_ctx else {
249            return Ok(());
250        };
251
252        let (mut epoch_info, epoch_schedule) = {
253            let epoch_info = remote_client.get_epoch_info().await?;
254            let epoch_schedule = remote_client.get_epoch_schedule().await?;
255            (epoch_info, epoch_schedule)
256        };
257        epoch_info.transaction_count = None;
258
259        self.with_svm_writer(move |svm_writer| {
260            svm_writer.initialize(epoch_info, epoch_schedule);
261        });
262        Ok(())
263    }
264}
265
266/// Functions for getting accounts from the underlying SurfnetSvm instance or remote client
267impl SurfnetSvmLocker {
268    /// Filters the downloaded account result to remove accounts owned by offline owners.
269    fn filter_downloaded_account_result(
270        requested_pubkey: &Pubkey,
271        result: GetAccountResult,
272        offline_owners: &[Pubkey],
273    ) -> GetAccountResult {
274        match result {
275            GetAccountResult::FoundAccount(_, account, _)
276            | GetAccountResult::FoundProgramAccount((_, account), _)
277            | GetAccountResult::FoundTokenAccount((_, account), _)
278                if offline_owners.contains(&account.owner) =>
279            {
280                GetAccountResult::None(*requested_pubkey)
281            }
282            other => other,
283        }
284    }
285
286    /// Retrieves a local account from the SVM cache, returning a contextualized result.
287    pub fn get_account_local(&self, pubkey: &Pubkey) -> SvmAccessContext<GetAccountResult> {
288        self.with_contextualized_svm_reader(|svm_reader| {
289            return svm_reader.inner.get_account_result(pubkey).unwrap();
290        })
291    }
292
293    /// Attempts local retrieval, then fetches from remote if missing, returning a contextualized result.
294    ///
295    /// Does not fetch from remote if the account has been explicitly blocked from remote downloads.
296    pub async fn get_account_local_then_remote(
297        &self,
298        client: &SurfnetRemoteClient,
299        pubkey: &Pubkey,
300        commitment_config: CommitmentConfig,
301    ) -> SurfpoolContextualizedResult<GetAccountResult> {
302        let result = self.get_account_local(pubkey);
303
304        if result.inner.is_none() {
305            let is_offline = self.is_account_offline(pubkey);
306
307            if !is_offline {
308                let offline_owners = self.get_offline_account_owners();
309                let remote_account = client.get_account(pubkey, commitment_config).await?;
310                Ok(
311                    result.with_new_value(Self::filter_downloaded_account_result(
312                        pubkey,
313                        remote_account,
314                        &offline_owners,
315                    )),
316                )
317            } else {
318                Ok(result)
319            }
320        } else {
321            Ok(result)
322        }
323    }
324
325    /// Retrieves an account, using local or remote based on context, applying a default factory if provided.
326    pub async fn get_account(
327        &self,
328        remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>,
329        pubkey: &Pubkey,
330        factory: Option<AccountFactory>,
331    ) -> SurfpoolContextualizedResult<GetAccountResult> {
332        let result = if let Some((remote_client, commitment_config)) = remote_ctx {
333            self.get_account_local_then_remote(remote_client, pubkey, *commitment_config)
334                .await?
335        } else {
336            self.get_account_local(pubkey)
337        };
338
339        match (&result.inner, factory) {
340            (&GetAccountResult::None(_), Some(factory)) => {
341                let default = factory(self.clone());
342                Ok(result.with_new_value(default))
343            }
344            _ => Ok(result),
345        }
346    }
347    /// Retrieves multiple accounts from local cache, returning a contextualized result.
348    pub fn get_multiple_accounts_local(
349        &self,
350        pubkeys: &[Pubkey],
351    ) -> SvmAccessContext<Vec<GetAccountResult>> {
352        self.with_contextualized_svm_reader(|svm_reader| {
353            let mut accounts = vec![];
354
355            for pubkey in pubkeys {
356                let result = svm_reader.inner.get_account_result(pubkey).unwrap();
357                if result.is_none() {};
358                accounts.push(result);
359            }
360            accounts
361        })
362    }
363
364    /// Retrieves multiple accounts from local storage, with remote fallback for missing accounts.
365    ///
366    /// Returns accounts in the same order as the input `pubkeys` array. Accounts found locally
367    /// are returned as-is; accounts not found locally are fetched from the remote RPC client.
368    /// Accounts that have been marked offline are not fetched from remote.
369    pub async fn get_multiple_accounts_with_remote_fallback(
370        &self,
371        client: &SurfnetRemoteClient,
372        pubkeys: &[Pubkey],
373        commitment_config: CommitmentConfig,
374    ) -> SurfpoolContextualizedResult<Vec<GetAccountResult>> {
375        let SvmAccessContext {
376            slot,
377            latest_epoch_info,
378            latest_blockhash,
379            inner: local_results,
380        } = self.get_multiple_accounts_local(pubkeys);
381
382        // Collect missing pubkeys that are not offline (local_results is already in correct order from pubkeys).
383        let mut missing_accounts = Vec::new();
384        for result in &local_results {
385            let GetAccountResult::None(pubkey) = result else {
386                continue;
387            };
388            if self.is_account_offline(pubkey) {
389                continue;
390            }
391            missing_accounts.push(*pubkey);
392        }
393
394        if missing_accounts.is_empty() {
395            // All accounts found locally, already in correct order
396            return Ok(SvmAccessContext::new(
397                slot,
398                latest_epoch_info,
399                latest_blockhash,
400                local_results,
401            ));
402        }
403        debug!(
404            "Missing accounts will be fetched: {}",
405            missing_accounts.iter().join(", ")
406        );
407        // Fetch missing accounts from remote
408        let remote_results = client
409            .get_multiple_accounts(&missing_accounts, commitment_config)
410            .await?;
411
412        // Build map of pubkey -> remote result for O(1) lookup
413        let offline_owners = self.get_offline_account_owners();
414        let remote_map: HashMap<Pubkey, GetAccountResult> = missing_accounts
415            .iter()
416            .copied()
417            .zip(remote_results.into_iter())
418            .map(|(requested_pubkey, result)| {
419                (
420                    requested_pubkey,
421                    Self::filter_downloaded_account_result(
422                        &requested_pubkey,
423                        result,
424                        &offline_owners,
425                    ),
426                )
427            })
428            .collect();
429
430        // Replace None entries with remote results while preserving order
431        // We iterate through original pubkeys array to ensure order is explicit
432        let combined_results: Vec<GetAccountResult> = pubkeys
433            .iter()
434            .zip(local_results.into_iter())
435            .map(|(pubkey, local_result)| {
436                match local_result {
437                    GetAccountResult::None(_) => remote_map
438                        .get(pubkey)
439                        .cloned()
440                        .unwrap_or(GetAccountResult::None(*pubkey)),
441                    found => {
442                        debug!("Keeping local account: {}", pubkey);
443                        found
444                    } // Keep found accounts (no clone, just move)
445                }
446            })
447            .collect();
448
449        Ok(SvmAccessContext::new(
450            slot,
451            latest_epoch_info,
452            latest_blockhash,
453            combined_results,
454        ))
455    }
456
457    /// Retrieves multiple accounts, using local or remote context and applying factory defaults if provided.
458    pub async fn get_multiple_accounts(
459        &self,
460        remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>,
461        pubkeys: &[Pubkey],
462        factory: Option<AccountFactory>,
463    ) -> SurfpoolContextualizedResult<Vec<GetAccountResult>> {
464        let results = if let Some((remote_client, commitment_config)) = remote_ctx {
465            self.get_multiple_accounts_with_remote_fallback(
466                remote_client,
467                pubkeys,
468                *commitment_config,
469            )
470            .await?
471        } else {
472            self.get_multiple_accounts_local(pubkeys)
473        };
474
475        let mut combined = Vec::with_capacity(results.inner.len());
476        for result in results.inner.clone() {
477            match (&result, &factory) {
478                (&GetAccountResult::None(_), Some(factory)) => {
479                    let default = factory(self.clone());
480                    combined.push(default);
481                }
482                _ => combined.push(result),
483            }
484        }
485        Ok(results.with_new_value(combined))
486    }
487
488    /// Loads accounts from a snapshot into the SVM.
489    ///
490    /// This method should be called before geyser plugins start to ensure they receive
491    /// the account updates with `is_startup=true`.
492    ///
493    /// # Arguments
494    /// * `snapshot` - A map of pubkey strings to optional account snapshots.
495    ///   - If the value is Some(AccountSnapshot), the account is loaded directly.
496    ///   - If the value is None, the account is fetched from the remote RPC (if available).
497    /// * `remote_client` - Optional remote RPC client to fetch None accounts.
498    /// * `commitment_config` - Commitment level for remote RPC calls.
499    ///
500    /// # Returns
501    /// The number of accounts successfully loaded.
502    pub async fn load_snapshot(
503        &self,
504        snapshot: &BTreeMap<String, Option<AccountSnapshot>>,
505        remote_client: Option<&SurfnetRemoteClient>,
506        commitment_config: CommitmentConfig,
507    ) -> SurfpoolResult<usize> {
508        use std::str::FromStr;
509
510        use base64::{Engine, prelude::BASE64_STANDARD};
511
512        let mut loaded_count = 0;
513
514        // Separate accounts into those with data and those needing remote fetch
515        let mut accounts_to_load: Vec<(Pubkey, Account)> = Vec::new();
516        let mut pubkeys_to_fetch: Vec<Pubkey> = Vec::new();
517
518        for (pubkey_str, account_snapshot_opt) in snapshot.iter() {
519            let pubkey = match Pubkey::from_str(pubkey_str) {
520                Ok(pk) => pk,
521                Err(e) => {
522                    self.with_svm_reader(|svm| {
523                        let _ = svm.simnet_events_tx.send(SimnetEvent::warn(format!(
524                            "Skipping invalid pubkey '{}' in snapshot: {}",
525                            pubkey_str, e
526                        )));
527                    });
528                    continue;
529                }
530            };
531
532            match account_snapshot_opt {
533                Some(account_snapshot) => {
534                    // Decode base64 data
535                    let data = match BASE64_STANDARD.decode(&account_snapshot.data) {
536                        Ok(d) => d,
537                        Err(e) => {
538                            self.with_svm_reader(|svm| {
539                                let _ = svm.simnet_events_tx.send(SimnetEvent::warn(format!(
540                                    "Skipping account '{}': failed to decode base64 data: {}",
541                                    pubkey_str, e
542                                )));
543                            });
544                            continue;
545                        }
546                    };
547
548                    // Parse owner pubkey
549                    let owner = match Pubkey::from_str(&account_snapshot.owner) {
550                        Ok(pk) => pk,
551                        Err(e) => {
552                            self.with_svm_reader(|svm| {
553                                let _ = svm.simnet_events_tx.send(SimnetEvent::warn(format!(
554                                    "Skipping account '{}': invalid owner pubkey: {}",
555                                    pubkey_str, e
556                                )));
557                            });
558                            continue;
559                        }
560                    };
561
562                    // Create the account
563                    let account = Account {
564                        lamports: account_snapshot.lamports,
565                        data,
566                        owner,
567                        executable: account_snapshot.executable,
568                        rent_epoch: account_snapshot.rent_epoch,
569                    };
570
571                    accounts_to_load.push((pubkey, account));
572                }
573                None => {
574                    // Queue for remote fetch if client is available
575                    if remote_client.is_some() {
576                        pubkeys_to_fetch.push(pubkey);
577                    }
578                }
579            }
580        }
581
582        // Fetch None accounts from remote RPC if client is available
583        if let Some(client) = remote_client {
584            if !pubkeys_to_fetch.is_empty() {
585                self.with_svm_reader(|svm| {
586                    let _ = svm.simnet_events_tx.send(SimnetEvent::info(format!(
587                        "Fetching {} accounts from remote RPC for snapshot",
588                        pubkeys_to_fetch.len()
589                    )));
590                });
591
592                match client
593                    .get_multiple_accounts(&pubkeys_to_fetch, commitment_config)
594                    .await
595                {
596                    Ok(remote_results) => {
597                        for (pubkey, result) in pubkeys_to_fetch.iter().zip(remote_results) {
598                            match result {
599                                GetAccountResult::FoundAccount(_, account, _) => {
600                                    accounts_to_load.push((*pubkey, account));
601                                }
602                                GetAccountResult::FoundProgramAccount(
603                                    (program_pubkey, program_account),
604                                    (data_pubkey, data_account_opt),
605                                ) => {
606                                    accounts_to_load.push((program_pubkey, program_account));
607                                    if let Some(data_account) = data_account_opt {
608                                        accounts_to_load.push((data_pubkey, data_account));
609                                    }
610                                }
611                                GetAccountResult::FoundTokenAccount(
612                                    (token_pubkey, token_account),
613                                    (mint_pubkey, mint_account_opt),
614                                ) => {
615                                    accounts_to_load.push((token_pubkey, token_account));
616                                    if let Some(mint_account) = mint_account_opt {
617                                        accounts_to_load.push((mint_pubkey, mint_account));
618                                    }
619                                }
620                                GetAccountResult::None(_) => {
621                                    // Account not found on remote, skip
622                                }
623                            }
624                        }
625                    }
626                    Err(e) => {
627                        self.with_svm_reader(|svm| {
628                            let _ = svm.simnet_events_tx.send(SimnetEvent::warn(format!(
629                                "Failed to fetch some accounts from remote: {}",
630                                e
631                            )));
632                        });
633                    }
634                }
635            }
636        }
637
638        accounts_to_load.sort_by_key(|(_, account)| snapshot_load_priority(account));
639
640        // Load all accounts into the SVM
641        self.with_svm_writer(|svm| {
642            let slot = svm.get_latest_absolute_slot();
643
644            for (pubkey, account) in accounts_to_load {
645                if let Err(e) = svm.set_account(&pubkey, account.clone()) {
646                    let _ = svm.simnet_events_tx.send(SimnetEvent::warn(format!(
647                        "Failed to set account '{}': {}",
648                        pubkey, e
649                    )));
650                    continue;
651                }
652
653                // Send startup account update to geyser
654                let write_version = svm.increment_write_version();
655                let _ = svm.geyser_events_tx.send(GeyserEvent::StartupAccountUpdate(
656                    GeyserAccountUpdate::startup_update(pubkey, account, slot, write_version),
657                ));
658
659                loaded_count += 1;
660            }
661        });
662
663        Ok(loaded_count)
664    }
665
666    /// Retrieves largest accounts from local cache, returning a contextualized result.
667    pub fn get_largest_accounts_local(
668        &self,
669        config: RpcLargestAccountsConfig,
670    ) -> SurfpoolContextualizedResult<Vec<RpcAccountBalance>> {
671        let res: Vec<RpcAccountBalance> = self.with_svm_reader(|svm_reader| {
672            let non_circulating_accounts: Vec<_> = svm_reader
673                .non_circulating_accounts
674                .iter()
675                .flat_map(|acct| verify_pubkey(acct))
676                .collect();
677
678            let ordered_accounts = svm_reader
679                .get_all_accounts()?
680                .into_iter()
681                .sorted_by(|a, b| b.1.lamports().cmp(&a.1.lamports()))
682                .collect::<Vec<_>>();
683            let ordered_filtered_accounts = match config.filter {
684                Some(RpcLargestAccountsFilter::NonCirculating) => ordered_accounts
685                    .into_iter()
686                    .filter(|(pubkey, _)| non_circulating_accounts.contains(pubkey))
687                    .collect::<Vec<_>>(),
688                Some(RpcLargestAccountsFilter::Circulating) => ordered_accounts
689                    .into_iter()
690                    .filter(|(pubkey, _)| !non_circulating_accounts.contains(pubkey))
691                    .collect::<Vec<_>>(),
692                None => ordered_accounts,
693            };
694
695            Ok::<Vec<RpcAccountBalance>, SurfpoolError>(
696                ordered_filtered_accounts
697                    .iter()
698                    .take(20)
699                    .map(|(pubkey, account)| RpcAccountBalance {
700                        address: pubkey.to_string(),
701                        lamports: account.lamports(),
702                    })
703                    .collect(),
704            )
705        })?;
706        Ok(self.with_contextualized_svm_reader(|_| res.to_owned()))
707    }
708
709    pub async fn get_largest_accounts_local_then_remote(
710        &self,
711        client: &SurfnetRemoteClient,
712        config: RpcLargestAccountsConfig,
713        commitment_config: CommitmentConfig,
714    ) -> SurfpoolContextualizedResult<Vec<RpcAccountBalance>> {
715        // get all non-circulating and circulating pubkeys from the remote client first,
716        // and insert them locally
717        {
718            let remote_non_circulating_pubkeys_result = client
719                .get_largest_accounts(Some(RpcLargestAccountsConfig {
720                    filter: Some(RpcLargestAccountsFilter::NonCirculating),
721                    ..config
722                }))
723                .await?;
724
725            let (mut remote_non_circulating_pubkeys, mut remote_circulating_pubkeys) =
726                match remote_non_circulating_pubkeys_result {
727                    RemoteRpcResult::Ok(non_circulating_accounts) => {
728                        let remote_circulating_pubkeys_result = client
729                            .get_largest_accounts(Some(RpcLargestAccountsConfig {
730                                filter: Some(RpcLargestAccountsFilter::Circulating),
731                                ..config
732                            }))
733                            .await?;
734
735                        let remote_circulating_pubkeys = match remote_circulating_pubkeys_result {
736                            RemoteRpcResult::Ok(circulating_accounts) => circulating_accounts,
737                            RemoteRpcResult::MethodNotSupported => {
738                                unreachable!()
739                            }
740                        };
741                        (
742                            non_circulating_accounts
743                                .iter()
744                                .map(|account_balance| verify_pubkey(&account_balance.address))
745                                .collect::<SurfpoolResult<Vec<_>>>()?,
746                            remote_circulating_pubkeys
747                                .iter()
748                                .map(|account_balance| verify_pubkey(&account_balance.address))
749                                .collect::<SurfpoolResult<Vec<_>>>()?,
750                        )
751                    }
752                    RemoteRpcResult::MethodNotSupported => {
753                        let tx = self.simnet_events_tx();
754                        let _ = tx.send(SimnetEvent::warn("The `getLargestAccounts` method was sent to the remote RPC, but this method isn't supported by your RPC provider. Only local accounts will be returned."));
755                        (vec![], vec![])
756                    }
757                };
758
759            let mut combined = Vec::with_capacity(
760                remote_non_circulating_pubkeys.len() + remote_circulating_pubkeys.len(),
761            );
762            combined.append(&mut remote_non_circulating_pubkeys);
763            combined.append(&mut remote_circulating_pubkeys);
764
765            let get_account_results = self
766                .get_multiple_accounts_with_remote_fallback(client, &combined, commitment_config)
767                .await?
768                .inner;
769
770            self.write_multiple_account_updates(&get_account_results);
771        }
772
773        // now that our local cache is aware of all large remote accounts, we can get the largest accounts locally
774        // and filter according to the config
775        self.get_largest_accounts_local(config)
776    }
777
778    pub async fn get_largest_accounts(
779        &self,
780        remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>,
781        config: RpcLargestAccountsConfig,
782    ) -> SurfpoolContextualizedResult<Vec<RpcAccountBalance>> {
783        if let Some((remote_client, commitment_config)) = remote_ctx {
784            self.get_largest_accounts_local_then_remote(remote_client, config, *commitment_config)
785                .await
786        } else {
787            self.get_largest_accounts_local(config)
788        }
789    }
790
791    pub fn account_to_rpc_keyed_account<T: ReadableAccount + Send + Sync>(
792        &self,
793        pubkey: &Pubkey,
794        account: &T,
795        config: &RpcAccountInfoConfig,
796        token_mint: Option<Pubkey>,
797    ) -> RpcKeyedAccount {
798        self.with_svm_reader(|svm_reader| {
799            svm_reader.account_to_rpc_keyed_account(pubkey, account, config, token_mint)
800        })
801    }
802}
803
804/// Builds the `getSignaturesForAddress` config for the remote leg of a local-then-remote
805/// query, given whether the caller's `before` / `until` reference locally-stored signatures
806/// and how many slots remain under the caller's `limit` after local results were collected.
807///
808/// Surfpool's local transactions are treated as strictly newer than anything on the upstream
809/// chain, so a local-only pagination boundary must be rewritten before being forwarded:
810///   - `before` local-only  => drop the boundary; remote signatures are all older anyway.
811///   - `until` local-only   => every remote signature would be excluded; return `None` to
812///                              skip the remote call entirely.
813///   - neither local-only   => forward the caller's boundaries unchanged.
814///
815/// The returned config always pins `limit` to `remaining_limit` so the remote can't push the
816/// combined result past the caller's requested cap.
817fn signatures_for_address_remote_config(
818    config: Option<&RpcSignaturesForAddressConfig>,
819    before_is_local: bool,
820    until_is_local: bool,
821    remaining_limit: usize,
822) -> Option<RpcSignaturesForAddressConfig> {
823    if until_is_local {
824        return None;
825    }
826    let base = config.cloned().unwrap_or_default();
827    Some(RpcSignaturesForAddressConfig {
828        before: if before_is_local { None } else { base.before },
829        until: base.until,
830        limit: Some(remaining_limit),
831        commitment: base.commitment,
832        min_context_slot: base.min_context_slot,
833    })
834}
835
836/// Get signatures for Addresses
837impl SurfnetSvmLocker {
838    /// Returns local `getSignaturesForAddress` results in the same newest-first order expected by
839    /// the Solana RPC.
840    ///
841    /// The implementation has to do more than filter by slot:
842    /// - transactions are ordered by descending slot
843    /// - transactions within the same slot are ordered by their execution order in the block
844    /// - `before` and `until` are pagination boundaries in that final ordered stream
845    ///
846    /// To preserve those semantics, we first collect matching transactions, reconstruct their
847    /// intra-slot ordering from block headers, sort the full result stream, and only then apply
848    /// the `before` / `until` window followed by `limit`.
849    pub fn get_signatures_for_address_local(
850        &self,
851        pubkey: &Pubkey,
852        config: Option<&RpcSignaturesForAddressConfig>,
853    ) -> SvmAccessContext<Vec<RpcConfirmedTransactionStatusWithSignature>> {
854        let before = config.and_then(|c| c.before.as_ref());
855        let until = config.and_then(|c| c.until.as_ref());
856        let limit = config.and_then(|c| c.limit).unwrap_or(1000);
857        let min_context_slot = config.and_then(|c| c.min_context_slot).unwrap_or_default();
858
859        self.with_contextualized_svm_reader(move |svm_reader| {
860            let current_slot = svm_reader.get_latest_absolute_slot();
861
862            let sigs: Vec<_> = svm_reader
863                .transactions
864                .into_iter()
865                .map(|iter| {
866                    iter.filter_map(|(sig, status)| {
867                        let (
868                            TransactionWithStatusMeta {
869                                slot,
870                                transaction,
871                                meta,
872                            },
873                            _,
874                        ) = status
875                            .as_processed()
876                            .expect("expected processed transaction");
877
878                        if slot < min_context_slot {
879                            return None;
880                        }
881
882                        // Check if the pubkey is a signer
883
884                        if !transaction.message.static_account_keys().contains(pubkey) {
885                            return None;
886                        }
887
888                        // Determine confirmation status
889                        let confirmation_status = match current_slot {
890                            cs if cs == slot => SolanaTransactionConfirmationStatus::Processed,
891                            cs if cs < slot + FINALIZATION_SLOT_THRESHOLD => {
892                                SolanaTransactionConfirmationStatus::Confirmed
893                            }
894                            _ => SolanaTransactionConfirmationStatus::Finalized,
895                        };
896
897                        // Reconstruct the memo summary the same way a full Agave validator
898                        // does, reusing its canonical extractor. `account_keys()` on the
899                        // wrapper folds in `meta.loaded_addresses`, so memos invoked via an
900                        // address lookup table are handled too.
901                        let tx_with_meta = VersionedTransactionWithStatusMeta { transaction, meta };
902                        let memo = extract_and_fmt_memos(&tx_with_meta);
903                        let err = match &tx_with_meta.meta.status {
904                            Ok(_) => None,
905                            Err(e) => Some(e.clone().into()),
906                        };
907
908                        // Synthesize the block time from the slot, matching `getBlockTime`
909                        // (and real Agave). `calculate_block_time_for_slot` returns
910                        // milliseconds, so divide by 1000 to get `UnixTimestamp` seconds.
911                        let block_time =
912                            Some((svm_reader.calculate_block_time_for_slot(slot) / 1_000) as i64);
913
914                        Some(RpcConfirmedTransactionStatusWithSignature {
915                            err,
916                            slot,
917                            memo,
918                            block_time,
919                            confirmation_status: Some(confirmation_status),
920                            signature: sig,
921                            transaction_index: None,
922                        })
923                    })
924                    .collect()
925                })
926                .unwrap_or_default();
927
928            // `getSignaturesForAddress` is ordered newest-first, but transactions that share a
929            // slot also need to preserve their execution order within that block.
930            let unique_slots: HashSet<u64> = sigs.iter().map(|s| s.slot).collect();
931            let mut sig_position: HashMap<String, usize> = HashMap::new();
932            for slot in unique_slots {
933                if let Ok(Some(block_header)) = svm_reader.blocks.get(&slot) {
934                    for (idx, block_sig) in block_header.signatures.iter().enumerate() {
935                        sig_position.insert(block_sig.to_string(), idx);
936                    }
937                }
938            }
939
940            let sigs: Vec<_> = sigs
941                .into_iter()
942                // Order from most recent to least recent so pagination boundaries
943                // can be applied against the exact transaction sequence.
944                .sorted_by(|a, b| {
945                    b.slot.cmp(&a.slot).then_with(|| {
946                        let a_pos = sig_position.get(&a.signature).unwrap_or(&usize::MAX);
947                        let b_pos = sig_position.get(&b.signature).unwrap_or(&usize::MAX);
948                        b_pos.cmp(&a_pos)
949                    })
950                })
951                .collect();
952
953            let window = {
954                // `before` and `until` are boundaries in the final ordered result stream, not
955                // just slot filters. We compute a [start..end) index range after sorting so
956                // same-slot pagination behaves correctly and `until` stays exclusive.
957                let start = match before {
958                    // `before` is exclusive, so we start one item after the boundary when it
959                    // exists. If it does not exist locally, the local window is empty.
960                    Some(before) => match sigs.iter().position(|sig| sig.signature.eq(before)) {
961                        Some(idx) => idx + 1,
962                        None => sigs.len(),
963                    },
964                    None => 0,
965                };
966
967                let end = match until {
968                    // `until` is also exclusive, so the boundary itself is not included. We only
969                    // search within `sigs[start..]` so the end boundary is resolved relative to the
970                    // already-trimmed start of the window. If it is missing, we keep the full tail.
971                    Some(until) => {
972                        match sigs[start..].iter().position(|sig| sig.signature.eq(until)) {
973                            Some(offset) => start + offset,
974                            None => sigs.len(),
975                        }
976                    }
977                    None => sigs.len(),
978                };
979                start..end
980            };
981
982            // Apply the pagination window first, then enforce the RPC limit on that slice.
983            sigs[window].iter().take(limit).cloned().collect()
984        })
985    }
986
987    pub async fn get_signatures_for_address_local_then_remote(
988        &self,
989        client: &SurfnetRemoteClient,
990        pubkey: &Pubkey,
991        config: Option<&RpcSignaturesForAddressConfig>,
992    ) -> SurfpoolContextualizedResult<Vec<RpcConfirmedTransactionStatusWithSignature>> {
993        let results = self.get_signatures_for_address_local(pubkey, config);
994        let limit = config.and_then(|c| c.limit).unwrap_or(1000);
995
996        let SvmAccessContext {
997            slot,
998            latest_blockhash,
999            latest_epoch_info,
1000            inner: mut combined_results,
1001        } = results;
1002        if combined_results.len() < limit {
1003            let (before_is_local, until_is_local) =
1004                self.with_svm_reader(|svm_reader| -> StorageResult<(bool, bool)> {
1005                    let is_local = |sig: &String| -> StorageResult<bool> {
1006                        Ok(svm_reader.transactions.get(sig)?.is_some())
1007                    };
1008                    let before_local = match config.and_then(|c| c.before.as_ref()) {
1009                        Some(sig) => is_local(sig)?,
1010                        None => false,
1011                    };
1012                    let until_local = match config.and_then(|c| c.until.as_ref()) {
1013                        Some(sig) => is_local(sig)?,
1014                        None => false,
1015                    };
1016                    Ok((before_local, until_local))
1017                })?;
1018
1019            let remaining_limit = limit - combined_results.len();
1020            if let Some(remote_config) = signatures_for_address_remote_config(
1021                config,
1022                before_is_local,
1023                until_is_local,
1024                remaining_limit,
1025            ) {
1026                let mut remote_results = client
1027                    .get_signatures_for_address(pubkey, Some(&remote_config))
1028                    .await?;
1029                combined_results.append(&mut remote_results);
1030                // Belt-and-suspenders: enforce the caller's cap even if the remote returned
1031                // more than the requested slice.
1032                combined_results.truncate(limit);
1033            }
1034        }
1035
1036        Ok(SvmAccessContext::new(
1037            slot,
1038            latest_epoch_info,
1039            latest_blockhash,
1040            combined_results,
1041        ))
1042    }
1043
1044    pub async fn get_signatures_for_address(
1045        &self,
1046        remote_ctx: &Option<(SurfnetRemoteClient, ())>,
1047        pubkey: &Pubkey,
1048        config: Option<&RpcSignaturesForAddressConfig>,
1049    ) -> SurfpoolContextualizedResult<Vec<RpcConfirmedTransactionStatusWithSignature>> {
1050        let results = if let Some((remote_client, _)) = remote_ctx {
1051            self.get_signatures_for_address_local_then_remote(remote_client, pubkey, config)
1052                .await?
1053        } else {
1054            self.get_signatures_for_address_local(pubkey, config)
1055        };
1056
1057        Ok(results)
1058    }
1059}
1060
1061/// Functions for getting transactions from the underlying SurfnetSvm instance or remote client
1062impl SurfnetSvmLocker {
1063    /// Retrieves a transaction by signature, using local or remote based on context.
1064    pub async fn get_transaction(
1065        &self,
1066        remote_ctx: &Option<SurfnetRemoteClient>,
1067        signature: &Signature,
1068        config: RpcTransactionConfig,
1069    ) -> SurfpoolResult<GetTransactionResult> {
1070        if let Some(remote_client) = remote_ctx {
1071            self.get_transaction_local_then_remote(remote_client, signature, config)
1072                .await
1073        } else {
1074            self.get_transaction_local(signature, &config)
1075        }
1076    }
1077
1078    /// Stores a bundle's signatures under the given bundle ID.
1079    pub fn store_bundle(&self, bundle_id: String, signatures: Vec<String>) -> SurfpoolResult<()> {
1080        self.with_svm_writer(|svm_writer| svm_writer.jito_bundles.store(bundle_id, signatures))?;
1081        Ok(())
1082    }
1083
1084    /// Retrieves the list of transaction signatures for a previously stored bundle.
1085    ///
1086    /// Returns `None` when there is no local entry for `bundle_id` (or the backing store read fails).
1087    /// This is not an "invalid id" signal: callers such as Jito `getBundleStatuses` treat `None` as
1088    /// “no data” and return a null RPC result rather than an error.
1089    pub fn get_bundle(&self, bundle_id: &str) -> Option<Vec<String>> {
1090        self.with_svm_reader(|svm_reader| {
1091            svm_reader
1092                .jito_bundles
1093                .get(&bundle_id.to_string())
1094                .unwrap_or_default()
1095        })
1096    }
1097
1098    /// Retrieves a transaction from local cache, returning a contextualized result.
1099    pub fn get_transaction_local(
1100        &self,
1101        signature: &Signature,
1102        config: &RpcTransactionConfig,
1103    ) -> SurfpoolResult<GetTransactionResult> {
1104        self.with_svm_reader(|svm_reader| {
1105            let latest_absolute_slot = svm_reader.get_latest_absolute_slot();
1106
1107            let Some(entry) = svm_reader.transactions.get(&signature.to_string())? else {
1108                return Ok(GetTransactionResult::None(*signature));
1109            };
1110
1111            let (transaction_with_status_meta, _) = entry.expect_processed();
1112            let slot = transaction_with_status_meta.slot;
1113            let block_time = svm_reader
1114                .blocks
1115                .get(&slot)?
1116                .map(|b| (b.block_time / 1_000) as UnixTimestamp)
1117                .unwrap_or(0);
1118            let encoded = transaction_with_status_meta.encode(
1119                config.encoding.unwrap_or(UiTransactionEncoding::JsonParsed),
1120                config.max_supported_transaction_version,
1121                true,
1122            )?;
1123            Ok(GetTransactionResult::found_transaction(
1124                *signature,
1125                EncodedConfirmedTransactionWithStatusMeta {
1126                    slot,
1127                    transaction: encoded,
1128                    block_time: Some(block_time),
1129                    transaction_index: None,
1130                },
1131                latest_absolute_slot,
1132            ))
1133        })
1134    }
1135
1136    /// Retrieves a transaction locally then from remote if missing, returning a contextualized result.
1137    pub async fn get_transaction_local_then_remote(
1138        &self,
1139        client: &SurfnetRemoteClient,
1140        signature: &Signature,
1141        config: RpcTransactionConfig,
1142    ) -> SurfpoolResult<GetTransactionResult> {
1143        let local_result = self.get_transaction_local(signature, &config)?;
1144        let latest_absolute_slot = self.get_latest_absolute_slot();
1145        if local_result.is_none() {
1146            Ok(client
1147                .get_transaction(*signature, config, latest_absolute_slot)
1148                .await)
1149        } else {
1150            Ok(local_result)
1151        }
1152    }
1153}
1154
1155/// Functions for simulating and processing transactions in the underlying SurfnetSvm instance
1156impl SurfnetSvmLocker {
1157    /// Simulates a transaction on the SVM, returning detailed info or failure metadata.
1158    #[allow(clippy::result_large_err)]
1159    pub fn simulate_transaction(
1160        &self,
1161        transaction: VersionedTransaction,
1162        sigverify: bool,
1163    ) -> Result<SimulatedTransactionInfo, FailedTransactionMetadata> {
1164        self.with_svm_reader(move |svm_reader| {
1165            svm_reader.simulate_transaction(transaction, sigverify)
1166        })
1167    }
1168
1169    pub fn is_instruction_profiling_enabled(&self) -> bool {
1170        self.with_svm_reader(|svm_reader| svm_reader.instruction_profiling_enabled)
1171    }
1172
1173    pub fn get_profiling_map_capacity(&self) -> usize {
1174        self.with_svm_reader(|svm_reader| svm_reader.max_profiles)
1175    }
1176
1177    pub async fn process_transaction(
1178        &self,
1179        remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>,
1180        transaction: VersionedTransaction,
1181        status_tx: Sender<TransactionStatusEvent>,
1182        skip_preflight: bool,
1183        sigverify: bool,
1184    ) -> SurfpoolResult<()> {
1185        let do_propagate_status_updates = true;
1186        let signature = transaction.signatures[0];
1187        let profile_result = match self
1188            .fetch_all_tx_accounts_then_process_tx_returning_profile_res(
1189                remote_ctx,
1190                transaction,
1191                &status_tx,
1192                skip_preflight,
1193                sigverify,
1194                do_propagate_status_updates,
1195            )
1196            .await
1197        {
1198            Ok(result) => result,
1199            Err(e) => {
1200                // Ensure the status channel always receives a response to prevent
1201                // the RPC handler from hanging on recv() when errors occur during
1202                // account fetching, ALT resolution, or other pre-processing steps.
1203                // This is critical for issue #454 where program close stops block production.
1204                //
1205                // AccountLoadedTwice errors should go through SimulationFailure to produce
1206                // Agave-compatible JSON-RPC error format with structured `err` and `data` fields.
1207                let err_str = e.to_string();
1208                if err_str.contains("Account loaded twice") {
1209                    let _ = status_tx.try_send(TransactionStatusEvent::SimulationFailure((
1210                        TransactionError::AccountLoadedTwice,
1211                        surfpool_types::TransactionMetadata::default(),
1212                    )));
1213                } else {
1214                    let _ =
1215                        status_tx.try_send(TransactionStatusEvent::VerificationFailure(err_str));
1216                }
1217                return Err(e);
1218            }
1219        };
1220
1221        self.with_svm_writer(|svm_writer| {
1222            svm_writer.write_executed_profile_result(signature, profile_result)
1223        })?;
1224
1225        Ok(())
1226    }
1227
1228    pub async fn profile_transaction(
1229        &self,
1230        remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>,
1231        transaction: VersionedTransaction,
1232        tag: Option<String>,
1233    ) -> SurfpoolContextualizedResult<Uuid> {
1234        // Use clone_for_profiling to wrap all storage fields with overlay storage,
1235        // ensuring mutations during profiling don't affect the underlying database
1236        let svm_clone = self.with_svm_reader(|svm_reader| svm_reader.clone_for_profiling());
1237
1238        let svm_locker = SurfnetSvmLocker::new(svm_clone);
1239
1240        let (status_tx, _) = crossbeam_channel::unbounded();
1241
1242        let skip_preflight = true; // skip preflight checks during transaction profiling
1243        let sigverify = true; // do verify signatures during transaction profiling
1244        let do_propagate_status_updates = false; // don't propagate status updates during transaction profiling
1245        let mut profile_result = svm_locker
1246            .fetch_all_tx_accounts_then_process_tx_returning_profile_res(
1247                remote_ctx,
1248                transaction,
1249                &status_tx,
1250                skip_preflight,
1251                sigverify,
1252                do_propagate_status_updates,
1253            )
1254            .await?;
1255
1256        let uuid = Uuid::new_v4();
1257        profile_result.key = UuidOrSignature::Uuid(uuid);
1258
1259        self.with_svm_writer(|svm_writer| {
1260            svm_writer.write_simulated_profile_result(uuid, tag, profile_result)
1261        })?;
1262
1263        Ok(self.with_contextualized_svm_reader(|_| uuid))
1264    }
1265
1266    pub(crate) async fn fetch_all_tx_accounts_then_process_tx_returning_profile_res(
1267        &self,
1268        remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>,
1269        transaction: VersionedTransaction,
1270        status_tx: &Sender<TransactionStatusEvent>,
1271        skip_preflight: bool,
1272        sigverify: bool,
1273        do_propagate: bool,
1274    ) -> SurfpoolResult<KeyedProfileResult> {
1275        let signature = transaction.signatures[0];
1276
1277        // Sigverify the transaction upfront before doing any account fetching or other pre-processing.
1278        if sigverify {
1279            self.with_svm_reader(|svm_reader| svm_reader.sigverify(&transaction))
1280                .map_err(|e| Into::<SurfpoolError>::into(e.err))?;
1281        }
1282
1283        let latest_absolute_slot = self.with_svm_writer(|svm_writer| {
1284            let latest_absolute_slot = svm_writer.get_latest_absolute_slot();
1285            svm_writer.notify_signature_subscribers(
1286                SignatureSubscriptionType::received(),
1287                &signature,
1288                latest_absolute_slot,
1289                None,
1290            );
1291
1292            latest_absolute_slot
1293        });
1294
1295        // find accounts that are needed for this transaction but are missing from the local
1296        // svm cache, fetch them from the RPC, and insert them locally
1297        let tx_loaded_addresses = self
1298            .get_loaded_addresses(remote_ctx, &transaction.message)
1299            .await?;
1300
1301        // Check for duplicate accounts between static keys and ALT-loaded addresses.
1302        // Agave rejects such transactions pre-execution with AccountLoadedTwice.
1303        if let Some(ref loaded) = tx_loaded_addresses {
1304            let static_keys: HashSet<&Pubkey> =
1305                transaction.message.static_account_keys().iter().collect();
1306            for loaded_key in loaded.all_loaded_addresses() {
1307                if static_keys.contains(loaded_key) {
1308                    return Err(TransactionError::AccountLoadedTwice.into());
1309                }
1310            }
1311        }
1312
1313        // we don't want the pubkeys of the address lookup tables to be included in the transaction accounts,
1314        // but we do want the pubkeys of the accounts _loaded_ by the ALT to be in the transaction accounts.
1315        let transaction_accounts = self.get_pubkeys_from_message(
1316            &transaction.message,
1317            tx_loaded_addresses
1318                .as_ref()
1319                .map(|l| l.all_loaded_addresses()),
1320        );
1321        debug!(
1322            "Transaction {} accounts inputs: {}",
1323            transaction.get_signature(),
1324            transaction_accounts.iter().join(", ")
1325        );
1326
1327        let account_updates = self
1328            .get_multiple_accounts(remote_ctx, &transaction_accounts, None)
1329            .await?
1330            .inner;
1331
1332        // We also need the pubkeys of the ALTs to be pulled from the remote, so we'll do a fetch for them
1333        let alt_account_updates = self
1334            .get_multiple_accounts(
1335                remote_ctx,
1336                &tx_loaded_addresses
1337                    .as_ref()
1338                    .map(|l| l.alt_addresses())
1339                    .unwrap_or_default(),
1340                None,
1341            )
1342            .await?
1343            .inner;
1344
1345        let readonly_account_states = transaction_accounts
1346            .iter()
1347            .enumerate()
1348            .filter_map(|(i, pubkey)| {
1349                if transaction.message.is_maybe_writable(i, None) {
1350                    None
1351                } else {
1352                    self.get_account_local(pubkey)
1353                        .inner
1354                        .map_account()
1355                        .ok()
1356                        .map(|a| (*pubkey, a))
1357                }
1358            })
1359            .collect::<HashMap<_, _>>();
1360
1361        self.with_svm_writer(|svm_writer| {
1362            for update in &account_updates {
1363                svm_writer.write_account_update(update.clone());
1364            }
1365            for update in alt_account_updates {
1366                svm_writer.write_account_update(update);
1367            }
1368        });
1369
1370        let pre_execution_capture = {
1371            let mut capture = ExecutionCapture::new();
1372            for account_update in account_updates.into_iter() {
1373                match account_update {
1374                    GetAccountResult::None(pubkey) => {
1375                        capture.insert(pubkey, None);
1376                    }
1377                    GetAccountResult::FoundAccount(pubkey, account, _)
1378                    | GetAccountResult::FoundProgramAccount((pubkey, account), _)
1379                    | GetAccountResult::FoundTokenAccount((pubkey, account), _) => {
1380                        capture.insert(pubkey, Some(account));
1381                    }
1382                }
1383            }
1384            capture
1385        };
1386
1387        let (accounts_before, token_accounts_before, token_programs) =
1388            self.with_svm_reader(|svm_reader| {
1389                let accounts_before = transaction_accounts
1390                    .iter()
1391                    .map(|p| svm_reader.inner.get_account(p))
1392                    .collect::<Result<Vec<Option<Account>>, SurfpoolError>>()?;
1393
1394                let token_accounts_before = transaction_accounts
1395                    .iter()
1396                    .enumerate()
1397                    .filter_map(|(i, p)| {
1398                        svm_reader
1399                            .token_accounts
1400                            .get(&p.to_string())
1401                            .ok()
1402                            .flatten()
1403                            .map(|a| (i, a))
1404                    })
1405                    .collect::<Vec<_>>();
1406
1407                let token_programs = token_accounts_before
1408                    .iter()
1409                    .map(|(i, ta)| {
1410                        svm_reader
1411                            .get_account(&transaction_accounts[*i])
1412                            .map(|res| res.map(|a| a.owner).unwrap_or(ta.token_program_id()))
1413                    })
1414                    .collect::<Result<Vec<_>, SurfpoolError>>()?;
1415
1416                Ok::<
1417                    (
1418                        Vec<Option<Account>>,
1419                        Vec<(usize, TokenAccount)>,
1420                        Vec<Pubkey>,
1421                    ),
1422                    SurfpoolError,
1423                >((accounts_before, token_accounts_before, token_programs))
1424            })?;
1425
1426        let loaded_addresses = tx_loaded_addresses.as_ref().map(|l| l.loaded_addresses());
1427
1428        let ix_profiles = if self.is_instruction_profiling_enabled() {
1429            match self
1430                .generate_instruction_profiles(
1431                    &transaction,
1432                    &transaction_accounts,
1433                    &tx_loaded_addresses,
1434                    &accounts_before,
1435                    &token_accounts_before,
1436                    &token_programs,
1437                    pre_execution_capture.clone(),
1438                    &status_tx,
1439                )
1440                .await
1441            {
1442                Ok(profiles) => profiles,
1443                Err(e) => {
1444                    let _ = self.simnet_events_tx().try_send(SimnetEvent::error(format!(
1445                        "Failed to generate instruction profiles: {}",
1446                        e
1447                    )));
1448                    None
1449                }
1450            }
1451        } else {
1452            None
1453        };
1454
1455        let profile_result = self
1456            .process_transaction_internal(
1457                transaction,
1458                skip_preflight,
1459                sigverify,
1460                &transaction_accounts,
1461                &loaded_addresses,
1462                &accounts_before,
1463                &token_accounts_before,
1464                &token_programs,
1465                pre_execution_capture,
1466                &status_tx,
1467                do_propagate,
1468            )
1469            .await?;
1470
1471        Ok(KeyedProfileResult::new(
1472            latest_absolute_slot,
1473            UuidOrSignature::Signature(signature),
1474            ix_profiles,
1475            profile_result,
1476            readonly_account_states,
1477        ))
1478    }
1479
1480    #[allow(clippy::too_many_arguments)]
1481    async fn generate_instruction_profiles(
1482        &self,
1483        transaction: &VersionedTransaction,
1484        transaction_accounts: &[Pubkey],
1485        loaded_addresses: &Option<TransactionLoadedAddresses>,
1486        accounts_before: &[Option<Account>],
1487        token_accounts_before: &[(usize, TokenAccount)],
1488        token_programs: &[Pubkey],
1489        pre_execution_capture: ExecutionCapture,
1490        status_tx: &Sender<TransactionStatusEvent>,
1491    ) -> SurfpoolResult<Option<Vec<ProfileResult>>> {
1492        let instructions = transaction.message.instructions();
1493        let ix_count = instructions.len();
1494        if ix_count == 0 {
1495            return Ok(None);
1496        }
1497        // Extract account categories from original transaction
1498
1499        let mut ix_profile_results: Vec<ProfileResult> = vec![];
1500
1501        for idx in 1..=ix_count {
1502            let partial_transaction_res = self.create_partial_transaction(
1503                instructions,
1504                transaction_accounts,
1505                transaction,
1506                idx,
1507                loaded_addresses,
1508            );
1509
1510            let mut ix_required_accounts = IndexSet::new();
1511            for &account_idx in &instructions[idx - 1].accounts {
1512                ix_required_accounts.insert(transaction_accounts[account_idx as usize]);
1513            }
1514            ix_required_accounts
1515                .insert(transaction_accounts[instructions[idx - 1].program_id_index as usize]);
1516
1517            let Some(partial_tx) = partial_transaction_res else {
1518                debug!("Unable to create partial transaction");
1519                return Ok(None);
1520            };
1521
1522            let mut previous_execution_captures = ExecutionCapture::new();
1523            let mut previous_cus = 0;
1524            let mut previous_log_count = 0;
1525            for result in ix_profile_results.iter() {
1526                previous_execution_captures.extend(result.post_execution_capture.clone());
1527                previous_cus += result.compute_units_consumed;
1528                previous_log_count += result.log_messages.as_ref().map(|m| m.len()).unwrap_or(0);
1529            }
1530
1531            let skip_preflight = true;
1532            let sigverify = false;
1533            let do_propagate = false;
1534
1535            let mut pre_execution_capture_cursor = pre_execution_capture.clone();
1536            // If a pre-execution capture was provided, any pubkeys that are in the capture
1537            // that we just took should be replaced with those from the pre-execution capture.
1538            let capture_keys: Vec<_> = pre_execution_capture_cursor.keys().cloned().collect();
1539            for pubkey in capture_keys.into_iter() {
1540                if let Some(pre_account) = previous_execution_captures.remove(&pubkey) {
1541                    // Replace the account with the pre-execution one
1542                    pre_execution_capture_cursor.insert(pubkey, pre_account);
1543                }
1544            }
1545            let mut svm_clone = self.with_svm_reader(|svm_reader| svm_reader.clone_for_profiling());
1546
1547            let (dummy_simnet_tx, _) = crossbeam_channel::bounded(1);
1548            let (dummy_geyser_tx, _) = crossbeam_channel::bounded(1);
1549            svm_clone.simnet_events_tx = dummy_simnet_tx;
1550            svm_clone.geyser_events_tx = dummy_geyser_tx;
1551
1552            let svm_locker = SurfnetSvmLocker::new(svm_clone);
1553            let mut profile_result = svm_locker
1554                .process_transaction_internal(
1555                    partial_tx,
1556                    skip_preflight,
1557                    sigverify,
1558                    transaction_accounts,
1559                    &loaded_addresses.as_ref().map(|l| l.loaded_addresses()),
1560                    accounts_before,
1561                    token_accounts_before,
1562                    token_programs,
1563                    pre_execution_capture_cursor,
1564                    status_tx,
1565                    do_propagate,
1566                )
1567                .await?;
1568
1569            profile_result
1570                .pre_execution_capture
1571                .retain(|pubkey, _| ix_required_accounts.contains(pubkey));
1572            profile_result
1573                .post_execution_capture
1574                .retain(|pubkey, _| ix_required_accounts.contains(pubkey));
1575
1576            profile_result.compute_units_consumed = profile_result
1577                .compute_units_consumed
1578                .saturating_sub(previous_cus);
1579            profile_result.log_messages = profile_result.log_messages.map(|logs| {
1580                logs.into_iter()
1581                    .skip(previous_log_count)
1582                    .collect::<Vec<_>>()
1583            });
1584
1585            ix_profile_results.push(profile_result);
1586        }
1587
1588        Ok(Some(ix_profile_results))
1589    }
1590
1591    fn handle_simulation_failure(
1592        &self,
1593        signature: Signature,
1594        failed_transaction_metadata: FailedTransactionMetadata,
1595        pre_execution_capture: ExecutionCapture,
1596        simulated_slot: Slot,
1597        status_tx: Sender<TransactionStatusEvent>,
1598        do_propagate: bool,
1599    ) -> ProfileResult {
1600        let FailedTransactionMetadata { err, meta } = failed_transaction_metadata;
1601
1602        let cus = meta.compute_units_consumed;
1603        let log_messages = meta.logs.clone();
1604        let err_string = err.to_string();
1605
1606        if do_propagate {
1607            let meta = convert_transaction_metadata_from_canonical(&meta);
1608            let simnet_events_tx = self.simnet_events_tx();
1609            let _ = simnet_events_tx.try_send(SimnetEvent::error(format!(
1610                "Transaction simulation failed: {}",
1611                err
1612            )));
1613
1614            self.with_svm_writer(|svm_writer| {
1615                svm_writer.notify_signature_subscribers(
1616                    SignatureSubscriptionType::processed(),
1617                    &signature,
1618                    simulated_slot,
1619                    Some(err.clone()),
1620                );
1621                svm_writer.notify_logs_subscribers(
1622                    &signature,
1623                    Some(err.clone()),
1624                    log_messages.clone(),
1625                    CommitmentLevel::Processed,
1626                );
1627            });
1628            let _ = status_tx.try_send(TransactionStatusEvent::SimulationFailure((err, meta)));
1629        }
1630        ProfileResult::new(
1631            pre_execution_capture,
1632            BTreeMap::new(),
1633            cus,
1634            Some(log_messages),
1635            Some(err_string),
1636        )
1637    }
1638
1639    #[allow(clippy::too_many_arguments)]
1640    fn handle_execution_failure(
1641        &self,
1642        failed_transaction_metadata: FailedTransactionMetadata,
1643        transaction: VersionedTransaction,
1644        simulated_slot: Slot,
1645        pubkeys_from_message: &[Pubkey],
1646        accounts_before: &[Option<Account>],
1647        token_accounts_before: &[(usize, TokenAccount)],
1648        token_programs: &[Pubkey],
1649        loaded_addresses: &Option<LoadedAddresses>,
1650        pre_execution_capture: ExecutionCapture,
1651        status_tx: Sender<TransactionStatusEvent>,
1652        do_propagate: bool,
1653    ) -> SurfpoolResult<ProfileResult> {
1654        let FailedTransactionMetadata { err, meta } = failed_transaction_metadata;
1655
1656        let cus = meta.compute_units_consumed;
1657        let log_messages = meta.logs.clone();
1658        let err_string = err.to_string();
1659        let signature = meta.signature;
1660
1661        let accounts_after = pubkeys_from_message
1662            .iter()
1663            .map(|p| self.with_svm_reader(|svm_reader| svm_reader.inner.get_account(p)))
1664            .collect::<SurfpoolResult<Vec<Option<Account>>>>()?;
1665
1666        for (pubkey, (before, after)) in pubkeys_from_message
1667            .iter()
1668            .zip(accounts_before.iter().zip(accounts_after.iter()))
1669        {
1670            if before.ne(&after) {
1671                self.with_svm_writer(|svm_writer| {
1672                    if let Some(after) = &after {
1673                        let _ = svm_writer.update_account_registries(pubkey, after);
1674                        svm_writer.notify_account_subscribers(pubkey, &after);
1675                        svm_writer.notify_program_subscribers(pubkey, &after);
1676                    } else {
1677                        svm_writer.notify_account_subscribers(pubkey, &Account::default());
1678                        svm_writer.notify_program_subscribers(pubkey, &Account::default());
1679                    }
1680                });
1681            }
1682        }
1683
1684        let token_mints = self
1685            .with_svm_reader(|svm_reader| {
1686                token_accounts_before
1687                    .iter()
1688                    .map(|(_, a)| {
1689                        svm_reader
1690                            .token_mints
1691                            .get(&a.mint().to_string())
1692                            .ok()
1693                            .flatten()
1694                            .ok_or(SurfpoolError::token_mint_not_found(a.mint()))
1695                    })
1696                    .collect::<Result<Vec<_>, SurfpoolError>>()
1697            })
1698            .unwrap_or_default();
1699
1700        if do_propagate {
1701            let meta_canonical = convert_transaction_metadata_from_canonical(&meta);
1702            let simnet_events_tx = self.simnet_events_tx();
1703            let _ = simnet_events_tx.try_send(SimnetEvent::error(format!(
1704                "Transaction execution failed: {}",
1705                err
1706            )));
1707            let _ = status_tx.try_send(TransactionStatusEvent::ExecutionFailure((
1708                err.clone(),
1709                meta_canonical.clone(),
1710            )));
1711
1712            self.with_svm_writer(|svm_writer| {
1713                let transaction_with_status_meta = TransactionWithStatusMeta::from_failure(
1714                    simulated_slot,
1715                    transaction.clone(),
1716                    &FailedTransactionMetadata {
1717                        err: err.clone(),
1718                        meta: meta.clone(),
1719                    },
1720                    accounts_before,
1721                    &accounts_after,
1722                    token_accounts_before,
1723                    token_mints,
1724                    token_programs,
1725                    loaded_addresses.clone().unwrap_or_default(),
1726                );
1727                svm_writer.transactions.store(
1728                    signature.to_string(),
1729                    SurfnetTransactionStatus::processed(
1730                        transaction_with_status_meta.clone(),
1731                        HashSet::new(),
1732                    ),
1733                )?;
1734
1735                let _ = svm_writer
1736                    .geyser_events_tx
1737                    .send(GeyserEvent::NotifyTransaction(
1738                        transaction_with_status_meta,
1739                        Some(transaction.clone()),
1740                    ));
1741
1742                svm_writer.transactions_queued_for_confirmation.push_back((
1743                    transaction.clone(),
1744                    status_tx.clone(),
1745                    Some(err.clone()),
1746                ));
1747
1748                svm_writer.notify_signature_subscribers(
1749                    SignatureSubscriptionType::processed(),
1750                    &signature,
1751                    simulated_slot,
1752                    Some(err.clone()),
1753                );
1754                svm_writer.notify_logs_subscribers(
1755                    &signature,
1756                    Some(err.clone()),
1757                    log_messages.clone(),
1758                    CommitmentLevel::Processed,
1759                );
1760                let _ = svm_writer
1761                    .simnet_events_tx
1762                    .try_send(SimnetEvent::transaction_processed(
1763                        meta_canonical,
1764                        Some(err),
1765                    ));
1766                Ok::<(), SurfpoolError>(())
1767            })?;
1768        }
1769        Ok(ProfileResult::new(
1770            pre_execution_capture,
1771            BTreeMap::new(),
1772            cus,
1773            Some(log_messages),
1774            Some(err_string),
1775        ))
1776    }
1777
1778    #[allow(clippy::too_many_arguments)]
1779    fn handle_execution_success(
1780        &self,
1781        transaction_metadata: TransactionMetadata,
1782        transaction: VersionedTransaction,
1783        simulated_slot: Slot,
1784        pubkeys_from_message: &[Pubkey],
1785        loaded_addresses: &Option<LoadedAddresses>,
1786        accounts_before: &[Option<Account>],
1787        token_accounts_before: &[(usize, TokenAccount)],
1788        token_programs: &[Pubkey],
1789        pre_execution_capture: ExecutionCapture,
1790        status_tx: &Sender<TransactionStatusEvent>,
1791        do_propagate: bool,
1792    ) -> SurfpoolResult<ProfileResult> {
1793        let cus = transaction_metadata.compute_units_consumed;
1794        let logs = transaction_metadata.logs.clone();
1795        let signature = transaction.signatures[0];
1796
1797        let post_execution_capture = self.with_svm_writer(|svm_writer| {
1798            let accounts_after = pubkeys_from_message
1799                .iter()
1800                .map(|p| svm_writer.inner.get_account_no_db(p))
1801                .collect::<Vec<Option<Account>>>();
1802            let (sanitized_transaction, versioned_transaction) = if do_propagate {
1803                let address_loader = match (&transaction.message, &loaded_addresses) {
1804                    (VersionedMessage::V0(_), Some(loaded_addresses)) => {
1805                        SimpleAddressLoader::Enabled(loaded_addresses.clone())
1806                    }
1807                    // V0 messages without address table lookups still require an enabled loader.
1808                    (VersionedMessage::V0(_), None) => {
1809                        SimpleAddressLoader::Enabled(LoadedAddresses::default())
1810                    }
1811                    (VersionedMessage::Legacy(_) | VersionedMessage::V1(_), _) => {
1812                        SimpleAddressLoader::Disabled
1813                    }
1814                };
1815
1816                (
1817                    SanitizedTransaction::try_create(
1818                        transaction.clone(),
1819                        transaction.message.hash(),
1820                        Some(false),
1821                        address_loader,
1822                        &HashSet::new(), // todo: provide reserved account keys
1823                    )
1824                    .map_err(|error| {
1825                        debug!(
1826                            "Failed to sanitize transaction {} for Geyser account updates: {:?}",
1827                            signature, error
1828                        );
1829                        error
1830                    })
1831                    .ok(),
1832                    Some(transaction.clone()),
1833                )
1834            } else {
1835                (None, None)
1836            };
1837
1838            let mut mutated_account_pubkeys = HashSet::new();
1839            for (pubkey, (before, after)) in pubkeys_from_message
1840                .iter()
1841                .zip(accounts_before.iter().zip(accounts_after.clone()))
1842            {
1843                if before.ne(&after) {
1844                    mutated_account_pubkeys.insert(*pubkey);
1845                    let after = after.unwrap_or_default();
1846                    svm_writer.update_account_registries(pubkey, &after)?;
1847                    let write_version = svm_writer.increment_write_version();
1848
1849                    if let Some(sanitized_transaction) = sanitized_transaction.clone() {
1850                        let _ = svm_writer.geyser_events_tx.send(GeyserEvent::UpdateAccount(
1851                            GeyserAccountUpdate::transaction_update(
1852                                *pubkey,
1853                                after.clone(),
1854                                svm_writer.get_latest_absolute_slot(),
1855                                sanitized_transaction.clone(),
1856                                write_version,
1857                            ),
1858                        ));
1859                    }
1860                    svm_writer.notify_account_subscribers(pubkey, &after);
1861                    svm_writer.notify_program_subscribers(pubkey, &after);
1862                }
1863            }
1864
1865            let mut token_accounts_after = vec![];
1866            let mut post_execution_capture = BTreeMap::new();
1867            let mut post_token_program_ids = vec![];
1868
1869            for (i, (pubkey, account)) in pubkeys_from_message
1870                .iter()
1871                .zip(accounts_after.iter())
1872                .enumerate()
1873            {
1874                let token_account = svm_writer
1875                    .token_accounts
1876                    .get(&pubkey.to_string())
1877                    .ok()
1878                    .flatten();
1879                post_execution_capture.insert(*pubkey, account.clone());
1880
1881                if let Some(token_account) = token_account {
1882                    token_accounts_after.push((i, token_account));
1883                    post_token_program_ids.push(
1884                        account
1885                            .as_ref()
1886                            .map(|a| a.owner)
1887                            .unwrap_or(spl_token_interface::id()),
1888                    );
1889                }
1890            }
1891
1892            let token_mints = token_accounts_after
1893                .iter()
1894                .map(|(_, a)| {
1895                    svm_writer
1896                        .token_mints
1897                        .get(&a.mint().to_string())
1898                        .ok()
1899                        .flatten()
1900                        .ok_or(SurfpoolError::token_mint_not_found(a.mint()))
1901                })
1902                .collect::<Result<Vec<_>, SurfpoolError>>()?;
1903
1904            if do_propagate {
1905                let transaction_meta =
1906                    convert_transaction_metadata_from_canonical(&transaction_metadata);
1907                let transaction_with_status_meta = TransactionWithStatusMeta::new(
1908                    svm_writer.get_latest_absolute_slot(),
1909                    transaction.clone(),
1910                    transaction_metadata,
1911                    accounts_before,
1912                    &accounts_after,
1913                    token_accounts_before,
1914                    &token_accounts_after,
1915                    token_mints,
1916                    token_programs,
1917                    &post_token_program_ids,
1918                    loaded_addresses.clone().unwrap_or_default(),
1919                );
1920                svm_writer.transactions.store(
1921                    transaction_meta.signature.to_string(),
1922                    SurfnetTransactionStatus::processed(
1923                        transaction_with_status_meta.clone(),
1924                        mutated_account_pubkeys,
1925                    ),
1926                )?;
1927
1928                let _ = svm_writer
1929                    .simnet_events_tx
1930                    .try_send(SimnetEvent::transaction_processed(transaction_meta, None));
1931
1932                let _ = svm_writer
1933                    .geyser_events_tx
1934                    .send(GeyserEvent::NotifyTransaction(
1935                        transaction_with_status_meta,
1936                        versioned_transaction,
1937                    ));
1938
1939                svm_writer.transactions_queued_for_confirmation.push_back((
1940                    transaction.clone(),
1941                    status_tx.clone(),
1942                    None,
1943                ));
1944
1945                svm_writer.notify_signature_subscribers(
1946                    SignatureSubscriptionType::processed(),
1947                    &signature,
1948                    simulated_slot,
1949                    None,
1950                );
1951                svm_writer.notify_logs_subscribers(
1952                    &signature,
1953                    None,
1954                    logs.clone(),
1955                    CommitmentLevel::Processed,
1956                );
1957                let _ = status_tx.try_send(TransactionStatusEvent::Success(
1958                    TransactionConfirmationStatus::Processed,
1959                ));
1960            }
1961
1962            Ok::<ExecutionCapture, SurfpoolError>(post_execution_capture)
1963        })?;
1964
1965        Ok(ProfileResult::new(
1966            pre_execution_capture,
1967            post_execution_capture,
1968            cus,
1969            Some(logs),
1970            None,
1971        ))
1972    }
1973
1974    #[allow(clippy::too_many_arguments)]
1975    async fn process_transaction_internal(
1976        &self,
1977        transaction: VersionedTransaction,
1978        skip_preflight: bool,
1979        sigverify: bool,
1980        transaction_accounts: &[Pubkey],
1981        loaded_addresses: &Option<LoadedAddresses>,
1982        accounts_before: &[Option<Account>],
1983        token_accounts_before: &[(usize, TokenAccount)],
1984        token_programs: &[Pubkey],
1985        pre_execution_capture: ExecutionCapture,
1986        status_tx: &Sender<TransactionStatusEvent>,
1987        do_propagate: bool,
1988    ) -> SurfpoolResult<ProfileResult> {
1989        let res = match self
1990            .do_process_transaction_internal(transaction.clone(), skip_preflight, sigverify)
1991            .await
1992        {
1993            ProcessTransactionResult::Success(transaction_metadata) => self
1994                .handle_execution_success(
1995                    transaction_metadata,
1996                    transaction,
1997                    self.get_latest_absolute_slot(),
1998                    transaction_accounts,
1999                    loaded_addresses,
2000                    accounts_before,
2001                    token_accounts_before,
2002                    token_programs,
2003                    pre_execution_capture,
2004                    status_tx,
2005                    do_propagate,
2006                )?,
2007            ProcessTransactionResult::SimulationFailure(failed_transaction_metadata) => self
2008                .handle_simulation_failure(
2009                    transaction.signatures[0],
2010                    failed_transaction_metadata,
2011                    pre_execution_capture,
2012                    self.get_latest_absolute_slot(),
2013                    status_tx.clone(),
2014                    do_propagate,
2015                ),
2016            ProcessTransactionResult::ExecutionFailure(failed) => self.handle_execution_failure(
2017                failed,
2018                transaction,
2019                self.get_latest_absolute_slot(),
2020                transaction_accounts,
2021                accounts_before,
2022                token_accounts_before,
2023                token_programs,
2024                loaded_addresses,
2025                pre_execution_capture,
2026                status_tx.clone(),
2027                do_propagate,
2028            )?,
2029        };
2030        Ok(res)
2031    }
2032
2033    async fn do_process_transaction_internal(
2034        &self,
2035        transaction: VersionedTransaction,
2036        skip_preflight: bool,
2037        sigverify: bool,
2038    ) -> ProcessTransactionResult {
2039        // if not skipping preflight, simulate the transaction
2040        if !skip_preflight {
2041            if let Err(e) = self.with_svm_reader(|svm_reader| {
2042                svm_reader
2043                    .simulate_transaction(transaction.clone(), sigverify)
2044                    .map_err(ProcessTransactionResult::SimulationFailure)
2045            }) {
2046                return e;
2047            }
2048        }
2049
2050        match self.with_svm_writer(|svm_writer| {
2051            svm_writer
2052                .send_transaction(transaction, false /* cu_analysis_enabled */, sigverify)
2053                .map_err(|e| {
2054                    debug!("Transaction execution failure: {:?}", e.meta);
2055                    ProcessTransactionResult::ExecutionFailure(e)
2056                })
2057                .map(ProcessTransactionResult::Success)
2058        }) {
2059            Ok(res) => res,
2060            Err(res) => res,
2061        }
2062    }
2063}
2064
2065/// Functions for writing account updates to the underlying SurfnetSvm instance
2066impl SurfnetSvmLocker {
2067    /// Writes a single account update into the SVM state if present.
2068    pub fn write_account_update(&self, account_update: GetAccountResult) {
2069        if !account_update.requires_update() {
2070            return;
2071        }
2072
2073        self.with_svm_writer(move |svm_writer| {
2074            svm_writer.write_account_update(account_update.clone())
2075        })
2076    }
2077
2078    /// Writes multiple account updates into the SVM state when any are present.
2079    pub fn write_multiple_account_updates(&self, account_updates: &[GetAccountResult]) {
2080        if account_updates
2081            .iter()
2082            .all(|update| !update.requires_update())
2083        {
2084            return;
2085        }
2086
2087        self.with_svm_writer(move |svm_writer| {
2088            for update in account_updates {
2089                svm_writer.write_account_update(update.clone());
2090            }
2091        });
2092    }
2093
2094    /// Resets an account in the SVM state for refresh/streaming.
2095    ///
2096    /// This function coordinates the reset of accounts by removing them from the local cache,
2097    /// allowing them to be fetched fresh from mainnet on the next access.
2098    /// It handles program accounts (including their program data accounts) and can optionally
2099    /// cascade the reset to all accounts owned by a program.
2100    pub fn reset_account(
2101        &self,
2102        pubkey: Pubkey,
2103        include_owned_accounts: bool,
2104    ) -> SurfpoolResult<()> {
2105        let simnet_events_tx = self.simnet_events_tx();
2106        let _ = simnet_events_tx.send(SimnetEvent::info(format!(
2107            "Account {} will be reset",
2108            pubkey
2109        )));
2110        // Set the account online so it can be fetched from mainnet again.
2111        self.remove_offline_account(pubkey, include_owned_accounts)?;
2112
2113        self.with_svm_writer(move |svm_writer| {
2114            svm_writer.reset_account(&pubkey, include_owned_accounts)
2115        })
2116    }
2117
2118    /// Resets SVM state and clears all offline account entries.
2119    ///
2120    /// This function coordinates the reset of the entire network state.
2121    /// It also clears the offline account set so all accounts can be fetched from mainnet again.
2122    pub async fn reset_network(
2123        &self,
2124        remote_ctx: &Option<SurfnetRemoteClient>,
2125    ) -> SurfpoolResult<()> {
2126        let simnet_events_tx = self.simnet_events_tx();
2127        let _ = simnet_events_tx.send(SimnetEvent::info("Resetting network..."));
2128
2129        // Fetch epoch info from remote if available (similar to initialize)
2130        let (mut epoch_info, epoch_schedule) = if let Some(remote_client) = remote_ctx {
2131            (
2132                remote_client.get_epoch_info().await?,
2133                remote_client.get_epoch_schedule().await?,
2134            )
2135        } else {
2136            let epoch_schedule = SurfnetSvm::default_epoch_schedule();
2137            let epoch_info = SurfnetSvm::default_epoch_info(&epoch_schedule);
2138            (epoch_info, epoch_schedule)
2139        };
2140        epoch_info.transaction_count = None;
2141
2142        self.with_svm_writer(move |svm_writer| {
2143            let _ = svm_writer.reset_network(epoch_info, epoch_schedule);
2144            let _ = svm_writer.offline_accounts.clear();
2145        });
2146        Ok(())
2147    }
2148
2149    /// Marks an account as offline, preventing it from being downloaded from the remote RPC.
2150    ///
2151    /// When `include_owned_accounts` is enabled, this also marks accounts as offline that are already known locally.
2152    /// Accounts discovered later through direct remote fetches are rejected lazily if they are
2153    /// owned by an offline owner.
2154    pub async fn insert_offline_account(
2155        &self,
2156        pubkey: Pubkey,
2157        include_owned_accounts: bool,
2158    ) -> SurfpoolResult<()> {
2159        let simnet_events_tx = self.simnet_events_tx();
2160        let _ = simnet_events_tx.send(SimnetEvent::info(format!(
2161            "Account {} will be marked offline (excluded from remote downloads)",
2162            pubkey
2163        )));
2164
2165        self.with_svm_writer(move |svm_writer| {
2166            if let Err(e) = svm_writer.offline_accounts.store(
2167                pubkey.to_string(),
2168                OfflineAccountConfig {
2169                    include_owned_accounts,
2170                },
2171            ) {
2172                warn!("Failed to store offline account {}: {}", pubkey, e);
2173            }
2174        });
2175
2176        Ok(())
2177    }
2178
2179    /// Streams an account by its pubkey.
2180    pub fn stream_account(
2181        &self,
2182        pubkey: Pubkey,
2183        include_owned_accounts: bool,
2184    ) -> SurfpoolResult<()> {
2185        let simnet_events_tx = self.simnet_events_tx();
2186        let _ = simnet_events_tx.send(SimnetEvent::info(format!(
2187            "Account {} changes will be streamed",
2188            pubkey
2189        )));
2190        self.with_svm_writer(|svm_writer| {
2191            svm_writer
2192                .streamed_accounts
2193                .store(pubkey.to_string(), include_owned_accounts)
2194        })?;
2195        Ok(())
2196    }
2197
2198    pub fn get_streamed_accounts(&self) -> Vec<(String, bool)> {
2199        self.with_svm_reader(|svm_reader| {
2200            svm_reader
2201                .streamed_accounts
2202                .into_iter()
2203                .map(|iter| iter.collect())
2204                .unwrap_or_default()
2205        })
2206    }
2207
2208    /// Removes an account from the offline account set.
2209    ///
2210    /// This allows the account to be fetched from mainnet again if requested.
2211    /// This is useful when resetting an account for a refresh/stream operation.
2212    pub fn remove_offline_account(
2213        &self,
2214        pubkey: Pubkey,
2215        include_owned_accounts: bool,
2216    ) -> SurfpoolResult<()> {
2217        self.with_svm_writer(move |svm_writer| {
2218            if let Err(e) = svm_writer.offline_accounts.take(&pubkey.to_string()) {
2219                warn!("Failed to set account online {}: {}", pubkey, e);
2220            }
2221
2222            if include_owned_accounts {
2223                // Set online any locally-known accounts owned by this pubkey.
2224                // Uses the accounts_by_owner index as a fast lookup.
2225                let owned_pubkeys: Vec<Pubkey> = svm_writer
2226                    .accounts_by_owner
2227                    .get(&pubkey.to_string())
2228                    .ok()
2229                    .flatten()
2230                    .unwrap_or_default()
2231                    .iter()
2232                    .filter_map(|pk_str| pk_str.parse().ok())
2233                    .collect();
2234                for owned_pk in owned_pubkeys {
2235                    if let Err(e) = svm_writer.offline_accounts.take(&owned_pk.to_string()) {
2236                        warn!("Failed to set account online {}: {}", owned_pk, e);
2237                    }
2238                }
2239            }
2240        });
2241        Ok(())
2242    }
2243
2244    /// Returns true if the given pubkey is marked offline.
2245    pub fn is_account_offline(&self, pubkey: &Pubkey) -> bool {
2246        self.with_svm_reader(|svm_reader| {
2247            svm_reader
2248                .offline_accounts
2249                .contains_key(&pubkey.to_string())
2250                .unwrap_or(false)
2251        })
2252    }
2253
2254    /// Gets all owners whose accounts are marked offline.
2255    pub fn get_offline_account_owners(&self) -> Vec<Pubkey> {
2256        self.with_svm_reader(|svm_reader| {
2257            svm_reader
2258                .offline_accounts
2259                .into_iter()
2260                .unwrap_or_else(|e| {
2261                    warn!("Failed to iterate offline_accounts: {}", e);
2262                    Box::new(std::iter::empty())
2263                })
2264                .filter(|(_, config)| config.include_owned_accounts)
2265                .filter_map(|(k, _)| match k.parse() {
2266                    Ok(pk) => Some(pk),
2267                    Err(e) => {
2268                        warn!("Invalid pubkey in offline_accounts: {}: {}", k, e);
2269                        None
2270                    }
2271                })
2272                .collect()
2273        })
2274    }
2275
2276    /// Registers a scenario for execution
2277    pub fn register_scenario(
2278        &self,
2279        scenario: surfpool_types::Scenario,
2280        slot: Option<Slot>,
2281    ) -> SurfpoolResult<()> {
2282        self.with_svm_writer(move |svm_writer| svm_writer.register_scenario(scenario, slot))
2283    }
2284
2285    /// Materializes overrides for a specific slot (not necessarily the current slot)
2286    pub async fn materialize_overrides_for_slot(
2287        &self,
2288        remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>,
2289        slot: Slot,
2290    ) -> SurfpoolResult<()> {
2291        let mut svm_writer = self.0.write().await;
2292        svm_writer
2293            .materialize_overrides_for_slot(remote_ctx, slot)
2294            .await
2295    }
2296}
2297
2298/// Token account related functions
2299impl SurfnetSvmLocker {
2300    /// Fetches all token accounts for an owner, returning remote results and missing pubkeys contexts.
2301    pub async fn get_token_accounts_by_owner(
2302        &self,
2303        remote_ctx: &Option<SurfnetRemoteClient>,
2304        owner: Pubkey,
2305        filter: &TokenAccountsFilter,
2306        config: &RpcAccountInfoConfig,
2307    ) -> SurfpoolContextualizedResult<Vec<RpcKeyedAccount>> {
2308        if let Some(remote_client) = remote_ctx {
2309            self.get_token_accounts_by_owner_local_then_remote(owner, filter, remote_client, config)
2310                .await
2311        } else {
2312            self.get_token_accounts_by_owner_local(owner, filter, config)
2313        }
2314    }
2315
2316    pub fn get_token_accounts_by_owner_local(
2317        &self,
2318        owner: Pubkey,
2319        filter: &TokenAccountsFilter,
2320        config: &RpcAccountInfoConfig,
2321    ) -> SurfpoolContextualizedResult<Vec<RpcKeyedAccount>> {
2322        let result = self.with_contextualized_svm_reader(|svm_reader| {
2323            svm_reader
2324                .get_parsed_token_accounts_by_owner(&owner)
2325                .iter()
2326                .filter_map(|(pubkey, token_account)| {
2327                    svm_reader
2328                        .get_account(pubkey)
2329                        .map(|res| {
2330                            let Some(account) = res else {
2331                                return None;
2332                            };
2333                            if match filter {
2334                                TokenAccountsFilter::Mint(mint) => token_account.mint().eq(mint),
2335                                TokenAccountsFilter::ProgramId(program_id) => {
2336                                    account.owner.eq(program_id)
2337                                }
2338                            } {
2339                                Some(svm_reader.account_to_rpc_keyed_account(
2340                                    pubkey,
2341                                    &account,
2342                                    config,
2343                                    Some(token_account.mint()),
2344                                ))
2345                            } else {
2346                                None
2347                            }
2348                        })
2349                        .transpose()
2350                })
2351                .collect::<SurfpoolResult<Vec<_>>>()
2352        });
2353        let SvmAccessContext {
2354            slot,
2355            latest_epoch_info,
2356            latest_blockhash,
2357            inner: accounts,
2358        } = result;
2359        Ok(SvmAccessContext::new(
2360            slot,
2361            latest_epoch_info,
2362            latest_blockhash,
2363            accounts?,
2364        ))
2365    }
2366
2367    pub async fn get_token_accounts_by_owner_local_then_remote(
2368        &self,
2369        owner: Pubkey,
2370        filter: &TokenAccountsFilter,
2371        remote_client: &SurfnetRemoteClient,
2372        config: &RpcAccountInfoConfig,
2373    ) -> SurfpoolContextualizedResult<Vec<RpcKeyedAccount>> {
2374        let SvmAccessContext {
2375            slot,
2376            latest_epoch_info,
2377            latest_blockhash,
2378            inner: local_accounts,
2379        } = self.get_token_accounts_by_owner_local(owner, filter, config)?;
2380
2381        let remote_accounts = remote_client
2382            .get_token_accounts_by_owner(owner, filter, config)
2383            .await?;
2384
2385        let mut combined_accounts = remote_accounts;
2386
2387        for local_account in local_accounts {
2388            if let Some((pos, _)) = combined_accounts
2389                .iter()
2390                .find_position(|RpcKeyedAccount { pubkey, .. }| pubkey.eq(&local_account.pubkey))
2391            {
2392                combined_accounts[pos] = local_account;
2393            } else {
2394                combined_accounts.push(local_account);
2395            }
2396        }
2397
2398        Ok(SvmAccessContext::new(
2399            slot,
2400            latest_epoch_info,
2401            latest_blockhash,
2402            combined_accounts,
2403        ))
2404    }
2405
2406    pub async fn get_token_accounts_by_delegate(
2407        &self,
2408        remote_ctx: &Option<SurfnetRemoteClient>,
2409        delegate: Pubkey,
2410        filter: &TokenAccountsFilter,
2411        config: &RpcAccountInfoConfig,
2412    ) -> SurfpoolContextualizedResult<Vec<RpcKeyedAccount>> {
2413        // Validate that the program is supported if using ProgramId filter
2414        if let TokenAccountsFilter::ProgramId(program_id) = filter {
2415            if !is_supported_token_program(program_id) {
2416                return Err(SurfpoolError::unsupported_token_program(*program_id));
2417            }
2418        }
2419
2420        if let Some(remote_client) = remote_ctx {
2421            self.get_token_accounts_by_delegate_local_then_remote(
2422                delegate,
2423                filter,
2424                remote_client,
2425                config,
2426            )
2427            .await
2428        } else {
2429            self.get_token_accounts_by_delegate_local(delegate, filter, config)
2430        }
2431    }
2432}
2433
2434/// Token account by delegate related functions
2435impl SurfnetSvmLocker {
2436    pub fn get_token_accounts_by_delegate_local(
2437        &self,
2438        delegate: Pubkey,
2439        filter: &TokenAccountsFilter,
2440        config: &RpcAccountInfoConfig,
2441    ) -> SurfpoolContextualizedResult<Vec<RpcKeyedAccount>> {
2442        let result = self.with_contextualized_svm_reader(|svm_reader| {
2443            svm_reader
2444                .get_token_accounts_by_delegate(&delegate)
2445                .iter()
2446                .filter_map(|(pubkey, token_account)| {
2447                    svm_reader
2448                        .get_account(pubkey)
2449                        .map(|res| {
2450                            let Some(account) = res else {
2451                                return None;
2452                            };
2453                            let include = match filter {
2454                                TokenAccountsFilter::Mint(mint) => token_account.mint() == *mint,
2455                                TokenAccountsFilter::ProgramId(program_id) => {
2456                                    account.owner == *program_id
2457                                        && is_supported_token_program(program_id)
2458                                }
2459                            };
2460
2461                            if include {
2462                                Some(svm_reader.account_to_rpc_keyed_account(
2463                                    pubkey,
2464                                    &account,
2465                                    config,
2466                                    Some(token_account.mint()),
2467                                ))
2468                            } else {
2469                                None
2470                            }
2471                        })
2472                        .transpose()
2473                })
2474                .collect::<SurfpoolResult<Vec<_>>>()
2475        });
2476        let SvmAccessContext {
2477            slot,
2478            latest_epoch_info,
2479            latest_blockhash,
2480            inner: accounts,
2481        } = result;
2482        Ok(SvmAccessContext::new(
2483            slot,
2484            latest_epoch_info,
2485            latest_blockhash,
2486            accounts?,
2487        ))
2488    }
2489
2490    pub async fn get_token_accounts_by_delegate_local_then_remote(
2491        &self,
2492        delegate: Pubkey,
2493        filter: &TokenAccountsFilter,
2494        remote_client: &SurfnetRemoteClient,
2495        config: &RpcAccountInfoConfig,
2496    ) -> SurfpoolContextualizedResult<Vec<RpcKeyedAccount>> {
2497        let SvmAccessContext {
2498            slot,
2499            latest_epoch_info,
2500            latest_blockhash,
2501            inner: local_accounts,
2502        } = self.get_token_accounts_by_delegate_local(delegate, filter, config)?;
2503
2504        let remote_accounts = remote_client
2505            .get_token_accounts_by_delegate(delegate, filter, config)
2506            .await?;
2507
2508        let mut combined_accounts = remote_accounts;
2509
2510        for local_account in local_accounts {
2511            if let Some((pos, _)) = combined_accounts
2512                .iter()
2513                .find_position(|RpcKeyedAccount { pubkey, .. }| pubkey.eq(&local_account.pubkey))
2514            {
2515                // Replace remote account with local one (local takes precedence)
2516                combined_accounts[pos] = local_account;
2517            } else {
2518                // Add local account that wasn't found in remote results
2519                combined_accounts.push(local_account);
2520            }
2521        }
2522
2523        Ok(SvmAccessContext::new(
2524            slot,
2525            latest_epoch_info,
2526            latest_blockhash,
2527            combined_accounts,
2528        ))
2529    }
2530}
2531
2532/// Get largest account related account
2533impl SurfnetSvmLocker {
2534    pub fn get_token_largest_accounts_local(
2535        &self,
2536        mint: &Pubkey,
2537    ) -> SvmAccessContext<Vec<RpcTokenAccountBalance>> {
2538        self.with_contextualized_svm_reader(|svm_reader| {
2539            let token_accounts = svm_reader.get_token_accounts_by_mint(mint);
2540
2541            // get mint information to determine decimals
2542            let mint_decimals = if let Some(mint_account) =
2543                svm_reader.token_mints.get(&mint.to_string()).ok().flatten()
2544            {
2545                mint_account.decimals()
2546            } else {
2547                0
2548            };
2549
2550            // convert to RpcTokenAccountBalance and sort by balance
2551            let mut balances: Vec<RpcTokenAccountBalance> = token_accounts
2552                .into_iter()
2553                .map(|(pubkey, token_account)| RpcTokenAccountBalance {
2554                    address: pubkey.to_string(),
2555                    amount: UiTokenAmount {
2556                        amount: token_account.amount().to_string(),
2557                        decimals: mint_decimals,
2558                        ui_amount: Some(format_ui_amount(token_account.amount(), mint_decimals)),
2559                        ui_amount_string: format_ui_amount_string(
2560                            token_account.amount(),
2561                            mint_decimals,
2562                        ),
2563                    },
2564                })
2565                .collect();
2566
2567            // sort by amount in descending order
2568            balances.sort_by(|a, b| {
2569                let amount_a: u64 = a.amount.amount.parse().unwrap_or(0);
2570                let amount_b: u64 = b.amount.amount.parse().unwrap_or(0);
2571                amount_b.cmp(&amount_a)
2572            });
2573
2574            // limit to top 20 accounts
2575            balances.truncate(20);
2576
2577            balances
2578        })
2579    }
2580
2581    pub async fn get_token_largest_accounts_local_then_remote(
2582        &self,
2583        client: &SurfnetRemoteClient,
2584        mint: &Pubkey,
2585        commitment_config: CommitmentConfig,
2586    ) -> SurfpoolContextualizedResult<Vec<RpcTokenAccountBalance>> {
2587        let SvmAccessContext {
2588            slot,
2589            latest_epoch_info,
2590            latest_blockhash,
2591            inner: local_accounts,
2592        } = self.get_token_largest_accounts_local(mint);
2593
2594        let remote_accounts = client
2595            .get_token_largest_accounts(mint, commitment_config)
2596            .await?;
2597
2598        let mut combined_accounts = remote_accounts;
2599
2600        // if the account is in both the local and remote list, add the local one and not the remote
2601        for local_account in local_accounts {
2602            if let Some((pos, _)) = combined_accounts
2603                .iter()
2604                .find_position(|remote_account| remote_account.address == local_account.address)
2605            {
2606                combined_accounts[pos] = local_account;
2607            } else {
2608                combined_accounts.push(local_account);
2609            }
2610        }
2611
2612        // re-sort and limit after combining
2613        combined_accounts.sort_by(|a, b| {
2614            let amount_a: u64 = a.amount.amount.parse().unwrap_or(0);
2615            let amount_b: u64 = b.amount.amount.parse().unwrap_or(0);
2616            amount_b.cmp(&amount_a)
2617        });
2618        combined_accounts.truncate(20);
2619
2620        Ok(SvmAccessContext::new(
2621            slot,
2622            latest_epoch_info,
2623            latest_blockhash,
2624            combined_accounts,
2625        ))
2626    }
2627
2628    /// Fetches the largest token accounts for a specific mint, returning contextualized results.
2629    pub async fn get_token_largest_accounts(
2630        &self,
2631        remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>,
2632        mint: &Pubkey,
2633    ) -> SurfpoolContextualizedResult<Vec<RpcTokenAccountBalance>> {
2634        if let Some((remote_client, commitment_config)) = remote_ctx {
2635            self.get_token_largest_accounts_local_then_remote(
2636                remote_client,
2637                mint,
2638                *commitment_config,
2639            )
2640            .await
2641        } else {
2642            Ok(self.get_token_largest_accounts_local(mint))
2643        }
2644    }
2645}
2646
2647/// Address lookup table related functions
2648impl SurfnetSvmLocker {
2649    /// Extracts pubkeys from a VersionedMessage, resolving address lookup tables as needed.
2650    pub fn get_pubkeys_from_message(
2651        &self,
2652        message: &VersionedMessage,
2653        all_transaction_lookup_table_addresses: Option<Vec<&Pubkey>>,
2654    ) -> Vec<Pubkey> {
2655        match message {
2656            VersionedMessage::Legacy(message) => message.account_keys.clone(),
2657            VersionedMessage::V0(message) => {
2658                let mut acc_keys = message.account_keys.clone();
2659
2660                if let Some(loaded_addresses) = all_transaction_lookup_table_addresses {
2661                    acc_keys.extend(loaded_addresses);
2662                }
2663                acc_keys
2664            }
2665            VersionedMessage::V1(message) => message.account_keys.clone(),
2666        }
2667    }
2668
2669    /// Gets addresses loaded from on-chain lookup tables from a VersionedMessage.
2670    pub async fn get_loaded_addresses(
2671        &self,
2672        remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>,
2673        message: &VersionedMessage,
2674    ) -> SurfpoolResult<Option<TransactionLoadedAddresses>> {
2675        match message {
2676            VersionedMessage::Legacy(_) | VersionedMessage::V1(_) => Ok(None),
2677            VersionedMessage::V0(message) => {
2678                if message.address_table_lookups.is_empty() {
2679                    return Ok(None);
2680                }
2681                let mut loaded = TransactionLoadedAddresses::new();
2682                for alt in message.address_table_lookups.iter() {
2683                    self.get_lookup_table_addresses(remote_ctx, alt, &mut loaded)
2684                        .await?;
2685                }
2686
2687                Ok(Some(loaded))
2688            }
2689        }
2690    }
2691
2692    /// Retrieves loaded addresses from a lookup table account, validating owner and indices.
2693    pub async fn get_lookup_table_addresses(
2694        &self,
2695        remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>,
2696        address_table_lookup: &MessageAddressTableLookup,
2697        transaction_loaded_addresses: &mut TransactionLoadedAddresses,
2698    ) -> SurfpoolResult<()> {
2699        let table_account = self
2700            .get_account(remote_ctx, &address_table_lookup.account_key, None)
2701            .await?
2702            .inner
2703            .map_account()?;
2704
2705        if table_account.owner == solana_sdk_ids::address_lookup_table::id() {
2706            let SvmAccessContext {
2707                slot: current_slot,
2708                inner: slot_hashes,
2709                ..
2710            } = self.with_contextualized_svm_reader(|svm_reader| {
2711                svm_reader
2712                    .inner
2713                    .get_sysvar::<solana_slot_hashes::SlotHashes>()
2714            });
2715
2716            //let current_slot = self.get_latest_absolute_slot(); // or should i use this?
2717            let data = &table_account.data.clone();
2718            let lookup_table = AddressLookupTable::deserialize(data).map_err(|_ix_err| {
2719                SurfpoolError::invalid_account_data(
2720                    address_table_lookup.account_key,
2721                    table_account.data,
2722                    Some("Attempted to lookup addresses from an invalid account"),
2723                )
2724            })?;
2725
2726            let writable = lookup_table
2727                .lookup(
2728                    current_slot,
2729                    &address_table_lookup.writable_indexes,
2730                    &slot_hashes,
2731                )
2732                .map_err(|_ix_err| {
2733                    SurfpoolError::invalid_lookup_index(address_table_lookup.account_key)
2734                })?;
2735
2736            let readable = lookup_table
2737                .lookup(
2738                    current_slot,
2739                    &address_table_lookup.readonly_indexes,
2740                    &slot_hashes,
2741                )
2742                .map_err(|_ix_err| {
2743                    SurfpoolError::invalid_lookup_index(address_table_lookup.account_key)
2744                })?;
2745
2746            let MessageAddressTableLookup {
2747                account_key,
2748                writable_indexes,
2749                readonly_indexes,
2750            } = address_table_lookup.to_owned();
2751
2752            transaction_loaded_addresses.insert_members(
2753                account_key,
2754                writable_indexes
2755                    .into_iter()
2756                    .zip(writable.into_iter())
2757                    .collect(),
2758                readonly_indexes
2759                    .into_iter()
2760                    .zip(readable.into_iter())
2761                    .collect(),
2762            );
2763
2764            Ok(())
2765        } else {
2766            Err(SurfpoolError::invalid_account_owner(
2767                table_account.owner,
2768                Some("Attempted to lookup addresses from an account owned by the wrong program"),
2769            ))
2770        }
2771    }
2772}
2773
2774/// Profiling helper functions
2775impl SurfnetSvmLocker {
2776    /// Estimates compute units for a transaction via contextualized simulation.
2777    pub fn estimate_compute_units(
2778        &self,
2779        transaction: &VersionedTransaction,
2780    ) -> SvmAccessContext<ComputeUnitsEstimationResult> {
2781        self.with_contextualized_svm_reader(|svm_reader| {
2782            svm_reader.estimate_compute_units(transaction)
2783        })
2784    }
2785
2786    /// Creates a partial transaction for instruction profiling by extracting and remapping
2787    /// a subset of instructions from the original transaction.
2788    ///
2789    /// This helper function handles the complex logic of:
2790    /// - Collecting all accounts referenced by the instruction subset
2791    /// - Categorizing accounts based on their original roles (signers vs non-signers)
2792    /// - Building a new account key list in the correct order
2793    /// - Remapping instruction account indices to match the new account list
2794    /// - Creating a valid partial transaction with appropriate signatures
2795    ///
2796    /// # Arguments
2797    /// * `instructions` - All instructions from the original transaction
2798    /// * `message_accounts` - Account keys from the original transaction
2799    /// * `mutable_signers` - Mutable signer accounts from original transaction
2800    /// * `readonly_signers` - Readonly signer accounts from original transaction
2801    /// * `mutable_non_signers` - Mutable non-signer accounts from original transaction
2802    /// * `transaction` - The original transaction for reference
2803    /// * `idx` - Number of instructions to include in the partial transaction
2804    ///
2805    /// # Returns
2806    /// A partial transaction containing the first `idx` instructions and the accounts used for
2807    /// the last instruction, or None if creation fails
2808    #[allow(clippy::too_many_arguments)]
2809    fn create_partial_transaction(
2810        &self,
2811        instructions: &[CompiledInstruction],
2812        message_accounts: &[Pubkey],
2813        transaction: &VersionedTransaction,
2814        idx: usize,
2815        loaded_addresses: &Option<TransactionLoadedAddresses>,
2816    ) -> Option<VersionedTransaction> {
2817        // Keep the full account map from the original transaction for every partial pass.
2818        // This simplifies remapping: we only keep the first `idx` instructions, but retain
2819        // the original `message_accounts` ordering and address table lookups.
2820        let ixs_for_tx = instructions[0..idx].to_vec();
2821
2822        // Build a new message that keeps the original account map and address table lookups,
2823        // but only contains the first `idx` instructions.
2824        let new_message = match transaction.message {
2825            VersionedMessage::Legacy(ref message) => VersionedMessage::Legacy(Message {
2826                account_keys: message_accounts[..message.account_keys.len()].to_vec(),
2827                header: message.header,
2828                recent_blockhash: *transaction.message.recent_blockhash(),
2829                instructions: ixs_for_tx.clone(),
2830            }),
2831            VersionedMessage::V0(ref message) => {
2832                VersionedMessage::V0(solana_message::v0::Message {
2833                    account_keys: message_accounts[..message.account_keys.len()].to_vec(),
2834                    header: message.header,
2835                    recent_blockhash: *transaction.message.recent_blockhash(),
2836                    instructions: ixs_for_tx.clone(),
2837                    // Preserve the original address table lookups when available.
2838                    address_table_lookups: loaded_addresses
2839                        .as_ref()
2840                        .map(|l| l.to_address_table_lookups())
2841                        .unwrap_or_default(),
2842                })
2843            }
2844            VersionedMessage::V1(ref message) => {
2845                let mut message = message.clone();
2846                message.account_keys = message_accounts[..message.account_keys.len()].to_vec();
2847                message.instructions = ixs_for_tx;
2848                VersionedMessage::V1(message)
2849            }
2850        };
2851
2852        let tx = VersionedTransaction {
2853            signatures: transaction.signatures.clone(),
2854            message: new_message,
2855        };
2856
2857        Some(tx)
2858    }
2859
2860    /// Returns the profile result for a given signature or UUID, and whether it exists in the SVM.
2861    pub fn get_profile_result(
2862        &self,
2863        signature_or_uuid: UuidOrSignature,
2864        config: &RpcProfileResultConfig,
2865    ) -> SurfpoolResult<Option<UiKeyedProfileResult>> {
2866        let result = match &signature_or_uuid {
2867            UuidOrSignature::Signature(signature) => self.with_svm_reader(|svm| {
2868                svm.executed_transaction_profiles
2869                    .get(&signature.to_string())
2870                    .ok()
2871                    .flatten()
2872            }),
2873            UuidOrSignature::Uuid(uuid) => self.with_svm_reader(|svm| {
2874                svm.simulated_transaction_profiles
2875                    .get(&uuid.to_string())
2876                    .ok()
2877                    .flatten()
2878            }),
2879        };
2880        Ok(result.map(|profile| self.encode_ui_keyed_profile_result(profile, config)))
2881    }
2882
2883    pub fn encode_ui_keyed_profile_result(
2884        &self,
2885        profile: KeyedProfileResult,
2886        config: &RpcProfileResultConfig,
2887    ) -> UiKeyedProfileResult {
2888        self.with_svm_reader(|svm_reader| {
2889            svm_reader.encode_ui_keyed_profile_result(profile, config)
2890        })
2891    }
2892
2893    /// Returns the profile results for a given tag.
2894    pub fn get_profile_results_by_tag(
2895        &self,
2896        tag: String,
2897        config: &RpcProfileResultConfig,
2898    ) -> SurfpoolResult<Option<Vec<UiKeyedProfileResult>>> {
2899        let tag_map = self.with_svm_reader(|svm| svm.profile_tag_map.get(&tag).ok().flatten());
2900        match tag_map {
2901            None => Ok(None),
2902            Some(uuids_or_sigs) => {
2903                let mut profiles = Vec::new();
2904                for id in uuids_or_sigs {
2905                    let profile = self.get_profile_result(id, config)?;
2906                    if profile.is_none() {
2907                        return Err(SurfpoolError::tag_not_found(&tag));
2908                    }
2909                    profiles.push(profile.unwrap());
2910                }
2911                Ok(Some(profiles))
2912            }
2913        }
2914    }
2915
2916    pub fn register_idl(&self, idl: Idl, slot: Option<Slot>) -> SurfpoolResult<()> {
2917        self.with_svm_writer(|svm_writer| svm_writer.register_idl(idl, slot))
2918    }
2919
2920    pub fn get_idl(&self, address: &Pubkey, slot: Option<Slot>) -> Option<Idl> {
2921        self.with_svm_reader(|svm_reader| {
2922            let query_slot = slot.unwrap_or_else(|| svm_reader.get_latest_absolute_slot());
2923            // IDLs are stored sorted by slot descending, so the first one that passes the filter is the latest
2924            svm_reader
2925                .registered_idls
2926                .get(&address.to_string())
2927                .ok()
2928                .flatten()
2929                .and_then(|idl_versions| {
2930                    idl_versions
2931                        .iter()
2932                        .filter(|VersionedIdl(s, _)| *s <= query_slot)
2933                        .max()
2934                        .map(|VersionedIdl(_, idl)| idl.clone())
2935                })
2936        })
2937    }
2938
2939    /// Forges account data by decoding with IDL, applying overrides, and re-encoding.
2940    ///
2941    /// # Arguments
2942    /// * `account_pubkey` - The public key of the account (used for error messages)
2943    /// * `account_data` - The raw account data bytes
2944    /// * `idl` - The IDL for decoding/encoding the account data
2945    /// * `overrides` - HashMap of field paths (dot notation) to values to override
2946    ///
2947    /// # Returns
2948    /// The modified account data bytes with discriminator
2949    /// Forges account data by applying overrides to existing account data
2950    ///
2951    /// This delegates to the SurfnetSvm implementation.
2952    ///
2953    /// # Arguments
2954    /// * `account_pubkey` - The account address (for error messages)
2955    /// * `account_data` - The original account data bytes
2956    /// * `idl` - The IDL for the account's program
2957    /// * `overrides` - Map of field paths to new values
2958    ///
2959    /// # Returns
2960    /// The forged account data as bytes, or an error
2961    pub fn get_forged_account_data(
2962        &self,
2963        account_pubkey: &Pubkey,
2964        account_data: &[u8],
2965        idl: &Idl,
2966        overrides: &HashMap<String, serde_json::Value>,
2967    ) -> SurfpoolResult<Vec<u8>> {
2968        self.with_svm_reader(|svm_reader| {
2969            svm_reader.get_forged_account_data(account_pubkey, account_data, idl, overrides)
2970        })
2971    }
2972}
2973/// Program account related functions
2974impl SurfnetSvmLocker {
2975    /// Clones a program account from source to destination, handling upgradeable loader state.
2976    pub async fn clone_program_account(
2977        &self,
2978        remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>,
2979        source_program_id: &Pubkey,
2980        destination_program_id: &Pubkey,
2981    ) -> SurfpoolContextualizedResult<()> {
2982        let expected_source_program_data_address = get_program_data_address(source_program_id);
2983
2984        let result = self
2985            .get_multiple_accounts(
2986                remote_ctx,
2987                &[*source_program_id, expected_source_program_data_address],
2988                None,
2989            )
2990            .await?;
2991
2992        let mut accounts = result
2993            .inner
2994            .clone()
2995            .into_iter()
2996            .map(|a| a.map_account())
2997            .collect::<SurfpoolResult<Vec<Account>>>()?;
2998
2999        let source_program_data_account = accounts.remove(1);
3000        let source_program_account = accounts.remove(0);
3001
3002        let BpfUpgradeableLoaderAccountType::Program(UiProgram {
3003            program_data: source_program_data_address,
3004        }) = parse_bpf_upgradeable_loader(&source_program_account.data).map_err(|e| {
3005            SurfpoolError::invalid_program_account(source_program_id, e.to_string())
3006        })?
3007        else {
3008            return Err(SurfpoolError::expected_program_account(source_program_id));
3009        };
3010
3011        if source_program_data_address.ne(&expected_source_program_data_address.to_string()) {
3012            return Err(SurfpoolError::invalid_program_account(
3013                source_program_id,
3014                format!(
3015                    "Program data address mismatch: expected {}, found {}",
3016                    expected_source_program_data_address, source_program_data_address
3017                ),
3018            ));
3019        }
3020
3021        let destination_program_data_address = get_program_data_address(destination_program_id);
3022
3023        // create a new program account that has the `program_data` field set to the
3024        // destination program data address
3025        let mut new_program_account = source_program_account;
3026        new_program_account.data = bincode::serialize(&UpgradeableLoaderState::Program {
3027            programdata_address: destination_program_data_address,
3028        })
3029        .map_err(|e| SurfpoolError::internal(format!("Failed to serialize program data: {}", e)))?;
3030
3031        self.with_svm_writer(|svm_writer| {
3032            svm_writer.set_account(
3033                &destination_program_data_address,
3034                source_program_data_account.clone(),
3035            )?;
3036
3037            svm_writer.set_account(destination_program_id, new_program_account.clone())?;
3038            Ok::<(), SurfpoolError>(())
3039        })?;
3040
3041        Ok(result.with_new_value(()))
3042    }
3043
3044    pub async fn set_program_authority(
3045        &self,
3046        remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>,
3047        program_id: Pubkey,
3048        new_authority: Option<Pubkey>,
3049    ) -> SurfpoolContextualizedResult<()> {
3050        let SvmAccessContext {
3051            slot,
3052            latest_epoch_info,
3053            latest_blockhash,
3054            inner: mut get_account_result,
3055        } = self.get_account(remote_ctx, &program_id, None).await?;
3056
3057        let original_authority = match &mut get_account_result {
3058            GetAccountResult::None(pubkey) => {
3059                return Err(SurfpoolError::invalid_program_account(
3060                    pubkey,
3061                    "Account not found",
3062                ));
3063            }
3064            GetAccountResult::FoundAccount(pubkey, program_account, _) => {
3065                let programdata_address = get_program_data_address(pubkey);
3066                let mut programdata_account_result = self
3067                    .get_account(remote_ctx, &programdata_address, None)
3068                    .await?
3069                    .inner;
3070                match &mut programdata_account_result {
3071                    GetAccountResult::None(pubkey) => {
3072                        return Err(SurfpoolError::invalid_program_account(
3073                            pubkey,
3074                            "Program data account does not exist",
3075                        ));
3076                    }
3077                    GetAccountResult::FoundAccount(_, programdata_account, _) => {
3078                        let original_authority = update_programdata_account(
3079                            &program_id,
3080                            programdata_account,
3081                            new_authority,
3082                        )?;
3083
3084                        get_account_result = GetAccountResult::FoundProgramAccount(
3085                            (*pubkey, program_account.clone()),
3086                            (programdata_address, Some(programdata_account.clone())),
3087                        );
3088
3089                        original_authority
3090                    }
3091                    GetAccountResult::FoundProgramAccount(_, _)
3092                    | GetAccountResult::FoundTokenAccount(_, _) => {
3093                        return Err(SurfpoolError::invalid_program_account(
3094                            pubkey,
3095                            "Not a program account",
3096                        ));
3097                    }
3098                }
3099            }
3100            GetAccountResult::FoundProgramAccount(_, (_, None)) => {
3101                return Err(SurfpoolError::invalid_program_account(
3102                    program_id,
3103                    "Program data account does not exist",
3104                ));
3105            }
3106            GetAccountResult::FoundProgramAccount(_, (_, Some(programdata_account))) => {
3107                update_programdata_account(&program_id, programdata_account, new_authority)?
3108            }
3109            GetAccountResult::FoundTokenAccount(_, _) => {
3110                return Err(SurfpoolError::invalid_program_account(
3111                    program_id,
3112                    "Not a program account",
3113                ));
3114            }
3115        };
3116
3117        let simnet_events_tx = self.simnet_events_tx();
3118        match (original_authority, new_authority) {
3119            (Some(original), Some(new)) => {
3120                if original != new {
3121                    let _ = simnet_events_tx.send(SimnetEvent::info(format!(
3122                        "Setting new authority for program {}",
3123                        program_id
3124                    )));
3125                    let _ = simnet_events_tx
3126                        .send(SimnetEvent::info(format!("Old Authority: {}", original)));
3127                    let _ =
3128                        simnet_events_tx.send(SimnetEvent::info(format!("New Authority: {}", new)));
3129                } else {
3130                    let _ = simnet_events_tx.send(SimnetEvent::info(format!(
3131                        "No authority change for program {}",
3132                        program_id
3133                    )));
3134                }
3135            }
3136            (Some(original), None) => {
3137                let _ = simnet_events_tx.send(SimnetEvent::info(format!(
3138                    "Removing authority for program {}",
3139                    program_id
3140                )));
3141                let _ = simnet_events_tx
3142                    .send(SimnetEvent::info(format!("Old Authority: {}", original)));
3143            }
3144            (None, Some(new)) => {
3145                let _ = simnet_events_tx.send(SimnetEvent::info(format!(
3146                    "Setting new authority for program {}",
3147                    program_id
3148                )));
3149                let _ = simnet_events_tx.send(SimnetEvent::info("Old Authority: None".to_string()));
3150                let _ = simnet_events_tx.send(SimnetEvent::info(format!("New Authority: {}", new)));
3151            }
3152            (None, None) => {
3153                let _ = simnet_events_tx.send(SimnetEvent::info(format!(
3154                    "No authority change for program {}",
3155                    program_id
3156                )));
3157            }
3158        };
3159
3160        self.write_account_update(get_account_result);
3161
3162        Ok(SvmAccessContext::new(
3163            slot,
3164            latest_epoch_info,
3165            latest_blockhash,
3166            (),
3167        ))
3168    }
3169
3170    pub async fn get_program_accounts(
3171        &self,
3172        remote_ctx: &Option<SurfnetRemoteClient>,
3173        program_id: &Pubkey,
3174        account_config: RpcAccountInfoConfig,
3175        filters: Option<Vec<RpcFilterType>>,
3176    ) -> SurfpoolContextualizedResult<Vec<RpcKeyedAccount>> {
3177        if let Some(remote_client) = remote_ctx {
3178            self.get_program_accounts_local_then_remote(
3179                remote_client,
3180                program_id,
3181                account_config,
3182                filters,
3183            )
3184            .await
3185        } else {
3186            self.get_program_accounts_local(program_id, account_config, filters)
3187        }
3188    }
3189
3190    /// Retrieves program accounts from the local SVM cache, returning a contextualized result.
3191    pub fn get_program_accounts_local(
3192        &self,
3193        program_id: &Pubkey,
3194        account_config: RpcAccountInfoConfig,
3195        filters: Option<Vec<RpcFilterType>>,
3196    ) -> SurfpoolContextualizedResult<Vec<RpcKeyedAccount>> {
3197        let res = self.with_svm_reader(|svm_reader| {
3198            let res = svm_reader.get_account_owned_by(program_id)?;
3199
3200            let mut filtered = vec![];
3201            for (pubkey, account) in &res {
3202                if let Some(ref active_filters) = filters {
3203                    match apply_rpc_filters(&account.data, active_filters) {
3204                        Ok(true) => {}           // Account matches all filters
3205                        Ok(false) => continue,   // Filtered out
3206                        Err(e) => return Err(e), // Error applying filter, already JsonRpcError
3207                    }
3208                }
3209
3210                filtered.push(svm_reader.account_to_rpc_keyed_account(
3211                    pubkey,
3212                    account,
3213                    &account_config,
3214                    None,
3215                ));
3216            }
3217            Ok(filtered)
3218        })?;
3219
3220        Ok(self.with_contextualized_svm_reader(|_| res.clone()))
3221    }
3222
3223    pub fn encode_ui_account(
3224        &self,
3225        pubkey: &Pubkey,
3226        account: &Account,
3227        encoding: UiAccountEncoding,
3228        additional_data: Option<AccountAdditionalDataV3>,
3229        data_slice: Option<UiDataSliceConfig>,
3230    ) -> UiAccount {
3231        self.with_svm_reader(|svm_reader| {
3232            svm_reader.encode_ui_account(pubkey, account, encoding, additional_data, data_slice)
3233        })
3234    }
3235
3236    /// Retrieves program accounts from the local cache and remote client, combining results.
3237    pub async fn get_program_accounts_local_then_remote(
3238        &self,
3239        client: &SurfnetRemoteClient,
3240        program_id: &Pubkey,
3241        account_config: RpcAccountInfoConfig,
3242        filters: Option<Vec<RpcFilterType>>,
3243    ) -> SurfpoolContextualizedResult<Vec<RpcKeyedAccount>> {
3244        let SvmAccessContext {
3245            slot,
3246            latest_epoch_info,
3247            latest_blockhash,
3248            inner: local_accounts,
3249        } = self.get_program_accounts_local(program_id, account_config.clone(), filters.clone())?;
3250
3251        let remote_accounts_result = client
3252            .get_program_accounts(program_id, account_config, filters)
3253            .await?;
3254
3255        let remote_accounts = remote_accounts_result.handle_method_not_supported(|| {
3256            let tx = self.simnet_events_tx();
3257            let _ = tx.send(SimnetEvent::warn("The `getProgramAccounts` method was sent to the remote RPC, but this method isn't supported by your RPC provider. If you need this method, please use a different RPC provider."));
3258            vec![]
3259        });
3260
3261        let mut combined_accounts = remote_accounts
3262            .into_iter()
3263            .map(|(pubkey, account)| RpcKeyedAccount {
3264                pubkey: pubkey.to_string(),
3265                account,
3266            })
3267            .collect::<Vec<RpcKeyedAccount>>();
3268
3269        for local_account in local_accounts {
3270            // if the local account is in the remote set, replace it with the local one
3271            if let Some((pos, _)) = combined_accounts.iter().find_position(
3272                |RpcKeyedAccount {
3273                     pubkey: remote_pubkey,
3274                     ..
3275                 }| remote_pubkey.eq(&local_account.pubkey),
3276            ) {
3277                combined_accounts[pos] = local_account;
3278            } else {
3279                // otherwise, add the local account to the combined list
3280                combined_accounts.push(local_account);
3281            };
3282        }
3283
3284        Ok(SvmAccessContext {
3285            slot,
3286            latest_epoch_info,
3287            latest_blockhash,
3288            inner: combined_accounts,
3289        })
3290    }
3291}
3292
3293impl SurfnetSvmLocker {
3294    /// Returns the first local slot (the genesis_slot when this surfnet started).
3295    /// Since empty blocks can be reconstructed on-the-fly, all slots from genesis_slot onwards are valid.
3296    pub fn get_first_local_slot(&self) -> Option<Slot> {
3297        self.with_svm_reader(|svm| Some(svm.genesis_slot))
3298    }
3299
3300    pub async fn get_block(
3301        &self,
3302        remote_ctx: &Option<SurfnetRemoteClient>,
3303        slot: &Slot,
3304        config: &RpcBlockConfig,
3305    ) -> SurfpoolContextualizedResult<Option<UiConfirmedBlock>> {
3306        let committed_slot = self.get_slot_for_commitment(&config.commitment.unwrap_or_default());
3307        if *slot > committed_slot {
3308            return Ok(SvmAccessContext {
3309                slot: committed_slot,
3310                latest_epoch_info: self.get_epoch_info(),
3311                latest_blockhash: self
3312                    .get_latest_blockhash(&CommitmentConfig::processed())
3313                    .unwrap_or_default(),
3314                inner: None,
3315            });
3316        }
3317
3318        let first_local_slot = self.get_first_local_slot();
3319
3320        let result = if first_local_slot.is_some() && first_local_slot.unwrap() > *slot {
3321            match remote_ctx {
3322                Some(remote_client) => Some(remote_client.get_block(slot, *config).await?),
3323                None => return Err(SurfpoolError::slot_too_old(*slot)),
3324            }
3325        } else {
3326            self.get_block_local(slot, config)?
3327        };
3328
3329        Ok(SvmAccessContext {
3330            slot: *slot,
3331            latest_epoch_info: self.get_epoch_info(),
3332            latest_blockhash: self
3333                .get_latest_blockhash(&CommitmentConfig::processed())
3334                .unwrap_or_default(),
3335            inner: result,
3336        })
3337    }
3338
3339    pub fn get_block_local(
3340        &self,
3341        slot: &Slot,
3342        config: &RpcBlockConfig,
3343    ) -> SurfpoolResult<Option<UiConfirmedBlock>> {
3344        self.with_svm_reader(|svm_reader| svm_reader.get_block_at_slot(*slot, config))
3345    }
3346
3347    pub fn get_genesis_hash_local(&self) -> SvmAccessContext<Hash> {
3348        self.with_contextualized_svm_reader(|svm_reader| svm_reader.genesis_config.hash())
3349    }
3350
3351    pub async fn get_genesis_hash(
3352        &self,
3353        remote_ctx: &Option<SurfnetRemoteClient>,
3354    ) -> SurfpoolContextualizedResult<Hash> {
3355        if let Some(client) = remote_ctx {
3356            let remote_hash = client.get_genesis_hash().await?;
3357            Ok(self.with_contextualized_svm_reader(|_| remote_hash))
3358        } else {
3359            Ok(self.get_genesis_hash_local())
3360        }
3361    }
3362}
3363
3364/// Pass through functions for accessing the underlying SurfnetSvm instance
3365impl SurfnetSvmLocker {
3366    /// Returns a sender for simulation events from the underlying SVM.
3367    pub fn simnet_events_tx(&self) -> Sender<SimnetEvent> {
3368        self.with_svm_reader(|svm_reader| svm_reader.simnet_events_tx.clone())
3369    }
3370
3371    /// Retrieves the latest epoch info from the underlying SVM.
3372    pub fn get_epoch_info(&self) -> EpochInfo {
3373        self.with_svm_reader(|svm_reader| svm_reader.latest_epoch_info.clone())
3374    }
3375
3376    pub fn time_travel(
3377        &self,
3378        key: Option<(blake3::Hash, String)>,
3379        simnet_command_tx: Sender<SimnetCommand>,
3380        config: TimeTravelConfig,
3381    ) -> SurfpoolResult<EpochInfo> {
3382        let (epoch_info, slot_time, updated_at) = self.with_svm_reader(|svm_reader| {
3383            (
3384                svm_reader.latest_epoch_info.clone(),
3385                svm_reader.slot_time,
3386                svm_reader.updated_at,
3387            )
3388        });
3389
3390        let clock_update: Clock =
3391            calculate_time_travel_clock(&config, updated_at, slot_time, &epoch_info)
3392                .map_err(|e| SurfpoolError::internal(e.to_string()))?;
3393
3394        let formated_time = chrono::DateTime::from_timestamp(clock_update.unix_timestamp, 0)
3395            .unwrap_or_else(|| chrono::DateTime::from_timestamp(0, 0).unwrap())
3396            .format("%Y-%m-%d %H:%M:%S")
3397            .to_string();
3398
3399        // Create a channel for confirmation
3400        let (response_tx, response_rx) = crossbeam_channel::bounded(1);
3401
3402        // Send the command with confirmation
3403        let _ = simnet_command_tx.send(SimnetCommand::UpdateInternalClockWithConfirmation(
3404            key,
3405            clock_update,
3406            response_tx,
3407        ));
3408
3409        // Wait for confirmation with timeout
3410        let updated_epoch_info = response_rx
3411            .recv_timeout(std::time::Duration::from_secs(2))
3412            .map_err(|e| {
3413                SurfpoolError::internal(format!("Failed to confirm clock update: {}", e))
3414            })?;
3415
3416        let _ = self.simnet_events_tx().send(SimnetEvent::info(format!(
3417            "Time travel to {} successful (epoch {} / slot {})",
3418            formated_time, updated_epoch_info.epoch, updated_epoch_info.absolute_slot
3419        )));
3420
3421        Ok(updated_epoch_info)
3422    }
3423
3424    /// Retrieves the latest absolute slot from the underlying SVM.
3425    pub fn get_latest_absolute_slot(&self) -> Slot {
3426        self.with_svm_reader(|svm_reader| svm_reader.get_latest_absolute_slot())
3427    }
3428
3429    /// Retrieves the latest blockhash for the given commitment config from the underlying SVM.
3430    pub fn get_latest_blockhash(&self, config: &CommitmentConfig) -> Option<Hash> {
3431        let slot = self.get_slot_for_commitment(config);
3432        self.with_svm_reader(|svm_reader| svm_reader.blockhash_for_slot(slot))
3433    }
3434
3435    pub fn latest_absolute_blockhash(&self) -> Hash {
3436        self.with_svm_reader(|svm_reader| svm_reader.latest_blockhash())
3437    }
3438
3439    pub fn get_slot_for_commitment(&self, commitment: &CommitmentConfig) -> Slot {
3440        self.with_svm_reader(|svm_reader| {
3441            let slot = svm_reader.get_latest_absolute_slot();
3442            match commitment.commitment {
3443                CommitmentLevel::Processed => slot,
3444                CommitmentLevel::Confirmed => slot.saturating_sub(1),
3445                CommitmentLevel::Finalized => slot.saturating_sub(FINALIZATION_SLOT_THRESHOLD),
3446            }
3447        })
3448    }
3449
3450    /// Executes an airdrop via the underlying SVM.
3451    pub fn airdrop(
3452        &self,
3453        pubkey: &Pubkey,
3454        lamports: u64,
3455    ) -> Result<TransactionResult, AirdropError> {
3456        self.with_svm_writer(|svm_writer| svm_writer.airdrop(pubkey, lamports))
3457    }
3458
3459    /// Executes a batch airdrop via the underlying SVM.
3460    pub fn airdrop_pubkeys(&self, lamports: u64, addresses: &[Pubkey]) {
3461        self.with_svm_writer(|svm_writer| svm_writer.airdrop_pubkeys(lamports, addresses))
3462    }
3463
3464    /// Confirms the current block on the underlying SVM, returning `Ok(())` or an error.
3465    pub async fn confirm_current_block(
3466        &self,
3467        remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>,
3468    ) -> SurfpoolResult<()> {
3469        // Acquire write lock once and do both operations atomically
3470        // This prevents lock contention and potential deadlocks from mixing blocking and async locks
3471        let mut svm_writer = self.0.write().await;
3472        svm_writer.confirm_current_block()?;
3473        svm_writer.materialize_overrides(remote_ctx).await
3474    }
3475
3476    /// Subscribes for signature updates (confirmed/finalized) and returns a receiver of events.
3477    pub fn subscribe_for_signature_updates(
3478        &self,
3479        signature: &Signature,
3480        subscription_type: SignatureSubscriptionType,
3481    ) -> Receiver<(Slot, Option<TransactionError>)> {
3482        self.with_svm_writer(|svm_writer| {
3483            svm_writer.subscribe_for_signature_updates(signature, subscription_type.clone())
3484        })
3485    }
3486
3487    /// Subscribes for account updates and returns a receiver of account updates.
3488    pub fn subscribe_for_account_updates(
3489        &self,
3490        account_pubkey: &Pubkey,
3491        encoding: Option<UiAccountEncoding>,
3492    ) -> Receiver<UiAccount> {
3493        // Handles the locking/unlocking safely
3494        self.with_svm_writer(|svm_writer| {
3495            svm_writer.subscribe_for_account_updates(account_pubkey, encoding)
3496        })
3497    }
3498
3499    /// Subscribes for program account updates and returns a receiver of keyed account updates.
3500    pub fn subscribe_for_program_updates(
3501        &self,
3502        program_id: &Pubkey,
3503        encoding: Option<UiAccountEncoding>,
3504        filters: Option<Vec<RpcFilterType>>,
3505    ) -> Receiver<RpcKeyedAccount> {
3506        self.with_svm_writer(|svm_writer| {
3507            svm_writer.subscribe_for_program_updates(program_id, encoding, filters)
3508        })
3509    }
3510
3511    /// Subscribes for slot updates and returns a receiver of slot updates.
3512    pub fn subscribe_for_slot_updates(&self) -> Receiver<SlotInfo> {
3513        self.with_svm_writer(|svm_writer| svm_writer.subscribe_for_slot_updates())
3514    }
3515
3516    /// Subscribes for tagged `slotsUpdatesSubscribe` notifications and
3517    /// returns a receiver of shared `SlotUpdate` events.
3518    pub fn subscribe_for_slots_updates(&self) -> Receiver<Arc<SlotUpdate>> {
3519        self.with_svm_writer(|svm_writer| svm_writer.subscribe_for_slots_updates())
3520    }
3521
3522    /// Subscribes for logs updates and returns a receiver of logs updates.
3523    pub fn subscribe_for_logs_updates(
3524        &self,
3525        commitment_level: &CommitmentLevel,
3526        filter: &RpcTransactionLogsFilter,
3527    ) -> Receiver<(Slot, RpcLogsResponse)> {
3528        self.with_svm_writer(|svm_writer| {
3529            svm_writer.subscribe_for_logs_updates(commitment_level, filter)
3530        })
3531    }
3532
3533    /// Subscribes for snapshot import updates and returns a receiver of snapshot import notifications.
3534    /// This method spawns a background task that fetches the snapshot and loads it via `load_snapshot`.
3535    pub fn subscribe_for_snapshot_import_updates(
3536        &self,
3537        snapshot_url: &str,
3538        snapshot_id: &str,
3539    ) -> Receiver<super::SnapshotImportNotification> {
3540        // Register the subscription and get the sender/receiver
3541        let (tx, rx) =
3542            self.with_svm_writer(|svm_writer| svm_writer.register_snapshot_subscription());
3543
3544        // Clone the locker for use in the spawned task
3545        let locker = self.clone();
3546        let snapshot_url = snapshot_url.to_string();
3547        let snapshot_id = snapshot_id.to_string();
3548
3549        tokio::spawn(async move {
3550            // Send initial notification
3551            let _ = tx.send(super::SnapshotImportNotification {
3552                snapshot_id: snapshot_id.clone(),
3553                status: super::SnapshotImportStatus::Started,
3554                accounts_loaded: 0,
3555                total_accounts: 0,
3556                error: None,
3557            });
3558
3559            // Fetch snapshot from URL and parse it
3560            let snapshot_data = match SurfnetSvm::fetch_snapshot_from_url(&snapshot_url).await {
3561                Ok(data) => data,
3562                Err(e) => {
3563                    let _ = tx.send(super::SnapshotImportNotification {
3564                        snapshot_id,
3565                        status: super::SnapshotImportStatus::Failed,
3566                        accounts_loaded: 0,
3567                        total_accounts: 0,
3568                        error: Some(format!("Failed to fetch snapshot: {}", e)),
3569                    });
3570                    return;
3571                }
3572            };
3573
3574            let total_accounts = snapshot_data.len() as u64;
3575
3576            // Send progress notification with total count
3577            let _ = tx.send(super::SnapshotImportNotification {
3578                snapshot_id: snapshot_id.clone(),
3579                status: super::SnapshotImportStatus::InProgress,
3580                accounts_loaded: 0,
3581                total_accounts,
3582                error: None,
3583            });
3584
3585            // Load the snapshot using the load_snapshot method
3586            match locker
3587                .load_snapshot(&snapshot_data, None, CommitmentConfig::processed())
3588                .await
3589            {
3590                Ok(loaded_count) => {
3591                    let _ = tx.send(super::SnapshotImportNotification {
3592                        snapshot_id,
3593                        status: super::SnapshotImportStatus::Completed,
3594                        accounts_loaded: loaded_count as u64,
3595                        total_accounts,
3596                        error: None,
3597                    });
3598                }
3599                Err(e) => {
3600                    let _ = tx.send(super::SnapshotImportNotification {
3601                        snapshot_id,
3602                        status: super::SnapshotImportStatus::Failed,
3603                        accounts_loaded: 0,
3604                        total_accounts,
3605                        error: Some(format!("Failed to load snapshot: {}", e)),
3606                    });
3607                }
3608            }
3609        });
3610
3611        rx
3612    }
3613
3614    pub fn runbook_executions(&self) -> Vec<RunbookExecutionStatusReport> {
3615        self.with_svm_reader(|svm_reader| svm_reader.runbook_executions.clone())
3616    }
3617
3618    pub fn start_runbook_execution(&self, runbook_id: String) {
3619        self.with_svm_writer(|svm_writer| {
3620            svm_writer.instruction_profiling_enabled = false;
3621            svm_writer.start_runbook_execution(runbook_id);
3622        });
3623    }
3624
3625    pub fn complete_runbook_execution(
3626        &self,
3627        runbook_id: String,
3628        error: Option<Vec<String>>,
3629        re_enable_ix_profiling: bool,
3630    ) {
3631        self.with_svm_writer(|svm_writer| {
3632            svm_writer.complete_runbook_execution(&runbook_id, error);
3633            let some_runbook_executing = svm_writer
3634                .runbook_executions
3635                .iter()
3636                .any(|e| e.completed_at.is_none());
3637            if !some_runbook_executing && re_enable_ix_profiling {
3638                svm_writer.instruction_profiling_enabled = true;
3639            }
3640        });
3641    }
3642
3643    pub fn export_snapshot(
3644        &self,
3645        config: ExportSnapshotConfig,
3646    ) -> SurfpoolResult<BTreeMap<String, AccountSnapshot>> {
3647        self.with_svm_reader(|svm_reader| svm_reader.export_snapshot(config))
3648    }
3649
3650    pub fn get_start_time(&self) -> SystemTime {
3651        self.with_svm_reader(|svm_reader| svm_reader.start_time)
3652    }
3653}
3654
3655/// Helpers for writing program accounts
3656impl SurfnetSvmLocker {
3657    pub async fn write_program(
3658        &self,
3659        program_id: Pubkey,
3660        authority: Option<Pubkey>,
3661        offset: usize,
3662        data: &[u8],
3663        remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>,
3664    ) -> SurfpoolResult<()> {
3665        let program_data_address = get_program_data_address(&program_id);
3666
3667        let program_account = self
3668            .get_or_create_program_account(program_id, program_data_address, remote_ctx)
3669            .await?;
3670
3671        let _ = self
3672            .write_program_data_account_with_offset(
3673                program_id,
3674                authority,
3675                program_data_address,
3676                offset,
3677                data,
3678                remote_ctx,
3679            )
3680            .await?;
3681
3682        // Re-set the program account to force LiteSVM to recompile the program
3683        // from the updated programdata. Without this, the program cache retains
3684        // the noop placeholder compiled during initial program account creation.
3685        // Errors are expected for incomplete ELF (multi-chunk writes) and are
3686        // logged but not propagated.
3687        let set_result = self.with_svm_writer(|svm_writer| {
3688            svm_writer.set_account(&program_id, program_account.clone())
3689        });
3690        if let Err(e) = set_result {
3691            let _ = self.simnet_events_tx().send(SimnetEvent::info(format!(
3692                "Program cache update deferred for {}: {}",
3693                program_id, e
3694            )));
3695        }
3696
3697        Ok(())
3698    }
3699
3700    pub async fn get_or_create_program_account(
3701        &self,
3702        program_id: Pubkey,
3703        program_data_address: Pubkey,
3704        remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>,
3705    ) -> SurfpoolResult<Account> {
3706        // Get program account
3707        let SvmAccessContext {
3708            inner: program_account_result,
3709            ..
3710        } = self
3711            .get_account(
3712                remote_ctx,
3713                &program_id,
3714                Some(Box::new(move |svm_locker| {
3715                    // Create default program account if it doesn't exist
3716                    let program_state =
3717                        solana_loader_v3_interface::state::UpgradeableLoaderState::Program {
3718                            programdata_address: program_data_address,
3719                        };
3720                    let program_data = bincode::serialize(&program_state)
3721                        .expect("Failed to serialize program state");
3722                    let program_lamports = svm_locker.with_svm_reader(|svm_reader| {
3723                        svm_reader
3724                            .inner
3725                            .minimum_balance_for_rent_exemption(program_data.len())
3726                    });
3727
3728                    let _ = svm_locker
3729                        .simnet_events_tx()
3730                        .send(SimnetEvent::info(format!(
3731                            "Creating program account {} with program data address {}",
3732                            program_id, program_data_address
3733                        )));
3734
3735                    GetAccountResult::FoundAccount(
3736                        program_id,
3737                        solana_account::Account {
3738                            lamports: program_lamports,
3739                            data: program_data,
3740                            owner: solana_sdk_ids::bpf_loader_upgradeable::id(),
3741                            executable: true,
3742                            rent_epoch: 0,
3743                        },
3744                        true,
3745                    )
3746                })),
3747            )
3748            .await?;
3749
3750        // Check if account was created before consuming it
3751        let was_program_created = matches!(
3752            program_account_result,
3753            GetAccountResult::FoundAccount(_, _, true)
3754        );
3755
3756        // Ensure we have a valid program account
3757        let program_account = program_account_result.map_account()?;
3758
3759        // Validate it's owned by the upgradeable loader
3760        if program_account.owner != solana_sdk_ids::bpf_loader_upgradeable::id() {
3761            return Err(SurfpoolError::invalid_program_account(
3762                &program_id,
3763                "Account not owned by the BPF Upgradeable Loader",
3764            ));
3765        }
3766
3767        // Validate it's an executable program account
3768        if !program_account.executable {
3769            return Err(SurfpoolError::invalid_program_account(
3770                &program_id,
3771                "Account not executable",
3772            ));
3773        }
3774
3775        // Persist the program account if it was newly created
3776        if was_program_created {
3777            self.write_account_update(GetAccountResult::FoundAccount(
3778                program_id,
3779                program_account.clone(),
3780                true,
3781            ));
3782        }
3783        Ok(program_account)
3784    }
3785
3786    pub async fn write_program_data_account_with_offset(
3787        &self,
3788        program_id: Pubkey,
3789        authority: Option<Pubkey>,
3790        program_data_address: Pubkey,
3791        offset: usize,
3792        data: &[u8],
3793        remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>,
3794    ) -> SurfpoolResult<Account> {
3795        // Get or create program data account
3796        let SvmAccessContext {
3797            inner: program_data_result,
3798            slot,
3799            ..
3800        } = self
3801            .get_account(
3802                &remote_ctx,
3803                &program_data_address,
3804                Some(Box::new(move |svm_locker| {
3805                    // Create default program data account if it doesn't exist
3806                    let programdata_state =
3807                        solana_loader_v3_interface::state::UpgradeableLoaderState::ProgramData {
3808                            slot: svm_locker.get_latest_absolute_slot(),
3809                            // TODO: currently litesvm breaks if you don't provide an authority,
3810                            // but once that's fixed we should remove the default to system program
3811                            upgrade_authority_address: authority
3812                                .or(Some(solana_system_interface::program::id())),
3813                        };
3814                    let mut programdata_data = bincode::serialize(&programdata_state)
3815                        .expect("Failed to serialize program data state");
3816
3817                    // Add empty program data (will be filled by writes)
3818                    programdata_data.extend(vec![0u8; 0]);
3819
3820                    let programdata_lamports = svm_locker.with_svm_reader(|svm_reader| {
3821                        svm_reader
3822                            .inner
3823                            .minimum_balance_for_rent_exemption(programdata_data.len())
3824                    });
3825
3826                    let _ = svm_locker
3827                        .simnet_events_tx()
3828                        .send(SimnetEvent::info(format!(
3829                            "Creating program data account {} for program {}",
3830                            program_data_address, program_id
3831                        )));
3832
3833                    GetAccountResult::FoundAccount(
3834                        program_data_address,
3835                        solana_account::Account {
3836                            lamports: programdata_lamports,
3837                            data: programdata_data,
3838                            owner: solana_sdk_ids::bpf_loader_upgradeable::id(),
3839                            executable: false,
3840                            rent_epoch: 0,
3841                        },
3842                        true,
3843                    )
3844                })),
3845            )
3846            .await?;
3847
3848        // Get mutable program data account
3849        let mut program_data_account = program_data_result.map_account()?;
3850
3851        // Calculate metadata size
3852        let metadata_size =
3853            solana_loader_v3_interface::state::UpgradeableLoaderState::size_of_programdata_metadata(
3854            );
3855
3856        // Verify program data account has valid state
3857        let upgrade_authority_address = match bincode::deserialize::<
3858            solana_loader_v3_interface::state::UpgradeableLoaderState,
3859        >(&program_data_account.data[..metadata_size])
3860        {
3861            Ok(solana_loader_v3_interface::state::UpgradeableLoaderState::ProgramData {
3862                upgrade_authority_address,
3863                ..
3864            }) => upgrade_authority_address,
3865            Ok(_) => {
3866                return Err(SurfpoolError::invalid_program_data_account(
3867                    program_data_address,
3868                    "Account is not a program data account",
3869                ));
3870            }
3871            Err(e) => {
3872                return Err(SurfpoolError::invalid_program_data_account(
3873                    program_data_address,
3874                    format!("Invalid program data account state: {}", e),
3875                ));
3876            }
3877        };
3878
3879        let new_metadata = if upgrade_authority_address.ne(&authority) {
3880            let _ = self.simnet_events_tx().send(SimnetEvent::info(format!(
3881                "Updating program authority of program {} to {}",
3882                program_id,
3883                authority.unwrap_or(solana_system_interface::program::id())
3884            )));
3885            solana_loader_v3_interface::state::UpgradeableLoaderState::ProgramData {
3886                slot,
3887                upgrade_authority_address: authority
3888                    .or(Some(solana_system_interface::program::id())),
3889            }
3890        } else {
3891            solana_loader_v3_interface::state::UpgradeableLoaderState::ProgramData {
3892                slot,
3893                upgrade_authority_address,
3894            }
3895        };
3896
3897        let metadata_bytes = bincode::serialize(&new_metadata).map_err(|e| {
3898            SurfpoolError::internal(format!("Failed to serialize program data metadata: {}", e))
3899        })?;
3900
3901        // Strip the minimum_program.so placeholder if it was pre-filled by
3902        // init_programdata_account during program account creation. This prevents
3903        // leftover placeholder bytes when the actual program is smaller than 3312 bytes.
3904        let minimum_program_bytes = crate::surfnet::noop_program::NOOP_PROGRAM_ELF;
3905        if program_data_account.data.len() == metadata_size + minimum_program_bytes.len()
3906            && program_data_account.data[metadata_size..] == *minimum_program_bytes
3907        {
3908            program_data_account.data.truncate(metadata_size);
3909        }
3910
3911        // Calculate absolute offset in account data (metadata + offset)
3912        let absolute_offset = metadata_size + offset;
3913        let end_offset = absolute_offset + data.len();
3914
3915        // Expand account data if necessary
3916        if end_offset > program_data_account.data.len() {
3917            let new_size = end_offset;
3918            program_data_account.data.resize(new_size, 0);
3919
3920            // Update lamports for rent exemption
3921            let new_lamports = self.with_svm_reader(|svm_reader| {
3922                svm_reader
3923                    .inner
3924                    .minimum_balance_for_rent_exemption(new_size)
3925            });
3926            program_data_account.lamports = new_lamports;
3927
3928            let _ = self.simnet_events_tx().send(SimnetEvent::info(format!(
3929                "Expanding program data account to {} bytes",
3930                new_size
3931            )));
3932        }
3933
3934        // Write the metadata
3935        program_data_account.data[..metadata_size].copy_from_slice(&metadata_bytes);
3936        // Write data at the specified offset
3937        program_data_account.data[absolute_offset..end_offset].copy_from_slice(&data);
3938
3939        // Update the account in SVM
3940        self.with_svm_writer(|svm_writer| {
3941            svm_writer.set_account(&program_data_address, program_data_account.clone())?;
3942            Ok::<(), SurfpoolError>(())
3943        })?;
3944
3945        let _ = self.simnet_events_tx().send(SimnetEvent::info(format!(
3946            "Wrote {} bytes to program {} at offset {}",
3947            data.len(),
3948            program_id,
3949            offset
3950        )));
3951
3952        Ok(program_data_account)
3953    }
3954}
3955
3956// Helper function to apply filters
3957pub(crate) fn apply_rpc_filters(
3958    account_data: &[u8],
3959    filters: &[RpcFilterType],
3960) -> SurfpoolResult<bool> {
3961    for filter in filters {
3962        match filter {
3963            RpcFilterType::DataSize(size) => {
3964                if account_data.len() as u64 != *size {
3965                    return Ok(false);
3966                }
3967            }
3968            RpcFilterType::Memcmp(memcmp_filter) => {
3969                // Use the public bytes_match method from solana_client::rpc_filter::Memcmp
3970                if !memcmp_filter.bytes_match(account_data) {
3971                    return Ok(false); // Content mismatch or out of bounds handled by bytes_match
3972                }
3973            }
3974            RpcFilterType::TokenAccountState => {
3975                return Err(SurfpoolError::internal(
3976                    "TokenAccountState filter is not supported",
3977                ));
3978            }
3979        }
3980    }
3981    Ok(true)
3982}
3983
3984// used in the remote.rs
3985pub fn is_supported_token_program(program_id: &Pubkey) -> bool {
3986    *program_id == spl_token_interface::ID || *program_id == spl_token_2022_interface::ID
3987}
3988
3989fn update_programdata_account(
3990    program_id: &Pubkey,
3991    programdata_account: &mut Account,
3992    new_authority: Option<Pubkey>,
3993) -> SurfpoolResult<Option<Pubkey>> {
3994    let upgradeable_loader_state =
3995        bincode::deserialize::<UpgradeableLoaderState>(&programdata_account.data).map_err(|e| {
3996            SurfpoolError::invalid_program_account(
3997                program_id,
3998                format!("Failed to serialize program data: {}", e),
3999            )
4000        })?;
4001    if let UpgradeableLoaderState::ProgramData {
4002        upgrade_authority_address,
4003        slot,
4004    } = upgradeable_loader_state
4005    {
4006        let offset = if upgrade_authority_address.is_some() {
4007            UpgradeableLoaderState::size_of_programdata_metadata()
4008        } else {
4009            UpgradeableLoaderState::size_of_programdata_metadata()
4010                - serialized_size(&Pubkey::default()).unwrap() as usize
4011        };
4012
4013        let mut data = bincode::serialize(&UpgradeableLoaderState::ProgramData {
4014            upgrade_authority_address: new_authority,
4015            slot,
4016        })
4017        .map_err(|e| {
4018            SurfpoolError::invalid_program_account(
4019                program_id,
4020                format!("Failed to serialize program data: {}", e),
4021            )
4022        })?;
4023
4024        data.append(&mut programdata_account.data[offset..].to_vec());
4025
4026        programdata_account.data = data;
4027
4028        Ok(upgrade_authority_address)
4029    } else {
4030        Err(SurfpoolError::invalid_program_account(
4031            program_id,
4032            "Invalid program data account",
4033        ))
4034    }
4035}
4036
4037pub fn format_ui_amount_string(amount: u64, decimals: u8) -> String {
4038    if decimals > 0 {
4039        let divisor = 10u64.pow(decimals as u32);
4040        format!(
4041            "{:.decimals$}",
4042            amount as f64 / divisor as f64,
4043            decimals = decimals as usize
4044        )
4045    } else {
4046        amount.to_string()
4047    }
4048}
4049
4050pub fn format_ui_amount(amount: u64, decimals: u8) -> f64 {
4051    if decimals > 0 {
4052        let divisor = 10u64.pow(decimals as u32);
4053        amount as f64 / divisor as f64
4054    } else {
4055        amount as f64
4056    }
4057}
4058
4059#[cfg(test)]
4060mod tests {
4061    use std::collections::HashMap;
4062
4063    use solana_account::Account;
4064    use solana_account_decoder::UiAccountEncoding;
4065    use solana_epoch_schedule::EpochSchedule;
4066    use solana_transaction_status::TransactionStatusMeta;
4067
4068    use super::*;
4069    use crate::{
4070        scenarios::registry::PYTH_V2_IDL_CONTENT,
4071        surfnet::{BlockHeader, SurfnetSvm, svm::apply_override_to_decoded_account},
4072    };
4073
4074    #[test]
4075    fn test_get_forged_account_data_with_pyth_fixture() {
4076        use borsh::{BorshDeserialize, BorshSerialize};
4077
4078        // Define local structures matching Pyth IDL
4079        #[derive(BorshSerialize, BorshDeserialize, Debug, Clone, PartialEq)]
4080        pub enum VerificationLevel {
4081            Partial { num_signatures: u8 },
4082            Full,
4083        }
4084
4085        #[derive(BorshSerialize, BorshDeserialize, Debug, Clone, PartialEq)]
4086        pub struct PriceFeedMessage {
4087            pub feed_id: [u8; 32],
4088            pub price: i64,
4089            pub conf: u64,
4090            pub exponent: i32,
4091            pub publish_time: i64,
4092            pub prev_publish_time: i64,
4093            pub ema_price: i64,
4094            pub ema_conf: u64,
4095        }
4096
4097        #[derive(BorshSerialize, BorshDeserialize, Debug, Clone, PartialEq)]
4098        pub struct PriceUpdateV2 {
4099            pub write_authority: Pubkey,
4100            pub verification_level: VerificationLevel,
4101            pub price_message: PriceFeedMessage,
4102            pub posted_slot: u64,
4103        }
4104
4105        // Pyth price feed account data fixture
4106        let account_data_hex = vec![
4107            0x22, 0xf1, 0x23, 0x63, 0x9d, 0x7e, 0xf4, 0xcd, // Discriminator
4108            0x35, 0xa7, 0x0c, 0x11, 0x16, 0x2f, 0xbf, 0x5a, 0x0e, 0x7f, 0x7d, 0x2f, 0x96, 0xe1,
4109            0x9f, 0x97, 0xb0, 0x22, 0x46, 0xa1, 0x56, 0x87, 0xee, 0x67, 0x27, 0x94, 0x89, 0x74,
4110            0x48, 0xe6, 0x58, 0xde, 0x01, 0xe6, 0x2d, 0xf6, 0xc8, 0xb4, 0xa8, 0x5f, 0xe1, 0xa6,
4111            0x7d, 0xb4, 0x4d, 0xc1, 0x2d, 0xe5, 0xdb, 0x33, 0x0f, 0x7a, 0xc6, 0x6b, 0x72, 0xdc,
4112            0x65, 0x8a, 0xfe, 0xdf, 0x0f, 0x4a, 0x41, 0x5b, 0x43, 0xd7, 0x1f, 0x18, 0x64, 0x5f,
4113            0x0a, 0x00, 0x00, 0x96, 0x67, 0xea, 0xc5, 0x00, 0x00, 0x00, 0x00, 0xf8, 0xff, 0xff,
4114            0xff, 0x5f, 0x2b, 0x00, 0x69, 0x00, 0x00, 0x00, 0x00, 0x5e, 0x2b, 0x00, 0x69, 0x00,
4115            0x00, 0x00, 0x00, 0xa0, 0x7c, 0x1a, 0x38, 0x63, 0x0a, 0x00, 0x00, 0x94, 0xa6, 0xb9,
4116            0xb5, 0x00, 0x00, 0x00, 0x00, 0x8c, 0x5e, 0x6d, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00,
4117        ];
4118
4119        // Create a minimal Pyth IDL for testing
4120        let idl: Idl = serde_json::from_str(PYTH_V2_IDL_CONTENT).expect("Failed to load IDL");
4121
4122        // Create overrides - note: this won't actually work with the JSON deserialization
4123        // since the account data is Borsh-encoded, but we're testing the structure
4124        let mut overrides: HashMap<String, serde_json::Value> = HashMap::new();
4125
4126        // Verify IDL has matching discriminator
4127        let account_def = idl
4128            .accounts
4129            .iter()
4130            .find(|acc| acc.discriminator.eq(&account_data_hex[..8]));
4131
4132        assert!(
4133            account_def.is_some(),
4134            "Should find PriceUpdateV2 account by discriminator"
4135        );
4136        assert_eq!(account_def.unwrap().name, "PriceUpdateV2");
4137
4138        // Step 1: Instantiate an offline Svm instance
4139        let (surfnet_svm, _simnet_events_rx, _geyser_events_rx) = SurfnetSvm::default();
4140        let svm_locker = SurfnetSvmLocker::new(surfnet_svm);
4141
4142        // Step 2: Register the IDL for this account
4143        let account_pubkey = Pubkey::from_str_const("rec5EKMGg6MxZYaMdyBfgwp4d5rB9T1VQH5pJv5LtFJ");
4144        svm_locker.register_idl(idl.clone(), None).unwrap();
4145
4146        // Step 3: Create an account with the Pyth data
4147        let pyth_account = Account {
4148            lamports: 1_000_000,
4149            data: account_data_hex.clone(),
4150            owner: account_pubkey,
4151            executable: false,
4152            rent_epoch: 0,
4153        };
4154
4155        // Step 4: Use encode_ui_account to decode/encode the account data
4156        let ui_account = svm_locker.encode_ui_account(
4157            &account_pubkey,
4158            &pyth_account,
4159            UiAccountEncoding::JsonParsed,
4160            None,
4161            None, // data_slice
4162        );
4163
4164        // Step 5: Verify the UI account has parsed data
4165        println!("UI Account lamports: {}", ui_account.lamports);
4166        println!("UI Account owner: {}", ui_account.owner);
4167
4168        // Assert on parsed account data
4169        use solana_account_decoder::UiAccountData;
4170        match &ui_account.data {
4171            UiAccountData::Json(parsed_account) => {
4172                let parsed_obj = &parsed_account.parsed;
4173
4174                // Extract price_message object
4175                let price_message = parsed_obj
4176                    .get("price_message")
4177                    .expect("Should have price_message field")
4178                    .as_object()
4179                    .expect("price_message should be an object");
4180
4181                // Assert on price
4182                let price = price_message
4183                    .get("price")
4184                    .expect("Should have price field")
4185                    .as_i64()
4186                    .expect("price should be a number");
4187                assert_eq!(price, 11404817473495, "Price should match expected value");
4188
4189                // Assert on exponent
4190                let exponent = price_message
4191                    .get("exponent")
4192                    .expect("Should have exponent field")
4193                    .as_i64()
4194                    .expect("exponent should be a number");
4195                assert_eq!(exponent, -8, "Exponent should be -8");
4196
4197                // Assert on ema_price
4198                let ema_price = price_message
4199                    .get("ema_price")
4200                    .expect("Should have ema_price field")
4201                    .as_i64()
4202                    .expect("ema_price should be a number");
4203                assert_eq!(
4204                    ema_price, 11421259300000,
4205                    "EMA price should match expected value"
4206                );
4207
4208                // Assert on publish_time
4209                let publish_time = price_message
4210                    .get("publish_time")
4211                    .expect("Should have publish_time field")
4212                    .as_i64()
4213                    .expect("publish_time should be a number");
4214                assert_eq!(
4215                    publish_time, 1761618783,
4216                    "Publish time should match expected value"
4217                );
4218
4219                println!("✓ All price assertions passed!");
4220            }
4221            _ => panic!("Expected JSON parsed account data"),
4222        }
4223
4224        // Step 6: Test get_forged_account_data without overrides (should return same data)
4225        println!("\n--- Testing get_forged_account_data without overrides ---");
4226        let forged_data_no_overrides = svm_locker.get_forged_account_data(
4227            &account_pubkey,
4228            &account_data_hex,
4229            &idl,
4230            &overrides,
4231        );
4232
4233        match forged_data_no_overrides {
4234            Ok(data) => {
4235                // If it succeeds, verify the data is unchanged
4236                assert_eq!(
4237                    data, account_data_hex,
4238                    "Data without overrides should match original"
4239                );
4240                println!("✓ Forged data without overrides matches original!");
4241            }
4242            Err(e) => {
4243                // If it fails, it's due to Borsh/JSON mismatch (expected for now)
4244                println!("Expected error (Borsh vs JSON): {:?}", e);
4245                println!("Note: This documents the need for proper Borsh implementation");
4246            }
4247        }
4248
4249        // Step 7: Test get_forged_account_data with overrides
4250        println!("\n--- Testing get_forged_account_data with overrides ---");
4251
4252        // Set new values for price and publish_time
4253        let new_price = 999999999999i64;
4254        let new_publish_time = 1234567890i64;
4255        let new_ema_price = 888888888888i64;
4256
4257        overrides.insert("price_message.price".into(), json!(new_price));
4258        overrides.insert("price_message.publish_time".into(), json!(new_publish_time));
4259        overrides.insert("price_message.ema_price".into(), json!(new_ema_price));
4260
4261        let forged_data_with_overrides = svm_locker.get_forged_account_data(
4262            &account_pubkey,
4263            &account_data_hex,
4264            &idl,
4265            &overrides,
4266        );
4267
4268        match forged_data_with_overrides {
4269            Ok(modified_data) => {
4270                // Verify the data is different from original
4271                assert_ne!(
4272                    modified_data, account_data_hex,
4273                    "Modified data should be different from original"
4274                );
4275                println!("✓ Modified data is different from original!");
4276
4277                // Create a modified account to verify the changes
4278                let modified_account = Account {
4279                    lamports: 1_000_000,
4280                    data: modified_data.clone(),
4281                    owner: account_pubkey,
4282                    executable: false,
4283                    rent_epoch: 0,
4284                };
4285
4286                // Re-encode the modified account to verify the changes
4287                let modified_ui_account = svm_locker.encode_ui_account(
4288                    &account_pubkey,
4289                    &modified_account,
4290                    UiAccountEncoding::JsonParsed,
4291                    None,
4292                    None,
4293                );
4294
4295                // Verify the modified values in the re-encoded account
4296                match &modified_ui_account.data {
4297                    UiAccountData::Json(parsed_account) => {
4298                        let parsed_obj = &parsed_account.parsed;
4299                        let price_message = parsed_obj
4300                            .get("price_message")
4301                            .expect("Should have price_message field")
4302                            .as_object()
4303                            .expect("price_message should be an object");
4304
4305                        // Verify new price
4306                        let modified_price = price_message
4307                            .get("price")
4308                            .expect("Should have price field")
4309                            .as_i64()
4310                            .expect("price should be a number");
4311                        assert_eq!(
4312                            modified_price, new_price,
4313                            "Modified price should match override value"
4314                        );
4315
4316                        // Verify new publish_time
4317                        let modified_publish_time = price_message
4318                            .get("publish_time")
4319                            .expect("Should have publish_time field")
4320                            .as_i64()
4321                            .expect("publish_time should be a number");
4322                        assert_eq!(
4323                            modified_publish_time, new_publish_time,
4324                            "Modified publish_time should match override value"
4325                        );
4326
4327                        // Verify new ema_price
4328                        let modified_ema_price = price_message
4329                            .get("ema_price")
4330                            .expect("Should have ema_price field")
4331                            .as_i64()
4332                            .expect("ema_price should be a number");
4333                        assert_eq!(
4334                            modified_ema_price, new_ema_price,
4335                            "Modified ema_price should match override value"
4336                        );
4337
4338                        // Verify exponent is unchanged
4339                        let exponent = price_message
4340                            .get("exponent")
4341                            .expect("Should have exponent field")
4342                            .as_i64()
4343                            .expect("exponent should be a number");
4344                        assert_eq!(exponent, -8, "Exponent should remain unchanged");
4345
4346                        println!("✓ All override assertions passed!");
4347                        println!("  - Price changed: 11404817473495 → {}", new_price);
4348                        println!(
4349                            "  - Publish time changed: 1761618783 → {}",
4350                            new_publish_time
4351                        );
4352                        println!("  - EMA price changed: 11421259300000 → {}", new_ema_price);
4353                        println!("  - Exponent unchanged: -8");
4354                    }
4355                    _ => panic!("Expected JSON parsed account data for modified account"),
4356                }
4357            }
4358            Err(e) => {
4359                // If it fails, it's due to Borsh/JSON mismatch (expected for now)
4360                println!("Expected error (Borsh vs JSON): {:?}", e);
4361                println!("Note: Once Borsh serialization is implemented, this test will:");
4362                println!("  1. Successfully modify the account data");
4363                println!("  2. Verify price changed to: {}", new_price);
4364                println!("  3. Verify publish_time changed to: {}", new_publish_time);
4365                println!("  4. Verify ema_price changed to: {}", new_ema_price);
4366                println!("  5. Verify other fields remain unchanged");
4367            }
4368        }
4369
4370        // Step 8: Demonstrate proper Borsh deserialization/serialization
4371        println!("\n--- Step 8: Testing with Borsh structures ---");
4372
4373        // Deserialize the original account data using Borsh
4374        let account_bytes = &account_data_hex[8..];
4375        println!(
4376            "Account data length (without discriminator): {} bytes",
4377            account_bytes.len()
4378        );
4379
4380        let mut reader = std::io::Cursor::new(account_bytes);
4381        let original_price_update = PriceUpdateV2::deserialize_reader(&mut reader)
4382            .expect("Should deserialize Pyth account data with Borsh");
4383
4384        let bytes_read = reader.position() as usize;
4385        println!("Bytes read by Borsh: {}", bytes_read);
4386        if bytes_read < account_bytes.len() {
4387            println!(
4388                "Note: {} extra bytes at end (likely padding)",
4389                account_bytes.len() - bytes_read
4390            );
4391        }
4392
4393        println!("Original Borsh-deserialized data:");
4394        println!("  - Price: {}", original_price_update.price_message.price);
4395        println!(
4396            "  - Exponent: {}",
4397            original_price_update.price_message.exponent
4398        );
4399        println!(
4400            "  - EMA Price: {}",
4401            original_price_update.price_message.ema_price
4402        );
4403        println!(
4404            "  - Publish time: {}",
4405            original_price_update.price_message.publish_time
4406        );
4407
4408        // Assert original values match what we saw in JSON parsing
4409        assert_eq!(
4410            original_price_update.price_message.price, 11404817473495,
4411            "Borsh price should match JSON parsed value"
4412        );
4413        assert_eq!(
4414            original_price_update.price_message.exponent, -8,
4415            "Borsh exponent should match JSON parsed value"
4416        );
4417        assert_eq!(
4418            original_price_update.price_message.ema_price, 11421259300000,
4419            "Borsh ema_price should match JSON parsed value"
4420        );
4421        assert_eq!(
4422            original_price_update.price_message.publish_time, 1761618783,
4423            "Borsh publish_time should match JSON parsed value"
4424        );
4425
4426        println!("✓ Borsh deserialization matches JSON parsing!");
4427
4428        // Step 9: Modify and re-serialize with Borsh
4429        println!("\n--- Step 9: Modifying account data with Borsh ---");
4430
4431        let mut modified_price_update = original_price_update.clone();
4432        modified_price_update.price_message.price = new_price;
4433        modified_price_update.price_message.publish_time = new_publish_time;
4434        modified_price_update.price_message.ema_price = new_ema_price;
4435
4436        // Serialize back to bytes
4437        let modified_account_data =
4438            borsh::to_vec(&modified_price_update).expect("Should serialize modified data");
4439
4440        // Prepend the discriminator
4441        let mut full_modified_data = account_data_hex[..8].to_vec();
4442        full_modified_data.extend_from_slice(&modified_account_data);
4443
4444        println!("Modified Borsh-serialized data:");
4445        println!(
4446            "  - Price: {} → {}",
4447            original_price_update.price_message.price, new_price
4448        );
4449        println!(
4450            "  - Publish time: {} → {}",
4451            original_price_update.price_message.publish_time, new_publish_time
4452        );
4453        println!(
4454            "  - EMA Price: {} → {}",
4455            original_price_update.price_message.ema_price, new_ema_price
4456        );
4457        println!(
4458            "  - Exponent: {} (unchanged)",
4459            modified_price_update.price_message.exponent
4460        );
4461
4462        // Verify the modified data is different
4463        assert_ne!(
4464            full_modified_data, account_data_hex,
4465            "Modified data should differ from original"
4466        );
4467
4468        // Verify we can deserialize the modified data back
4469        let mut modified_reader = std::io::Cursor::new(&full_modified_data[8..]);
4470        let reloaded_price_update = PriceUpdateV2::deserialize_reader(&mut modified_reader)
4471            .expect("Should deserialize modified data");
4472
4473        assert_eq!(
4474            reloaded_price_update.price_message.price, new_price,
4475            "Reloaded price should match modified value"
4476        );
4477        assert_eq!(
4478            reloaded_price_update.price_message.publish_time, new_publish_time,
4479            "Reloaded publish_time should match modified value"
4480        );
4481        assert_eq!(
4482            reloaded_price_update.price_message.ema_price, new_ema_price,
4483            "Reloaded ema_price should match modified value"
4484        );
4485        assert_eq!(
4486            reloaded_price_update.price_message.exponent,
4487            original_price_update.price_message.exponent,
4488            "Exponent should remain unchanged"
4489        );
4490
4491        println!("✓ Borsh round-trip successful!");
4492
4493        // Step 10: Verify with encode_ui_account
4494        println!("\n--- Step 10: Verify modified data with encode_ui_account ---");
4495
4496        let modified_test_account = Account {
4497            lamports: 1_000_000,
4498            data: full_modified_data,
4499            owner: account_pubkey,
4500            executable: false,
4501            rent_epoch: 0,
4502        };
4503
4504        let modified_ui_account = svm_locker.encode_ui_account(
4505            &account_pubkey,
4506            &modified_test_account,
4507            UiAccountEncoding::JsonParsed,
4508            None,
4509            None,
4510        );
4511
4512        // Verify through JSON encoding as well
4513        match &modified_ui_account.data {
4514            UiAccountData::Json(parsed_account) => {
4515                let parsed_obj = &parsed_account.parsed;
4516                let price_message = parsed_obj
4517                    .get("price_message")
4518                    .expect("Should have price_message")
4519                    .as_object()
4520                    .expect("Should be object");
4521
4522                let final_price = price_message
4523                    .get("price")
4524                    .expect("Should have price")
4525                    .as_i64()
4526                    .expect("Should be i64");
4527                let final_publish_time = price_message
4528                    .get("publish_time")
4529                    .expect("Should have publish_time")
4530                    .as_i64()
4531                    .expect("Should be i64");
4532                let final_ema_price = price_message
4533                    .get("ema_price")
4534                    .expect("Should have ema_price")
4535                    .as_i64()
4536                    .expect("Should be i64");
4537
4538                assert_eq!(
4539                    final_price, new_price,
4540                    "JSON-parsed price should match Borsh value"
4541                );
4542                assert_eq!(
4543                    final_publish_time, new_publish_time,
4544                    "JSON-parsed publish_time should match Borsh value"
4545                );
4546                assert_eq!(
4547                    final_ema_price, new_ema_price,
4548                    "JSON-parsed ema_price should match Borsh value"
4549                );
4550            }
4551            _ => panic!("Expected JSON parsed data"),
4552        }
4553    }
4554
4555    #[test]
4556    fn test_apply_override_to_decoded_account() {
4557        use txtx_addon_kit::{indexmap::IndexMap, types::types::Value};
4558
4559        // Create a txtx Value object
4560        let mut price_message_obj = IndexMap::new();
4561        price_message_obj.insert("price".to_string(), Value::Integer(100));
4562        price_message_obj.insert("publish_time".to_string(), Value::Integer(1234567890));
4563
4564        let mut decoded_value = IndexMap::new();
4565        decoded_value.insert(
4566            "price_message".to_string(),
4567            Value::Object(price_message_obj),
4568        );
4569        decoded_value.insert("expo".to_string(), Value::Integer(-8));
4570
4571        let mut decoded_value = Value::Object(decoded_value);
4572
4573        // Test simple override
4574        let result =
4575            apply_override_to_decoded_account(&mut decoded_value, "expo", &serde_json::json!(-6));
4576        assert!(result.is_ok());
4577        match &decoded_value {
4578            Value::Object(map) => {
4579                assert_eq!(map.get("expo"), Some(&Value::Integer(-6)));
4580            }
4581            _ => panic!("Expected Object"),
4582        }
4583
4584        // Test nested override
4585        let result = apply_override_to_decoded_account(
4586            &mut decoded_value,
4587            "price_message.price",
4588            &serde_json::json!(200),
4589        );
4590        assert!(result.is_ok());
4591        match &decoded_value {
4592            Value::Object(map) => match map.get("price_message") {
4593                Some(Value::Object(price_msg)) => {
4594                    assert_eq!(price_msg.get("price"), Some(&Value::Integer(200)));
4595                }
4596                _ => panic!("Expected price_message to be Object"),
4597            },
4598            _ => panic!("Expected Object"),
4599        }
4600
4601        // Test invalid path
4602        let result = apply_override_to_decoded_account(
4603            &mut decoded_value,
4604            "nonexistent.field",
4605            &serde_json::json!(999),
4606        );
4607        assert!(result.is_err());
4608    }
4609
4610    #[tokio::test(flavor = "multi_thread")]
4611    async fn test_v0_transaction_without_alt_emits_geyser_account_updates() {
4612        use std::time::Duration;
4613
4614        use crossbeam_channel::{RecvTimeoutError, unbounded};
4615        use solana_keypair::Keypair;
4616        use solana_message::{VersionedMessage, v0};
4617        use solana_signer::Signer;
4618        use solana_system_interface::instruction as system_instruction;
4619        use solana_transaction::versioned::VersionedTransaction;
4620
4621        let (svm, _events_rx, geyser_rx) = SurfnetSvm::default();
4622        let locker = SurfnetSvmLocker::new(svm);
4623
4624        let payer = Keypair::new();
4625        let payer_pubkey = payer.pubkey();
4626        let recipient = Pubkey::new_unique();
4627
4628        let _ = locker
4629            .airdrop(&payer_pubkey, 1_000_000_000)
4630            .expect("airdrop should succeed");
4631
4632        let recent_blockhash = locker.latest_absolute_blockhash();
4633        let message = v0::Message::try_compile(
4634            &payer_pubkey,
4635            &[system_instruction::transfer(
4636                &payer_pubkey,
4637                &recipient,
4638                1_000_000,
4639            )],
4640            &[],
4641            recent_blockhash,
4642        )
4643        .expect("v0 message should compile");
4644
4645        let tx =
4646            VersionedTransaction::try_new(VersionedMessage::V0(message), &[payer.insecure_clone()])
4647                .expect("v0 transaction should sign");
4648
4649        let tx_signature = tx.signatures[0];
4650        let (status_tx, _status_rx) = unbounded();
4651        locker
4652            .process_transaction(&None, tx, status_tx, true, true)
4653            .await
4654            .expect("transaction processing should succeed");
4655
4656        let mut account_updates = vec![];
4657        let mut got_transaction_notify = false;
4658
4659        for _ in 0..32 {
4660            match geyser_rx.recv_timeout(Duration::from_millis(50)) {
4661                Ok(crate::surfnet::GeyserEvent::UpdateAccount(update)) => {
4662                    account_updates.push(update);
4663                }
4664                Ok(crate::surfnet::GeyserEvent::NotifyTransaction(_, _)) => {
4665                    got_transaction_notify = true;
4666                }
4667                Ok(_) => {}
4668                Err(RecvTimeoutError::Timeout) | Err(RecvTimeoutError::Disconnected) => break,
4669            }
4670        }
4671
4672        assert!(
4673            got_transaction_notify,
4674            "Expected NotifyTransaction geyser event"
4675        );
4676        assert!(
4677            !account_updates.is_empty(),
4678            "Expected account update geyser events for v0 transaction without ALTs"
4679        );
4680        assert!(
4681            account_updates.iter().any(|u| u.pubkey == payer_pubkey),
4682            "Expected payer account update"
4683        );
4684        assert!(
4685            account_updates.iter().any(|u| u.pubkey == recipient),
4686            "Expected recipient account update"
4687        );
4688
4689        for update in account_updates {
4690            let sanitized_transaction = update
4691                .sanitized_transaction
4692                .expect("Expected sanitized transaction on account update");
4693            assert_eq!(
4694                *sanitized_transaction.signature(),
4695                tx_signature,
4696                "Account update should carry transaction signature"
4697            );
4698        }
4699    }
4700
4701    #[tokio::test(flavor = "multi_thread")]
4702    async fn test_get_signatures_for_address_local_populates_memo_and_block_time() {
4703        use std::str::FromStr;
4704
4705        use crossbeam_channel::unbounded;
4706        use solana_instruction::Instruction;
4707        use solana_keypair::Keypair;
4708        use solana_message::{VersionedMessage, v0};
4709        use solana_signer::Signer;
4710        use solana_system_interface::instruction as system_instruction;
4711        use solana_transaction::versioned::VersionedTransaction;
4712
4713        let (svm, _events_rx, _geyser_rx) = SurfnetSvm::default();
4714        let locker = SurfnetSvmLocker::new(svm);
4715
4716        let payer = Keypair::new();
4717        let payer_pubkey = payer.pubkey();
4718        let recipient = Pubkey::new_unique();
4719
4720        let _ = locker
4721            .airdrop(&payer_pubkey, 1_000_000_000)
4722            .expect("airdrop should succeed");
4723
4724        // SPL Memo v3 program id — the one `solana ... --with-memo` emits and the one
4725        // `extract_and_fmt_memos` recognizes.
4726        let memo_program = Pubkey::from_str("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr").unwrap();
4727
4728        // Helper: build, sign, and process a v0 transaction made of the given instructions.
4729        async fn process(
4730            locker: &SurfnetSvmLocker,
4731            payer: &Keypair,
4732            instructions: &[Instruction],
4733        ) -> String {
4734            let blockhash = locker.latest_absolute_blockhash();
4735            let message = v0::Message::try_compile(&payer.pubkey(), instructions, &[], blockhash)
4736                .expect("v0 message should compile");
4737            let tx = VersionedTransaction::try_new(
4738                VersionedMessage::V0(message),
4739                &[payer.insecure_clone()],
4740            )
4741            .expect("v0 transaction should sign");
4742            let signature = tx.signatures[0].to_string();
4743            let (status_tx, _status_rx) = unbounded();
4744            locker
4745                .process_transaction(&None, tx, status_tx, true, true)
4746                .await
4747                .expect("transaction processing should succeed");
4748            signature
4749        }
4750
4751        // tx A: transfer + memo instruction.
4752        let sig_with_memo = process(
4753            &locker,
4754            &payer,
4755            &[
4756                system_instruction::transfer(&payer_pubkey, &recipient, 1_000_000),
4757                Instruction {
4758                    program_id: memo_program,
4759                    accounts: vec![],
4760                    data: b"hello-memo".to_vec(),
4761                },
4762            ],
4763        )
4764        .await;
4765
4766        // tx B: plain transfer, no memo (negative control).
4767        let sig_without_memo = process(
4768            &locker,
4769            &payer,
4770            &[system_instruction::transfer(
4771                &payer_pubkey,
4772                &recipient,
4773                1_000_000,
4774            )],
4775        )
4776        .await;
4777
4778        let SvmAccessContext { inner: sigs, .. } =
4779            locker.get_signatures_for_address_local(&payer_pubkey, None);
4780
4781        let row_with_memo = sigs
4782            .iter()
4783            .find(|s| s.signature == sig_with_memo)
4784            .expect("memo-bearing tx should appear in getSignaturesForAddress");
4785        let row_without_memo = sigs
4786            .iter()
4787            .find(|s| s.signature == sig_without_memo)
4788            .expect("plain tx should appear in getSignaturesForAddress");
4789
4790        assert_eq!(
4791            row_with_memo.memo,
4792            Some("[10] hello-memo".to_string()),
4793            "memo-bearing tx should expose the Agave-formatted memo summary"
4794        );
4795        assert_eq!(
4796            row_without_memo.memo, None,
4797            "plain transfer must still yield a null memo"
4798        );
4799
4800        // block_time is synthesized from the slot and must match `getBlockTime`.
4801        // `calculate_block_time_for_slot` returns milliseconds; the summary field is
4802        // `UnixTimestamp` seconds, so it is scaled down by 1000.
4803        for row in [row_with_memo, row_without_memo] {
4804            let expected = (locker.with_svm_reader(|r| r.calculate_block_time_for_slot(row.slot))
4805                / 1_000) as i64;
4806            assert_eq!(
4807                row.block_time,
4808                Some(expected),
4809                "block_time should be the slot's synthetic unix-seconds time"
4810            );
4811        }
4812    }
4813
4814    // Snapshot loading tests
4815
4816    #[tokio::test(flavor = "multi_thread")]
4817    async fn test_load_snapshot_basic() {
4818        use base64::{Engine, engine::general_purpose};
4819
4820        let (svm, _events_rx, _geyser_rx) = SurfnetSvm::default();
4821        let locker = SurfnetSvmLocker::new(svm);
4822
4823        let pubkey = Pubkey::new_unique();
4824        let owner = Pubkey::new_unique();
4825        let data = vec![1, 2, 3, 4, 5];
4826        let data_base64 = general_purpose::STANDARD.encode(&data);
4827
4828        let mut snapshot = BTreeMap::new();
4829        snapshot.insert(
4830            pubkey.to_string(),
4831            Some(AccountSnapshot {
4832                lamports: 1_000_000,
4833                owner: owner.to_string(),
4834                executable: false,
4835                rent_epoch: 0,
4836                data: data_base64,
4837                parsed_data: None,
4838            }),
4839        );
4840
4841        let loaded = locker
4842            .load_snapshot(&snapshot, None, CommitmentConfig::confirmed())
4843            .await
4844            .unwrap();
4845        assert_eq!(loaded, 1);
4846
4847        let account = locker
4848            .with_svm_reader(|svm| svm.get_account(&pubkey))
4849            .unwrap();
4850        assert!(account.is_some());
4851        let account = account.unwrap();
4852        assert_eq!(account.lamports, 1_000_000);
4853        assert_eq!(account.owner, owner);
4854        assert_eq!(account.data, data);
4855        assert!(!account.executable);
4856    }
4857
4858    #[tokio::test(flavor = "multi_thread")]
4859    async fn test_load_snapshot_multiple_accounts() {
4860        use base64::{Engine, engine::general_purpose};
4861
4862        let (svm, _events_rx, _geyser_rx) = SurfnetSvm::default();
4863        let locker = SurfnetSvmLocker::new(svm);
4864
4865        let owner = Pubkey::new_unique();
4866        let mut snapshot = BTreeMap::new();
4867
4868        // Add 5 accounts
4869        let pubkeys: Vec<Pubkey> = (0..5).map(|_| Pubkey::new_unique()).collect();
4870        for (i, pubkey) in pubkeys.iter().enumerate() {
4871            let data = vec![i as u8; 10];
4872            snapshot.insert(
4873                pubkey.to_string(),
4874                Some(AccountSnapshot {
4875                    lamports: (i as u64 + 1) * 1_000_000,
4876                    owner: owner.to_string(),
4877                    executable: false,
4878                    rent_epoch: 0,
4879                    data: general_purpose::STANDARD.encode(&data),
4880                    parsed_data: None,
4881                }),
4882            );
4883        }
4884
4885        let loaded = locker
4886            .load_snapshot(&snapshot, None, CommitmentConfig::confirmed())
4887            .await
4888            .unwrap();
4889        assert_eq!(loaded, 5);
4890
4891        // Verify all accounts were loaded
4892        for (i, pubkey) in pubkeys.iter().enumerate() {
4893            let account = locker
4894                .with_svm_reader(|svm| svm.get_account(pubkey))
4895                .unwrap()
4896                .unwrap();
4897            assert_eq!(account.lamports, (i as u64 + 1) * 1_000_000);
4898            assert_eq!(account.owner, owner);
4899            assert_eq!(account.data, vec![i as u8; 10]);
4900        }
4901    }
4902
4903    #[tokio::test(flavor = "multi_thread")]
4904    async fn test_load_snapshot_skips_none_without_remote() {
4905        use base64::{Engine, engine::general_purpose};
4906
4907        let (svm, _events_rx, _geyser_rx) = SurfnetSvm::default();
4908        let locker = SurfnetSvmLocker::new(svm);
4909
4910        let pubkey1 = Pubkey::new_unique();
4911        let pubkey2 = Pubkey::new_unique();
4912        let owner = Pubkey::new_unique();
4913
4914        let mut snapshot = BTreeMap::new();
4915
4916        // Add one real account
4917        snapshot.insert(
4918            pubkey1.to_string(),
4919            Some(AccountSnapshot {
4920                lamports: 1_000_000,
4921                owner: owner.to_string(),
4922                executable: false,
4923                rent_epoch: 0,
4924                data: general_purpose::STANDARD.encode(&[1, 2, 3]),
4925                parsed_data: None,
4926            }),
4927        );
4928
4929        // Add one None account (should be skipped without remote client)
4930        snapshot.insert(pubkey2.to_string(), None);
4931
4932        let loaded = locker
4933            .load_snapshot(&snapshot, None, CommitmentConfig::confirmed())
4934            .await
4935            .unwrap();
4936        assert_eq!(loaded, 1);
4937
4938        // First account should exist
4939        assert!(
4940            locker
4941                .with_svm_reader(|svm| svm.get_account(&pubkey1))
4942                .unwrap()
4943                .is_some()
4944        );
4945
4946        // Second account should not exist (no remote client to fetch it)
4947        assert!(
4948            locker
4949                .with_svm_reader(|svm| svm.get_account(&pubkey2))
4950                .unwrap()
4951                .is_none()
4952        );
4953    }
4954
4955    #[tokio::test(flavor = "multi_thread")]
4956    async fn test_load_snapshot_invalid_pubkey() {
4957        use base64::{Engine, engine::general_purpose};
4958
4959        let (svm, _events_rx, _geyser_rx) = SurfnetSvm::default();
4960        let locker = SurfnetSvmLocker::new(svm);
4961
4962        let owner = Pubkey::new_unique();
4963        let mut snapshot = BTreeMap::new();
4964
4965        // Add an invalid pubkey
4966        snapshot.insert(
4967            "invalid_pubkey".to_string(),
4968            Some(AccountSnapshot {
4969                lamports: 1_000_000,
4970                owner: owner.to_string(),
4971                executable: false,
4972                rent_epoch: 0,
4973                data: general_purpose::STANDARD.encode(&[1, 2, 3]),
4974                parsed_data: None,
4975            }),
4976        );
4977
4978        // Should succeed but load 0 accounts (invalid pubkey is skipped)
4979        let loaded = locker
4980            .load_snapshot(&snapshot, None, CommitmentConfig::confirmed())
4981            .await
4982            .unwrap();
4983        assert_eq!(loaded, 0);
4984    }
4985
4986    #[tokio::test(flavor = "multi_thread")]
4987    async fn test_load_snapshot_invalid_base64_data() {
4988        let (svm, _events_rx, _geyser_rx) = SurfnetSvm::default();
4989        let locker = SurfnetSvmLocker::new(svm);
4990
4991        let pubkey = Pubkey::new_unique();
4992        let owner = Pubkey::new_unique();
4993        let mut snapshot = BTreeMap::new();
4994
4995        // Add account with invalid base64 data
4996        snapshot.insert(
4997            pubkey.to_string(),
4998            Some(AccountSnapshot {
4999                lamports: 1_000_000,
5000                owner: owner.to_string(),
5001                executable: false,
5002                rent_epoch: 0,
5003                data: "not_valid_base64!!!".to_string(),
5004                parsed_data: None,
5005            }),
5006        );
5007
5008        // Should succeed but load 0 accounts (invalid data is skipped)
5009        let loaded = locker
5010            .load_snapshot(&snapshot, None, CommitmentConfig::confirmed())
5011            .await
5012            .unwrap();
5013        assert_eq!(loaded, 0);
5014
5015        // Account should not exist
5016        assert!(
5017            locker
5018                .with_svm_reader(|svm| svm.get_account(&pubkey))
5019                .unwrap()
5020                .is_none()
5021        );
5022    }
5023
5024    #[tokio::test(flavor = "multi_thread")]
5025    async fn test_load_snapshot_invalid_owner() {
5026        use base64::{Engine, engine::general_purpose};
5027
5028        let (svm, _events_rx, _geyser_rx) = SurfnetSvm::default();
5029        let locker = SurfnetSvmLocker::new(svm);
5030
5031        let pubkey = Pubkey::new_unique();
5032        let mut snapshot = BTreeMap::new();
5033
5034        // Add account with invalid owner pubkey
5035        snapshot.insert(
5036            pubkey.to_string(),
5037            Some(AccountSnapshot {
5038                lamports: 1_000_000,
5039                owner: "invalid_owner".to_string(),
5040                executable: false,
5041                rent_epoch: 0,
5042                data: general_purpose::STANDARD.encode(&[1, 2, 3]),
5043                parsed_data: None,
5044            }),
5045        );
5046
5047        // Should succeed but load 0 accounts (invalid owner is skipped)
5048        let loaded = locker
5049            .load_snapshot(&snapshot, None, CommitmentConfig::confirmed())
5050            .await
5051            .unwrap();
5052        assert_eq!(loaded, 0);
5053    }
5054
5055    #[tokio::test(flavor = "multi_thread")]
5056    async fn test_load_snapshot_empty() {
5057        let (svm, _events_rx, _geyser_rx) = SurfnetSvm::default();
5058        let locker = SurfnetSvmLocker::new(svm);
5059
5060        let snapshot = BTreeMap::new();
5061        let loaded = locker
5062            .load_snapshot(&snapshot, None, CommitmentConfig::confirmed())
5063            .await
5064            .unwrap();
5065        assert_eq!(loaded, 0);
5066    }
5067
5068    #[tokio::test(flavor = "multi_thread")]
5069    async fn test_load_snapshot_updates_account_registries() {
5070        use base64::{Engine, engine::general_purpose};
5071
5072        let (svm, _events_rx, _geyser_rx) = SurfnetSvm::default();
5073        let locker = SurfnetSvmLocker::new(svm);
5074
5075        let pubkey = Pubkey::new_unique();
5076        let owner = Pubkey::new_unique();
5077
5078        let mut snapshot = BTreeMap::new();
5079        snapshot.insert(
5080            pubkey.to_string(),
5081            Some(AccountSnapshot {
5082                lamports: 1_000_000,
5083                owner: owner.to_string(),
5084                executable: false,
5085                rent_epoch: 0,
5086                data: general_purpose::STANDARD.encode(&[1, 2, 3]),
5087                parsed_data: None,
5088            }),
5089        );
5090
5091        locker
5092            .load_snapshot(&snapshot, None, CommitmentConfig::confirmed())
5093            .await
5094            .unwrap();
5095
5096        // Verify account is in the owner index
5097        let owned_accounts = locker
5098            .with_svm_reader(|svm| svm.get_account_owned_by(&owner))
5099            .unwrap();
5100        assert_eq!(owned_accounts.len(), 1);
5101        assert_eq!(owned_accounts[0].0, pubkey);
5102    }
5103
5104    #[tokio::test(flavor = "multi_thread")]
5105    async fn test_load_snapshot_mixed_valid_invalid() {
5106        use base64::{Engine, engine::general_purpose};
5107
5108        let (svm, _events_rx, _geyser_rx) = SurfnetSvm::default();
5109        let locker = SurfnetSvmLocker::new(svm);
5110
5111        let valid_pubkey = Pubkey::new_unique();
5112        let owner = Pubkey::new_unique();
5113
5114        let mut snapshot = BTreeMap::new();
5115
5116        // Valid account
5117        snapshot.insert(
5118            valid_pubkey.to_string(),
5119            Some(AccountSnapshot {
5120                lamports: 1_000_000,
5121                owner: owner.to_string(),
5122                executable: false,
5123                rent_epoch: 0,
5124                data: general_purpose::STANDARD.encode(&[1, 2, 3]),
5125                parsed_data: None,
5126            }),
5127        );
5128
5129        // Invalid pubkey
5130        snapshot.insert(
5131            "bad_pubkey".to_string(),
5132            Some(AccountSnapshot {
5133                lamports: 2_000_000,
5134                owner: owner.to_string(),
5135                executable: false,
5136                rent_epoch: 0,
5137                data: general_purpose::STANDARD.encode(&[4, 5, 6]),
5138                parsed_data: None,
5139            }),
5140        );
5141
5142        // None value (skipped without remote)
5143        snapshot.insert(Pubkey::new_unique().to_string(), None);
5144
5145        let loaded = locker
5146            .load_snapshot(&snapshot, None, CommitmentConfig::confirmed())
5147            .await
5148            .unwrap();
5149        assert_eq!(loaded, 1);
5150
5151        // Only the valid account should exist
5152        assert!(
5153            locker
5154                .with_svm_reader(|svm| svm.get_account(&valid_pubkey))
5155                .unwrap()
5156                .is_some()
5157        );
5158    }
5159
5160    /// Helper: create a VersionedTransaction with a given signature whose account keys contain `pubkey`.
5161    fn make_test_tx(sig: Signature, pubkey: &Pubkey) -> VersionedTransaction {
5162        use solana_system_interface::instruction as system_instruction;
5163        VersionedTransaction {
5164            signatures: vec![sig],
5165            message: VersionedMessage::Legacy(Message::new(
5166                &[system_instruction::transfer(pubkey, pubkey, 1)],
5167                Some(pubkey),
5168            )),
5169        }
5170    }
5171
5172    /// Helper: store a transaction into the SVM at the given slot.
5173    fn store_test_tx(svm: &mut SurfnetSvm, sig: Signature, pubkey: &Pubkey, slot: u64) {
5174        let tx = make_test_tx(sig, pubkey);
5175        svm.transactions
5176            .store(
5177                sig.to_string(),
5178                SurfnetTransactionStatus::processed(
5179                    TransactionWithStatusMeta {
5180                        slot,
5181                        transaction: tx,
5182                        meta: TransactionStatusMeta {
5183                            status: Ok(()),
5184                            fee: 5000,
5185                            pre_balances: vec![0; 3],
5186                            post_balances: vec![0; 3],
5187                            inner_instructions: Some(vec![]),
5188                            log_messages: Some(vec![]),
5189                            pre_token_balances: Some(vec![]),
5190                            post_token_balances: Some(vec![]),
5191                            rewards: Some(vec![]),
5192                            loaded_addresses: LoadedAddresses::default(),
5193                            return_data: None,
5194                            compute_units_consumed: Some(0),
5195                            cost_units: None,
5196                        },
5197                    },
5198                    HashSet::new(),
5199                ),
5200            )
5201            .unwrap();
5202    }
5203
5204    fn seed_signature_history(
5205        locker: &SurfnetSvmLocker,
5206        pubkey: &Pubkey,
5207        blocks: &[(u64, Vec<Signature>)],
5208    ) {
5209        locker.with_svm_writer(|svm| {
5210            for (slot, signatures) in blocks {
5211                for sig in signatures {
5212                    store_test_tx(svm, *sig, pubkey, *slot);
5213                }
5214
5215                svm.blocks
5216                    .store(
5217                        *slot,
5218                        BlockHeader {
5219                            hash: String::new(),
5220                            previous_blockhash: String::new(),
5221                            parent_slot: 0,
5222                            block_time: 0,
5223                            block_height: 0,
5224                            signatures: signatures.clone(),
5225                        },
5226                    )
5227                    .unwrap();
5228            }
5229        });
5230    }
5231
5232    fn fetch_signature_strings(
5233        locker: &SurfnetSvmLocker,
5234        pubkey: &Pubkey,
5235        config: Option<&RpcSignaturesForAddressConfig>,
5236    ) -> Vec<String> {
5237        locker
5238            .get_signatures_for_address_local(pubkey, config)
5239            .inner
5240            .iter()
5241            .map(|s| s.signature.clone())
5242            .collect()
5243    }
5244
5245    #[tokio::test(flavor = "multi_thread")]
5246    async fn test_get_signatures_for_address_ordering_within_block() {
5247        let (svm, _events_rx, _geyser_rx) = SurfnetSvm::default();
5248        let locker = SurfnetSvmLocker::new(svm);
5249
5250        let pubkey = Pubkey::new_unique();
5251        let sig_a = Signature::new_unique();
5252        let sig_b = Signature::new_unique();
5253        let sig_c = Signature::new_unique();
5254        let slot = 5;
5255
5256        seed_signature_history(&locker, &pubkey, &[(slot, vec![sig_a, sig_b, sig_c])]);
5257        let sigs = fetch_signature_strings(&locker, &pubkey, None);
5258
5259        // Last executed (C) should appear first, then B, then A
5260        assert_eq!(sigs.len(), 3);
5261        assert_eq!(sigs[0], sig_c.to_string());
5262        assert_eq!(sigs[1], sig_b.to_string());
5263        assert_eq!(sigs[2], sig_a.to_string());
5264    }
5265
5266    #[tokio::test(flavor = "multi_thread")]
5267    async fn test_get_signatures_for_address_until_excludes_boundary_signature() {
5268        let (svm, _events_rx, _geyser_rx) = SurfnetSvm::default();
5269        let locker = SurfnetSvmLocker::new(svm);
5270
5271        let pubkey = Pubkey::new_unique();
5272        let sig_a = Signature::new_unique();
5273        let sig_b = Signature::new_unique();
5274        let sig_c = Signature::new_unique();
5275        let slot = 5;
5276
5277        seed_signature_history(&locker, &pubkey, &[(slot, vec![sig_a, sig_b, sig_c])]);
5278        let sigs = fetch_signature_strings(
5279            &locker,
5280            &pubkey,
5281            Some(&RpcSignaturesForAddressConfig {
5282                until: Some(sig_b.to_string()),
5283                ..RpcSignaturesForAddressConfig::default()
5284            }),
5285        );
5286
5287        assert_eq!(sigs, vec![sig_c.to_string()]);
5288    }
5289
5290    #[tokio::test(flavor = "multi_thread")]
5291    async fn test_get_signatures_for_address_before_excludes_boundary_signature() {
5292        let (svm, _events_rx, _geyser_rx) = SurfnetSvm::default();
5293        let locker = SurfnetSvmLocker::new(svm);
5294
5295        let pubkey = Pubkey::new_unique();
5296        let sig_a = Signature::new_unique();
5297        let sig_b = Signature::new_unique();
5298        let sig_c = Signature::new_unique();
5299        let slot = 5;
5300
5301        seed_signature_history(&locker, &pubkey, &[(slot, vec![sig_a, sig_b, sig_c])]);
5302        let sigs = fetch_signature_strings(
5303            &locker,
5304            &pubkey,
5305            Some(&RpcSignaturesForAddressConfig {
5306                before: Some(sig_b.to_string()),
5307                ..RpcSignaturesForAddressConfig::default()
5308            }),
5309        );
5310
5311        assert_eq!(sigs, vec![sig_a.to_string()]);
5312    }
5313
5314    #[tokio::test(flavor = "multi_thread")]
5315    async fn test_get_signatures_for_address_before_and_until_form_window() {
5316        let (svm, _events_rx, _geyser_rx) = SurfnetSvm::default();
5317        let locker = SurfnetSvmLocker::new(svm);
5318
5319        let pubkey = Pubkey::new_unique();
5320        let sig_a = Signature::new_unique();
5321        let sig_b = Signature::new_unique();
5322        let sig_c = Signature::new_unique();
5323        let sig_d = Signature::new_unique();
5324        let slot = 5;
5325
5326        seed_signature_history(
5327            &locker,
5328            &pubkey,
5329            &[(slot, vec![sig_a, sig_b, sig_c, sig_d])],
5330        );
5331        let sigs = fetch_signature_strings(
5332            &locker,
5333            &pubkey,
5334            Some(&RpcSignaturesForAddressConfig {
5335                before: Some(sig_d.to_string()),
5336                until: Some(sig_b.to_string()),
5337                ..RpcSignaturesForAddressConfig::default()
5338            }),
5339        );
5340
5341        assert_eq!(sigs, vec![sig_c.to_string()]);
5342    }
5343
5344    #[tokio::test(flavor = "multi_thread")]
5345    async fn test_get_signatures_for_address_before_missing_returns_empty() {
5346        let (svm, _events_rx, _geyser_rx) = SurfnetSvm::default();
5347        let locker = SurfnetSvmLocker::new(svm);
5348
5349        let pubkey = Pubkey::new_unique();
5350        let sig_a = Signature::new_unique();
5351        let sig_b = Signature::new_unique();
5352        let missing_sig = Signature::new_unique();
5353        let slot = 5;
5354
5355        seed_signature_history(&locker, &pubkey, &[(slot, vec![sig_a, sig_b])]);
5356        let sigs = fetch_signature_strings(
5357            &locker,
5358            &pubkey,
5359            Some(&RpcSignaturesForAddressConfig {
5360                before: Some(missing_sig.to_string()),
5361                ..RpcSignaturesForAddressConfig::default()
5362            }),
5363        );
5364
5365        assert!(sigs.is_empty());
5366    }
5367
5368    #[tokio::test(flavor = "multi_thread")]
5369    async fn test_get_signatures_for_address_until_missing_returns_all_results() {
5370        let (svm, _events_rx, _geyser_rx) = SurfnetSvm::default();
5371        let locker = SurfnetSvmLocker::new(svm);
5372
5373        let pubkey = Pubkey::new_unique();
5374        let sig_a = Signature::new_unique();
5375        let sig_b = Signature::new_unique();
5376        let missing_sig = Signature::new_unique();
5377        let slot = 5;
5378
5379        seed_signature_history(&locker, &pubkey, &[(slot, vec![sig_a, sig_b])]);
5380        let sigs = fetch_signature_strings(
5381            &locker,
5382            &pubkey,
5383            Some(&RpcSignaturesForAddressConfig {
5384                until: Some(missing_sig.to_string()),
5385                ..RpcSignaturesForAddressConfig::default()
5386            }),
5387        );
5388
5389        assert_eq!(sigs, vec![sig_b.to_string(), sig_a.to_string()]);
5390    }
5391
5392    #[tokio::test(flavor = "multi_thread")]
5393    async fn test_get_signatures_for_address_limit_applies_after_windowing() {
5394        let (svm, _events_rx, _geyser_rx) = SurfnetSvm::default();
5395        let locker = SurfnetSvmLocker::new(svm);
5396
5397        let pubkey = Pubkey::new_unique();
5398        let sig_a = Signature::new_unique();
5399        let sig_b = Signature::new_unique();
5400        let sig_c = Signature::new_unique();
5401        let sig_d = Signature::new_unique();
5402        let sig_e = Signature::new_unique();
5403        let slot = 5;
5404
5405        seed_signature_history(
5406            &locker,
5407            &pubkey,
5408            &[(slot, vec![sig_a, sig_b, sig_c, sig_d, sig_e])],
5409        );
5410        let sigs = fetch_signature_strings(
5411            &locker,
5412            &pubkey,
5413            Some(&RpcSignaturesForAddressConfig {
5414                before: Some(sig_e.to_string()),
5415                until: Some(sig_a.to_string()),
5416                limit: Some(2),
5417                ..RpcSignaturesForAddressConfig::default()
5418            }),
5419        );
5420
5421        assert_eq!(sigs, vec![sig_d.to_string(), sig_c.to_string()]);
5422    }
5423
5424    #[tokio::test(flavor = "multi_thread")]
5425    async fn test_get_signatures_for_address_until_excludes_boundary_across_slots() {
5426        let (svm, _events_rx, _geyser_rx) = SurfnetSvm::default();
5427        let locker = SurfnetSvmLocker::new(svm);
5428
5429        let pubkey = Pubkey::new_unique();
5430        let sig_s5 = Signature::new_unique();
5431        let sig_s10_a = Signature::new_unique();
5432        let sig_s10_b = Signature::new_unique();
5433        let sig_s15 = Signature::new_unique();
5434
5435        seed_signature_history(
5436            &locker,
5437            &pubkey,
5438            &[
5439                (5, vec![sig_s5]),
5440                (10, vec![sig_s10_a, sig_s10_b]),
5441                (15, vec![sig_s15]),
5442            ],
5443        );
5444        let sigs = fetch_signature_strings(
5445            &locker,
5446            &pubkey,
5447            Some(&RpcSignaturesForAddressConfig {
5448                until: Some(sig_s10_b.to_string()),
5449                ..RpcSignaturesForAddressConfig::default()
5450            }),
5451        );
5452
5453        assert_eq!(sigs, vec![sig_s15.to_string()]);
5454    }
5455
5456    #[tokio::test(flavor = "multi_thread")]
5457    async fn test_get_signatures_for_address_ordering_across_slots() {
5458        let (svm, _events_rx, _geyser_rx) = SurfnetSvm::default();
5459        let locker = SurfnetSvmLocker::new(svm);
5460
5461        let pubkey = Pubkey::new_unique();
5462        let sig_s5_a = Signature::new_unique();
5463        let sig_s5_b = Signature::new_unique();
5464        let sig_s10_a = Signature::new_unique();
5465        let sig_s10_b = Signature::new_unique();
5466
5467        seed_signature_history(
5468            &locker,
5469            &pubkey,
5470            &[
5471                (5, vec![sig_s5_a, sig_s5_b]),
5472                (10, vec![sig_s10_a, sig_s10_b]),
5473            ],
5474        );
5475        let sigs = fetch_signature_strings(&locker, &pubkey, None);
5476
5477        // Slot 10 txs first (descending), then slot 5 txs
5478        // Within each slot: last executed first
5479        assert_eq!(sigs.len(), 4);
5480        assert_eq!(sigs[0], sig_s10_b.to_string());
5481        assert_eq!(sigs[1], sig_s10_a.to_string());
5482        assert_eq!(sigs[2], sig_s5_b.to_string());
5483        assert_eq!(sigs[3], sig_s5_a.to_string());
5484    }
5485
5486    #[tokio::test(flavor = "multi_thread")]
5487    async fn test_get_signatures_for_address_ordering_missing_block_header() {
5488        let (svm, _events_rx, _geyser_rx) = SurfnetSvm::default();
5489        let locker = SurfnetSvmLocker::new(svm);
5490
5491        let pubkey = Pubkey::new_unique();
5492        let sig_a = Signature::new_unique();
5493        let sig_b = Signature::new_unique();
5494        let slot = 5;
5495
5496        locker.with_svm_writer(|svm| {
5497            store_test_tx(svm, sig_a, &pubkey, slot);
5498            store_test_tx(svm, sig_b, &pubkey, slot);
5499            // No block header stored — should not panic
5500        });
5501
5502        let result = locker.get_signatures_for_address_local(&pubkey, None);
5503
5504        // Both transactions should be returned regardless
5505        assert_eq!(result.inner.len(), 2);
5506
5507        // Verify both signatures are present (order not guaranteed without block header)
5508        let sigs: HashSet<String> = result.inner.iter().map(|s| s.signature.clone()).collect();
5509        assert!(sigs.contains(&sig_a.to_string()));
5510        assert!(sigs.contains(&sig_b.to_string()));
5511    }
5512
5513    #[test]
5514    fn remote_config_passes_through_when_no_boundary_is_local() {
5515        let sig_before = Signature::new_unique().to_string();
5516        let sig_until = Signature::new_unique().to_string();
5517        let config = RpcSignaturesForAddressConfig {
5518            before: Some(sig_before.clone()),
5519            until: Some(sig_until.clone()),
5520            limit: Some(42),
5521            commitment: None,
5522            min_context_slot: Some(7),
5523        };
5524
5525        let translated = signatures_for_address_remote_config(Some(&config), false, false, 42)
5526            .expect("remote call should not be skipped");
5527
5528        assert_eq!(translated.before.as_deref(), Some(sig_before.as_str()));
5529        assert_eq!(translated.until.as_deref(), Some(sig_until.as_str()));
5530        assert_eq!(translated.limit, Some(42));
5531        assert_eq!(translated.min_context_slot, Some(7));
5532    }
5533
5534    #[test]
5535    fn remote_config_drops_local_before_boundary() {
5536        let sig_before = Signature::new_unique().to_string();
5537        let sig_until = Signature::new_unique().to_string();
5538        let config = RpcSignaturesForAddressConfig {
5539            before: Some(sig_before.clone()),
5540            until: Some(sig_until.clone()),
5541            limit: Some(100),
5542            commitment: None,
5543            min_context_slot: None,
5544        };
5545
5546        let translated = signatures_for_address_remote_config(Some(&config), true, false, 100)
5547            .expect("remote call should not be skipped");
5548
5549        assert!(
5550            translated.before.is_none(),
5551            "local `before` must not leak to the remote"
5552        );
5553        assert_eq!(translated.until.as_deref(), Some(sig_until.as_str()));
5554        assert_eq!(translated.limit, Some(100));
5555    }
5556
5557    #[test]
5558    fn remote_config_skipped_when_until_is_local() {
5559        let config = RpcSignaturesForAddressConfig {
5560            before: None,
5561            until: Some(Signature::new_unique().to_string()),
5562            limit: Some(100),
5563            commitment: None,
5564            min_context_slot: None,
5565        };
5566
5567        assert!(
5568            signatures_for_address_remote_config(Some(&config), false, true, 100).is_none(),
5569            "a local `until` boundary excludes the entire remote stream — the call must be skipped"
5570        );
5571        // And the same when both boundaries are local.
5572        assert!(signatures_for_address_remote_config(Some(&config), true, true, 100).is_none());
5573    }
5574
5575    #[test]
5576    fn remote_config_pins_remote_limit_to_remaining_slots() {
5577        // Caller asked for 100, local already produced 30 → remote should be capped at 70 so
5578        // the combined stream cannot exceed the caller-requested limit.
5579        let config = RpcSignaturesForAddressConfig {
5580            before: None,
5581            until: None,
5582            limit: Some(100),
5583            commitment: None,
5584            min_context_slot: None,
5585        };
5586
5587        let translated = signatures_for_address_remote_config(Some(&config), false, false, 70)
5588            .expect("remote call should not be skipped");
5589        assert_eq!(translated.limit, Some(70));
5590    }
5591
5592    #[test]
5593    fn remote_config_handles_missing_caller_config() {
5594        let translated = signatures_for_address_remote_config(None, false, false, 250)
5595            .expect("remote call should not be skipped");
5596        assert_eq!(
5597            translated.limit,
5598            Some(250),
5599            "even without a caller config, the remote must respect the remaining slot budget"
5600        );
5601        assert!(translated.before.is_none());
5602        assert!(translated.until.is_none());
5603    }
5604
5605    #[tokio::test(flavor = "multi_thread")]
5606    async fn initializes_epoch_schedule_without_warmup_when_offline() {
5607        let (surfnet_svm, _simnet_events_rx, _geyser_events_rx) = SurfnetSvm::default();
5608        let svm_locker = SurfnetSvmLocker::new(surfnet_svm);
5609
5610        svm_locker
5611            .initialize(&None)
5612            .await
5613            .expect("initialize should succeed");
5614
5615        let epoch_schedule =
5616            svm_locker.with_svm_reader(|svm_reader| svm_reader.inner.get_sysvar::<EpochSchedule>());
5617
5618        assert!(
5619            !epoch_schedule.warmup,
5620            "offline initialization should disable warmup to match mainnet"
5621        );
5622        assert_eq!(
5623            epoch_schedule.get_first_slot_in_epoch(886),
5624            886_u64 * 432_000,
5625            "first slot should align with mainnet epoch boundaries when warmup is disabled"
5626        );
5627    }
5628}