surfpool_core/surfnet/
locker.rs

1use std::sync::Arc;
2
3use crossbeam_channel::{Receiver, Sender};
4use jsonrpc_core::futures::future::join_all;
5use litesvm::types::{FailedTransactionMetadata, SimulatedTransactionInfo, TransactionResult};
6use solana_account::Account;
7use solana_account_decoder::parse_bpf_loader::{
8    parse_bpf_upgradeable_loader, BpfUpgradeableLoaderAccountType, UiProgram,
9};
10use solana_address_lookup_table_interface::state::AddressLookupTable;
11use solana_client::rpc_response::RpcKeyedAccount;
12use solana_clock::Slot;
13use solana_commitment_config::CommitmentConfig;
14use solana_epoch_info::EpochInfo;
15use solana_hash::Hash;
16use solana_message::{
17    v0::{LoadedAddresses, MessageAddressTableLookup},
18    VersionedMessage,
19};
20use solana_pubkey::Pubkey;
21use solana_sdk::{
22    bpf_loader_upgradeable::{get_program_data_address, UpgradeableLoaderState},
23    transaction::VersionedTransaction,
24};
25use solana_signature::Signature;
26use solana_transaction_error::TransactionError;
27use solana_transaction_status::{EncodedConfirmedTransactionWithStatusMeta, UiTransactionEncoding};
28use surfpool_types::{
29    ComputeUnitsEstimationResult, ProfileResult, SimnetEvent, TransactionConfirmationStatus,
30    TransactionStatusEvent,
31};
32use tokio::sync::RwLock;
33
34use super::{
35    remote::{SomeRemoteCtx, SurfnetRemoteClient},
36    AccountFactory, GetAccountResult, GetTransactionResult, GeyserEvent, SignatureSubscriptionType,
37    SurfnetSvm,
38};
39use crate::{
40    error::{SurfpoolError, SurfpoolResult},
41    rpc::utils::convert_transaction_metadata_from_canonical,
42};
43
44pub struct SvmAccessContext<T> {
45    pub slot: Slot,
46    pub latest_epoch_info: EpochInfo,
47    pub latest_blockhash: Hash,
48    pub inner: T,
49}
50
51impl<T> SvmAccessContext<T> {
52    pub fn new(slot: Slot, latest_epoch_info: EpochInfo, latest_blockhash: Hash, inner: T) -> Self {
53        Self {
54            slot,
55            latest_blockhash,
56            latest_epoch_info,
57            inner,
58        }
59    }
60
61    pub fn inner(&self) -> &T {
62        &self.inner
63    }
64
65    pub fn with_new_value<N>(&self, inner: N) -> SvmAccessContext<N> {
66        SvmAccessContext {
67            slot: self.slot,
68            latest_blockhash: self.latest_blockhash,
69            latest_epoch_info: self.latest_epoch_info.clone(),
70            inner,
71        }
72    }
73}
74
75pub type SurfpoolContextualizedResult<T> = SurfpoolResult<SvmAccessContext<T>>;
76
77pub struct SurfnetSvmLocker(pub Arc<RwLock<SurfnetSvm>>);
78
79impl Clone for SurfnetSvmLocker {
80    fn clone(&self) -> Self {
81        Self(self.0.clone())
82    }
83}
84
85/// Functions for reading and writing to the underlying SurfnetSvm instance
86impl SurfnetSvmLocker {
87    /// Executes a read-only operation on the underlying `SurfnetSvm` by acquiring a blocking read lock.
88    /// Accepts a closure that receives a shared reference to `SurfnetSvm` and returns a value.
89    ///
90    /// # Returns
91    /// The result produced by the closure.
92    pub fn with_svm_reader<T, F>(&self, reader: F) -> T
93    where
94        F: Fn(&SurfnetSvm) -> T + Send + Sync,
95        T: Send + 'static,
96    {
97        let read_lock = self.0.clone();
98        tokio::task::block_in_place(move || {
99            let read_guard = read_lock.blocking_read();
100            reader(&read_guard)
101        })
102    }
103
104    /// Executes a read-only operation and wraps the result in `SvmAccessContext`, capturing
105    /// slot, epoch info, and blockhash along with the closure's result.
106    fn with_contextualized_svm_reader<T, F>(&self, reader: F) -> SvmAccessContext<T>
107    where
108        F: Fn(&SurfnetSvm) -> T + Send + Sync,
109        T: Send + 'static,
110    {
111        let read_lock = self.0.clone();
112        tokio::task::block_in_place(move || {
113            let read_guard = read_lock.blocking_read();
114            let res = reader(&read_guard);
115
116            SvmAccessContext::new(
117                read_guard.get_latest_absolute_slot(),
118                read_guard.latest_epoch_info(),
119                read_guard.latest_blockhash(),
120                res,
121            )
122        })
123    }
124
125    /// Executes a write operation on the underlying `SurfnetSvm` by acquiring a blocking write lock.
126    /// Accepts a closure that receives a mutable reference to `SurfnetSvm` and returns a value.
127    ///
128    /// # Returns
129    /// The result produced by the closure.
130    pub fn with_svm_writer<T, F>(&self, writer: F) -> T
131    where
132        F: Fn(&mut SurfnetSvm) -> T + Send + Sync,
133        T: Send + 'static,
134    {
135        let write_lock = self.0.clone();
136        tokio::task::block_in_place(move || {
137            let mut write_guard = write_lock.blocking_write();
138            writer(&mut write_guard)
139        })
140    }
141}
142
143/// Functions for creating and initializing the underlying SurfnetSvm instance
144impl SurfnetSvmLocker {
145    /// Constructs a new `SurfnetSvmLocker` wrapping the given `SurfnetSvm` instance.
146    pub fn new(svm: SurfnetSvm) -> Self {
147        Self(Arc::new(RwLock::new(svm)))
148    }
149
150    /// Initializes the locked `SurfnetSvm` by fetching or defaulting epoch info,
151    /// then calling its `initialize` method. Returns the epoch info on success.
152    pub async fn initialize(
153        &self,
154        remote_ctx: &Option<SurfnetRemoteClient>,
155    ) -> SurfpoolResult<EpochInfo> {
156        let epoch_info = if let Some(remote_client) = remote_ctx {
157            remote_client.get_epoch_info().await?
158        } else {
159            EpochInfo {
160                epoch: 0,
161                slot_index: 0,
162                slots_in_epoch: 0,
163                absolute_slot: 0,
164                block_height: 0,
165                transaction_count: None,
166            }
167        };
168
169        self.with_svm_writer(|svm_writer| {
170            svm_writer.initialize(epoch_info.clone(), remote_ctx);
171        });
172        Ok(epoch_info)
173    }
174}
175
176/// Functions for getting accounts from the underlying SurfnetSvm instance or remote client
177impl SurfnetSvmLocker {
178    /// Retrieves a local account from the SVM cache, returning a contextualized result.
179    pub fn get_account_local(&self, pubkey: &Pubkey) -> SvmAccessContext<GetAccountResult> {
180        self.with_contextualized_svm_reader(|svm_reader| {
181            match svm_reader.inner.get_account(pubkey) {
182                Some(account) => GetAccountResult::FoundAccount(
183                    *pubkey, account,
184                    // mark as not an account that should be updated in the SVM, since this is a local read and it already exists
185                    false,
186                ),
187                None => GetAccountResult::None(*pubkey),
188            }
189        })
190    }
191
192    /// Attempts local retrieval, then fetches from remote if missing, returning a contextualized result.
193    pub async fn get_account_local_then_remote(
194        &self,
195        client: &SurfnetRemoteClient,
196        pubkey: &Pubkey,
197        commitment_config: CommitmentConfig,
198    ) -> SurfpoolContextualizedResult<GetAccountResult> {
199        let result = self.get_account_local(pubkey);
200
201        if result.inner.is_none() {
202            let remote_account = client.get_account(pubkey, commitment_config).await?;
203            Ok(result.with_new_value(remote_account))
204        } else {
205            Ok(result)
206        }
207    }
208
209    /// Retrieves an account, using local or remote based on context, applying a default factory if provided.
210    pub async fn get_account(
211        &self,
212        remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>,
213        pubkey: &Pubkey,
214        factory: Option<AccountFactory>,
215    ) -> SurfpoolContextualizedResult<GetAccountResult> {
216        let result = if let Some((remote_client, commitment_config)) = remote_ctx {
217            self.get_account_local_then_remote(remote_client, pubkey, *commitment_config)
218                .await?
219        } else {
220            self.get_account_local(pubkey)
221        };
222
223        match (&result.inner, factory) {
224            (&GetAccountResult::None(_), Some(factory)) => {
225                let default = factory(self.clone());
226                Ok(result.with_new_value(default))
227            }
228            _ => Ok(result),
229        }
230    }
231
232    /// Retrieves multiple accounts from local cache, returning a contextualized result.
233    pub fn get_multiple_accounts_local(
234        &self,
235        pubkeys: &[Pubkey],
236    ) -> SvmAccessContext<Vec<GetAccountResult>> {
237        self.with_contextualized_svm_reader(|svm_reader| {
238            let mut accounts = vec![];
239
240            for pubkey in pubkeys.iter() {
241                let res = match svm_reader.inner.get_account(pubkey) {
242                    Some(account) => GetAccountResult::FoundAccount(
243                        *pubkey, account,
244                        // mark as not an account that should be updated in the SVM, since this is a local read and it already exists
245                        false,
246                    ),
247                    None => GetAccountResult::None(*pubkey),
248                };
249                accounts.push(res);
250            }
251            accounts
252        })
253    }
254
255    /// Retrieves multiple accounts, fetching missing ones from remote, returning a contextualized result.
256    pub async fn get_multiple_accounts_local_then_remote(
257        &self,
258        client: &SurfnetRemoteClient,
259        pubkeys: &[Pubkey],
260        commitment_config: CommitmentConfig,
261    ) -> SurfpoolContextualizedResult<Vec<GetAccountResult>> {
262        let results = self.get_multiple_accounts_local(pubkeys);
263
264        let mut missing_accounts = vec![];
265        for result in &results.inner {
266            if let GetAccountResult::None(pubkey) = result {
267                missing_accounts.push(*pubkey)
268            }
269        }
270
271        if missing_accounts.is_empty() {
272            return Ok(results);
273        }
274
275        let mut remote_results = client
276            .get_multiple_accounts(&missing_accounts, commitment_config)
277            .await?;
278        let mut combined_results = results.inner.clone();
279        combined_results.append(&mut remote_results);
280
281        Ok(results.with_new_value(combined_results))
282    }
283
284    /// Retrieves multiple accounts, using local or remote context and applying factory defaults if provided.
285    pub async fn get_multiple_accounts(
286        &self,
287        remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>,
288        pubkeys: &[Pubkey],
289        factory: Option<AccountFactory>,
290    ) -> SurfpoolContextualizedResult<Vec<GetAccountResult>> {
291        let results = if let Some((remote_client, commitment_config)) = remote_ctx {
292            self.get_multiple_accounts_local_then_remote(remote_client, pubkeys, *commitment_config)
293                .await?
294        } else {
295            self.get_multiple_accounts_local(pubkeys)
296        };
297
298        let mut combined = Vec::with_capacity(results.inner.len());
299        for result in results.inner.clone() {
300            match (&result, &factory) {
301                (&GetAccountResult::None(_), Some(factory)) => {
302                    let default = factory(self.clone());
303                    combined.push(default);
304                }
305                _ => combined.push(result),
306            }
307        }
308        Ok(results.with_new_value(combined))
309    }
310}
311
312/// Functions for getting transactions from the underlying SurfnetSvm instance or remote client
313impl SurfnetSvmLocker {
314    /// Retrieves a transaction by signature, using local or remote based on context.
315    pub async fn get_transaction(
316        &self,
317        remote_ctx: &Option<(SurfnetRemoteClient, Option<UiTransactionEncoding>)>,
318        signature: &Signature,
319    ) -> SvmAccessContext<GetTransactionResult> {
320        if let Some((remote_client, encoding)) = remote_ctx {
321            self.get_transaction_local_then_remote(remote_client, signature, *encoding)
322                .await
323        } else {
324            self.get_transaction_local(signature)
325        }
326    }
327
328    /// Retrieves a transaction from local cache, returning a contextualized result.
329    pub fn get_transaction_local(
330        &self,
331        signature: &Signature,
332    ) -> SvmAccessContext<GetTransactionResult> {
333        self.with_contextualized_svm_reader(|svm_reader| {
334            let latest_absolute_slot = svm_reader.get_latest_absolute_slot();
335
336            match svm_reader.transactions.get(signature).map(|entry| {
337                Into::<EncodedConfirmedTransactionWithStatusMeta>::into(
338                    entry.expect_processed().clone(),
339                )
340            }) {
341                Some(tx) => {
342                    GetTransactionResult::found_transaction(*signature, tx, latest_absolute_slot)
343                }
344                None => GetTransactionResult::None(*signature),
345            }
346        })
347    }
348
349    /// Retrieves a transaction locally then from remote if missing, returning a contextualized result.
350    pub async fn get_transaction_local_then_remote(
351        &self,
352        client: &SurfnetRemoteClient,
353        signature: &Signature,
354        encoding: Option<UiTransactionEncoding>,
355    ) -> SvmAccessContext<GetTransactionResult> {
356        let local_result = self.get_transaction_local(signature);
357        if local_result.inner.is_none() {
358            let remote_result = client
359                .get_transaction(*signature, encoding, local_result.slot)
360                .await;
361            local_result.with_new_value(remote_result)
362        } else {
363            local_result
364        }
365    }
366}
367
368/// Functions for simulating and processing transactions in the underlying SurfnetSvm instance
369impl SurfnetSvmLocker {
370    /// Simulates a transaction on the SVM, returning detailed info or failure metadata.
371    pub fn simulate_transaction(
372        &self,
373        transaction: VersionedTransaction,
374    ) -> Result<SimulatedTransactionInfo, FailedTransactionMetadata> {
375        self.with_svm_reader(|svm_reader| svm_reader.simulate_transaction(transaction.clone()))
376    }
377
378    /// Processes a transaction: verifies signatures, preflight sim, sends to SVM, and enqueues status events.
379    pub async fn process_transaction(
380        &self,
381        remote_ctx: &Option<SurfnetRemoteClient>,
382        transaction: VersionedTransaction,
383        status_tx: Sender<TransactionStatusEvent>,
384        skip_preflight: bool,
385    ) -> SurfpoolContextualizedResult<()> {
386        let remote_ctx = &remote_ctx.get_remote_ctx(CommitmentConfig::confirmed());
387        let (latest_absolute_slot, latest_epoch_info, latest_blockhash) =
388            self.with_svm_writer(|svm_writer| {
389                let latest_absolute_slot = svm_writer.get_latest_absolute_slot();
390                svm_writer.notify_signature_subscribers(
391                    SignatureSubscriptionType::received(),
392                    &transaction.signatures[0],
393                    latest_absolute_slot,
394                    None,
395                );
396                (
397                    latest_absolute_slot,
398                    svm_writer.latest_epoch_info(),
399                    svm_writer.latest_blockhash(),
400                )
401            });
402
403        // verify valid signatures on the transaction
404        {
405            if transaction
406                .verify_with_results()
407                .iter()
408                .any(|valid| !*valid)
409            {
410                return Ok(self.with_contextualized_svm_reader(|svm_reader| {
411                    svm_reader
412                        .notify_invalid_transaction(transaction.signatures[0], status_tx.clone());
413                }));
414            }
415        }
416
417        let signature = transaction.signatures[0];
418
419        // find accounts that are needed for this transaction but are missing from the local
420        // svm cache, fetch them from the RPC, and insert them locally
421        let accounts = self
422            .get_pubkeys_from_message(remote_ctx, &transaction.message)
423            .await?;
424
425        let account_updates = self
426            .get_multiple_accounts(remote_ctx, &accounts, None)
427            .await?
428            .inner;
429
430        self.with_svm_writer(|svm_writer| {
431            for update in &account_updates {
432                svm_writer.write_account_update(update.clone());
433            }
434            // if not skipping preflight, simulate the transaction
435            if !skip_preflight {
436                match svm_writer.simulate_transaction(transaction.clone()) {
437                    Ok(_) => {}
438                    Err(res) => {
439                        let _ = svm_writer
440                            .simnet_events_tx
441                            .try_send(SimnetEvent::error(format!(
442                                "Transaction simulation failed: {}",
443                                res.err
444                            )));
445                        let meta = convert_transaction_metadata_from_canonical(&res.meta);
446                        let _ = status_tx.try_send(TransactionStatusEvent::SimulationFailure((
447                            res.err.clone(),
448                            meta,
449                        )));
450                        svm_writer.notify_signature_subscribers(
451                            SignatureSubscriptionType::processed(),
452                            &signature,
453                            latest_absolute_slot,
454                            Some(res.err),
455                        );
456                        return;
457                    }
458                }
459            }
460            // send the transaction to the SVM
461            let err = match svm_writer
462                .send_transaction(transaction.clone(), false /* cu_analysis_enabled */)
463            {
464                Ok(res) => {
465                    let transaction_meta = convert_transaction_metadata_from_canonical(&res);
466                    let _ = svm_writer
467                        .geyser_events_tx
468                        .send(GeyserEvent::NewTransaction(
469                            transaction.clone(),
470                            transaction_meta.clone(),
471                            latest_absolute_slot,
472                        ));
473                    let _ = status_tx.try_send(TransactionStatusEvent::Success(
474                        TransactionConfirmationStatus::Processed,
475                    ));
476                    svm_writer
477                        .transactions_queued_for_confirmation
478                        .push_back((transaction.clone(), status_tx.clone()));
479                    None
480                }
481                Err(res) => {
482                    let transaction_meta = convert_transaction_metadata_from_canonical(&res.meta);
483                    let _ = svm_writer
484                        .simnet_events_tx
485                        .try_send(SimnetEvent::error(format!(
486                            "Transaction execution failed: {}",
487                            res.err
488                        )));
489                    let _ = status_tx.try_send(TransactionStatusEvent::ExecutionFailure((
490                        res.err.clone(),
491                        transaction_meta,
492                    )));
493                    Some(res.err)
494                }
495            };
496
497            svm_writer.notify_signature_subscribers(
498                SignatureSubscriptionType::processed(),
499                &signature,
500                latest_absolute_slot,
501                err,
502            );
503        });
504
505        Ok(SvmAccessContext::new(
506            latest_absolute_slot,
507            latest_epoch_info,
508            latest_blockhash,
509            (),
510        ))
511    }
512}
513
514/// Functions for writing account updates to the underlying SurfnetSvm instance
515impl SurfnetSvmLocker {
516    /// Writes a single account update into the SVM state if present.
517    pub fn write_account_update(&self, account_update: GetAccountResult) {
518        if !account_update.requires_update() {
519            return;
520        }
521
522        self.with_svm_writer(move |svm_writer| {
523            svm_writer.write_account_update(account_update.clone())
524        })
525    }
526
527    /// Writes multiple account updates into the SVM state when any are present.
528    pub fn write_multiple_account_updates(&self, account_updates: &[GetAccountResult]) {
529        if account_updates
530            .iter()
531            .all(|update| !update.requires_update())
532        {
533            return;
534        }
535
536        self.with_svm_writer(move |svm_writer| {
537            for update in account_updates {
538                svm_writer.write_account_update(update.clone());
539            }
540        });
541    }
542}
543
544/// Token account related functions
545impl SurfnetSvmLocker {
546    /// Fetches all token accounts for an owner, returning remote results and missing pubkeys contexts.
547    pub async fn get_all_token_accounts(
548        &self,
549        remote_ctx: &Option<SurfnetRemoteClient>,
550        owner: Pubkey,
551        token_program: Pubkey,
552    ) -> SurfpoolContextualizedResult<(Vec<RpcKeyedAccount>, Vec<Pubkey>)> {
553        let keyed_accounts = if let Some(remote_client) = remote_ctx {
554            remote_client
555                .get_token_accounts_by_owner(owner, token_program)
556                .await?
557        } else {
558            vec![]
559        };
560
561        let token_account_pubkeys = keyed_accounts
562            .iter()
563            .map(|a| Pubkey::from_str_const(&a.pubkey))
564            .collect::<Vec<_>>();
565
566        // Fetch all of the returned accounts to see which ones aren't available in the local cache
567        let local_accounts = self.get_multiple_accounts_local(&token_account_pubkeys);
568
569        let missing_pubkeys = local_accounts
570            .inner
571            .iter()
572            .filter_map(|some_account_result| match &some_account_result {
573                GetAccountResult::None(pubkey) => Some(*pubkey),
574                _ => None,
575            })
576            .collect::<Vec<_>>();
577
578        // TODO: we still need to check local accounts, but I know of no way to iterate over the liteSVM accounts db
579
580        Ok(local_accounts.with_new_value((keyed_accounts, missing_pubkeys)))
581    }
582}
583
584/// Address lookup table related functions
585impl SurfnetSvmLocker {
586    /// Extracts pubkeys from a VersionedMessage, resolving address lookup tables as needed.
587    pub async fn get_pubkeys_from_message(
588        &self,
589        remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>,
590        message: &VersionedMessage,
591    ) -> SurfpoolResult<Vec<Pubkey>> {
592        match message {
593            VersionedMessage::Legacy(message) => Ok(message.account_keys.clone()),
594            VersionedMessage::V0(message) => {
595                let alts = message.address_table_lookups.clone();
596                let mut acc_keys = message.account_keys.clone();
597                let mut alt_pubkeys = alts.iter().map(|msg| msg.account_key).collect::<Vec<_>>();
598
599                let mut table_entries = join_all(alts.iter().map(|msg| async {
600                    let loaded_addresses = self
601                        .get_lookup_table_addresses(remote_ctx, msg)
602                        .await?
603                        .inner;
604                    let mut combined = loaded_addresses.writable;
605                    combined.extend(loaded_addresses.readonly);
606                    Ok::<_, SurfpoolError>(combined)
607                }))
608                .await
609                .into_iter()
610                .collect::<Result<Vec<Vec<Pubkey>>, SurfpoolError>>()?
611                .into_iter()
612                .flatten()
613                .collect();
614
615                acc_keys.append(&mut alt_pubkeys);
616                acc_keys.append(&mut table_entries);
617                Ok(acc_keys)
618            }
619        }
620    }
621
622    /// Retrieves loaded addresses from a lookup table account, validating owner and indices.
623    pub async fn get_lookup_table_addresses(
624        &self,
625        remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>,
626        address_table_lookup: &MessageAddressTableLookup,
627    ) -> SurfpoolContextualizedResult<LoadedAddresses> {
628        let result = self
629            .get_account(remote_ctx, &address_table_lookup.account_key, None)
630            .await?;
631        let table_account = result.inner.clone().map_account()?;
632
633        if table_account.owner == solana_sdk_ids::address_lookup_table::id() {
634            let SvmAccessContext {
635                slot: current_slot,
636                inner: slot_hashes,
637                ..
638            } = self.with_contextualized_svm_reader(|svm_reader| {
639                svm_reader
640                    .inner
641                    .get_sysvar::<solana_sdk::sysvar::slot_hashes::SlotHashes>()
642            });
643
644            //let current_slot = self.get_latest_absolute_slot(); // or should i use this?
645            let data = &table_account.data.clone();
646            let lookup_table = AddressLookupTable::deserialize(data).map_err(|_ix_err| {
647                SurfpoolError::invalid_account_data(
648                    address_table_lookup.account_key,
649                    table_account.data,
650                    Some("Attempted to lookup addresses from an invalid account"),
651                )
652            })?;
653
654            let loaded_addresses = LoadedAddresses {
655                writable: lookup_table
656                    .lookup(
657                        current_slot,
658                        &address_table_lookup.writable_indexes,
659                        &slot_hashes,
660                    )
661                    .map_err(|_ix_err| {
662                        SurfpoolError::invalid_lookup_index(address_table_lookup.account_key)
663                    })?,
664                readonly: lookup_table
665                    .lookup(
666                        current_slot,
667                        &address_table_lookup.readonly_indexes,
668                        &slot_hashes,
669                    )
670                    .map_err(|_ix_err| {
671                        SurfpoolError::invalid_lookup_index(address_table_lookup.account_key)
672                    })?,
673            };
674            Ok(result.with_new_value(loaded_addresses))
675        } else {
676            Err(SurfpoolError::invalid_account_owner(
677                table_account.owner,
678                Some("Attempted to lookup addresses from an account owned by the wrong program"),
679            ))
680        }
681    }
682}
683
684/// Profiling helper functions
685impl SurfnetSvmLocker {
686    /// Estimates compute units for a transaction via contextualized simulation.
687    pub fn estimate_compute_units(
688        &self,
689        transaction: &VersionedTransaction,
690    ) -> SvmAccessContext<ComputeUnitsEstimationResult> {
691        self.with_contextualized_svm_reader(|svm_reader| {
692            svm_reader.estimate_compute_units(transaction)
693        })
694    }
695
696    /// Records profiling results under a tag and emits a tagged profile event.
697    pub fn write_profiling_results(&self, tag: String, profile_result: ProfileResult) {
698        self.with_svm_writer(|svm_writer| {
699            svm_writer
700                .tagged_profiling_results
701                .entry(tag.clone())
702                .or_default()
703                .push(profile_result.clone());
704            let _ = svm_writer
705                .simnet_events_tx
706                .try_send(SimnetEvent::tagged_profile(
707                    profile_result.clone(),
708                    tag.clone(),
709                ));
710        });
711    }
712}
713
714/// Program account related functions
715impl SurfnetSvmLocker {
716    /// Clones a program account from source to destination, handling upgradeable loader state.
717    pub async fn clone_program_account(
718        &self,
719        remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>,
720        source_program_id: &Pubkey,
721        destination_program_id: &Pubkey,
722    ) -> SurfpoolContextualizedResult<()> {
723        let expected_source_program_data_address = get_program_data_address(source_program_id);
724
725        let result = self
726            .get_multiple_accounts(
727                remote_ctx,
728                &[*source_program_id, expected_source_program_data_address],
729                None,
730            )
731            .await?;
732
733        let mut accounts = result
734            .inner
735            .clone()
736            .into_iter()
737            .map(|a| a.map_account())
738            .collect::<SurfpoolResult<Vec<Account>>>()?;
739
740        let source_program_data_account = accounts.remove(1);
741        let source_program_account = accounts.remove(0);
742
743        let BpfUpgradeableLoaderAccountType::Program(UiProgram {
744            program_data: source_program_data_address,
745        }) = parse_bpf_upgradeable_loader(&source_program_account.data).map_err(|e| {
746            SurfpoolError::invalid_program_account(source_program_id, e.to_string())
747        })?
748        else {
749            return Err(SurfpoolError::expected_program_account(source_program_id));
750        };
751
752        if source_program_data_address.ne(&expected_source_program_data_address.to_string()) {
753            return Err(SurfpoolError::invalid_program_account(
754                source_program_id,
755                format!(
756                    "Program data address mismatch: expected {}, found {}",
757                    expected_source_program_data_address, source_program_data_address
758                ),
759            ));
760        }
761
762        let destination_program_data_address = get_program_data_address(destination_program_id);
763
764        // create a new program account that has the `program_data` field set to the
765        // destination program data address
766        let mut new_program_account = source_program_account;
767        new_program_account.data = bincode::serialize(&UpgradeableLoaderState::Program {
768            programdata_address: destination_program_data_address,
769        })
770        .map_err(|e| SurfpoolError::internal(format!("Failed to serialize program data: {}", e)))?;
771
772        self.with_svm_writer(|svm_writer| {
773            svm_writer.set_account(
774                &destination_program_data_address,
775                source_program_data_account.clone(),
776            )?;
777
778            svm_writer.set_account(destination_program_id, new_program_account.clone())?;
779            Ok::<(), SurfpoolError>(())
780        })?;
781
782        Ok(result.with_new_value(()))
783    }
784}
785
786/// Pass through functions for accessing the underlying SurfnetSvm instance
787impl SurfnetSvmLocker {
788    /// Returns a sender for simulation events from the underlying SVM.
789    pub fn simnet_events_tx(&self) -> Sender<SimnetEvent> {
790        self.with_svm_reader(|svm_reader| svm_reader.simnet_events_tx.clone())
791    }
792
793    /// Retrieves the latest epoch info from the underlying SVM.
794    pub fn get_epoch_info(&self) -> EpochInfo {
795        self.with_svm_reader(|svm_reader| svm_reader.latest_epoch_info.clone())
796    }
797
798    /// Retrieves the latest absolute slot from the underlying SVM.
799    pub fn get_latest_absolute_slot(&self) -> Slot {
800        self.with_svm_reader(|svm_reader| svm_reader.get_latest_absolute_slot())
801    }
802
803    /// Executes an airdrop via the underlying SVM.
804    pub fn airdrop(&self, pubkey: &Pubkey, lamports: u64) -> TransactionResult {
805        self.with_svm_writer(|svm_writer| svm_writer.airdrop(pubkey, lamports))
806    }
807
808    /// Executes a batch airdrop via the underlying SVM.
809    pub fn airdrop_pubkeys(&self, lamports: u64, addresses: &[Pubkey]) {
810        self.with_svm_writer(|svm_writer| svm_writer.airdrop_pubkeys(lamports, addresses))
811    }
812
813    /// Confirms the current block on the underlying SVM, returning `Ok(())` or an error.
814    pub fn confirm_current_block(&self) -> SurfpoolResult<()> {
815        self.with_svm_writer(|svm_writer| svm_writer.confirm_current_block())
816    }
817
818    /// Subscribes for signature updates (confirmed/finalized) and returns a receiver of events.
819    pub fn subscribe_for_signature_updates(
820        &self,
821        signature: &Signature,
822        subscription_type: SignatureSubscriptionType,
823    ) -> Receiver<(Slot, Option<TransactionError>)> {
824        self.with_svm_writer(|svm_writer| {
825            svm_writer.subscribe_for_signature_updates(signature, subscription_type.clone())
826        })
827    }
828}