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