Skip to main content

surfpool_core/rpc/
surfnet_cheatcodes.rs

1use std::{
2    collections::BTreeMap,
3    sync::{Arc, RwLock},
4};
5
6use base64::{Engine as _, engine::general_purpose::STANDARD};
7use jsonrpc_core::{BoxFuture, Error, Result, futures::future};
8use jsonrpc_derive::rpc;
9use solana_account::Account;
10use solana_client::rpc_response::{RpcLogsResponse, RpcResponseContext};
11use solana_clock::Slot;
12use solana_commitment_config::CommitmentConfig;
13use solana_epoch_info::EpochInfo;
14use solana_program_option::COption;
15use solana_rpc_client_api::response::Response as RpcResponse;
16use solana_system_interface::program as system_program;
17use solana_transaction::versioned::VersionedTransaction;
18use spl_associated_token_account_interface::address::get_associated_token_address_with_program_id;
19use surfpool_types::{
20    AccountSnapshot, CheatcodeControlConfig, CheatcodeFilter, ClockCommand, ExportSnapshotConfig,
21    GetStreamedAccountsResponse, GetSurfnetInfoResponse, Idl, OfflineAccountConfig,
22    ResetAccountConfig, RpcProfileResultConfig, Scenario, SimnetCommand, SimnetEvent,
23    StreamAccountConfig, StreamAccountsEntry, UiKeyedProfileResult,
24    types::{AccountUpdate, SetSomeAccount, SupplyUpdate, TokenAccountUpdate, UuidOrSignature},
25};
26
27use super::{RunloopContext, SurfnetRpcContext};
28use crate::{
29    error::SurfpoolError,
30    rpc::{
31        State,
32        utils::{verify_pubkey, verify_pubkeys},
33    },
34    surfnet::{GetAccountResult, locker::SvmAccessContext},
35    types::{TimeTravelConfig, TokenAccount},
36};
37
38pub trait AccountUpdateExt {
39    fn is_full_account_data_ext(&self) -> bool;
40    fn to_account_ext(&self) -> Result<Option<Account>>;
41    fn apply_ext(self, account: &mut GetAccountResult) -> Result<()>;
42    fn expect_hex_data_ext(&self) -> Result<Vec<u8>>;
43}
44
45impl AccountUpdateExt for AccountUpdate {
46    fn is_full_account_data_ext(&self) -> bool {
47        self.lamports.is_some()
48            && self.owner.is_some()
49            && self.executable.is_some()
50            && self.rent_epoch.is_some()
51            && self.data.is_some()
52    }
53
54    fn to_account_ext(&self) -> Result<Option<Account>> {
55        if self.is_full_account_data_ext() {
56            Ok(Some(Account {
57                lamports: self.lamports.unwrap(),
58                owner: verify_pubkey(&self.owner.clone().unwrap())?,
59                executable: self.executable.unwrap(),
60                rent_epoch: self.rent_epoch.unwrap(),
61                data: self.expect_hex_data_ext()?,
62            }))
63        } else {
64            Ok(None)
65        }
66    }
67
68    fn apply_ext(self, account_result: &mut GetAccountResult) -> Result<()> {
69        account_result.apply_update(|account| {
70            if let Some(lamports) = self.lamports {
71                account.lamports = lamports;
72            }
73            if let Some(owner) = &self.owner {
74                account.owner = verify_pubkey(owner)?;
75            }
76            if let Some(executable) = self.executable {
77                account.executable = executable;
78            }
79            if let Some(rent_epoch) = self.rent_epoch {
80                account.rent_epoch = rent_epoch;
81            }
82            if self.data.is_some() {
83                account.data = self.expect_hex_data_ext()?;
84            }
85            Ok(())
86        })?;
87        Ok(())
88    }
89
90    fn expect_hex_data_ext(&self) -> Result<Vec<u8>> {
91        let data = self.data.as_ref().expect("missing expected data field");
92        hex::decode(data)
93            .map_err(|e| Error::invalid_params(format!("Invalid hex data provided: {}", e)))
94    }
95}
96
97pub trait TokenAccountUpdateExt {
98    fn apply(self, token_account: &mut TokenAccount) -> Result<()>;
99}
100
101impl TokenAccountUpdateExt for TokenAccountUpdate {
102    /// Apply the update to the account
103    fn apply(self, token_account: &mut TokenAccount) -> Result<()> {
104        if let Some(amount) = self.amount {
105            token_account.set_amount(amount);
106        }
107        if let Some(delegate) = self.delegate {
108            match delegate {
109                SetSomeAccount::Account(pubkey) => {
110                    token_account.set_delegate(COption::Some(verify_pubkey(&pubkey)?));
111                }
112                SetSomeAccount::NoAccount => {
113                    token_account.set_delegate(COption::None);
114                }
115            }
116        }
117        if let Some(state) = self.state {
118            token_account.set_state_from_str(state.as_str())?;
119        }
120        if let Some(delegated_amount) = self.delegated_amount {
121            token_account.set_delegated_amount(delegated_amount);
122        }
123        if let Some(close_authority) = self.close_authority {
124            match close_authority {
125                SetSomeAccount::Account(pubkey) => {
126                    token_account.set_close_authority(COption::Some(verify_pubkey(&pubkey)?));
127                }
128                SetSomeAccount::NoAccount => {
129                    token_account.set_close_authority(COption::None);
130                }
131            }
132        }
133        Ok(())
134    }
135}
136
137#[rpc]
138pub trait SurfnetCheatcodes {
139    type Metadata;
140
141    /// A "cheat code" method for developers to set or update an account in Surfpool.
142    ///
143    /// This method allows developers to set or update the lamports, data, owner, executable status,
144    /// and rent epoch of a given account.
145    ///
146    /// ## Parameters
147    /// - `pubkey`: The public key of the account to be updated, as a base-58 encoded string.
148    /// - `update`: The `AccountUpdate` struct containing the fields to update the account.
149    ///
150    /// ## Returns
151    /// A `RpcResponse<()>` indicating whether the account update was successful.
152    ///
153    /// ## Example Request
154    /// ```json
155    /// {
156    ///   "jsonrpc": "2.0",
157    ///   "id": 1,
158    ///   "method": "surfnet_setAccount",
159    ///   "params": ["account_pubkey", {"lamports": 1000, "data": "base58string", "owner": "program_pubkey"}]
160    /// }
161    /// ```
162    ///
163    /// ## Example Response
164    /// ```json
165    /// {
166    ///   "jsonrpc": "2.0",
167    ///   "result": {},
168    ///   "id": 1
169    /// }
170    /// ```
171    ///
172    /// # Notes
173    /// This method is designed to help developers set or modify account properties within Surfpool.
174    /// Developers can quickly test or update account attributes, such as lamports, program ownership, and executable status.
175    ///
176    /// # See Also
177    /// - `getAccount`, `getAccountInfo`, `getAccountBalance`
178    #[rpc(meta, name = "surfnet_setAccount")]
179    fn set_account(
180        &self,
181        meta: Self::Metadata,
182        pubkey: String,
183        update: AccountUpdate,
184    ) -> BoxFuture<Result<RpcResponse<()>>>;
185
186    /// Enables one or more Surfpool cheatcode RPC methods for the current session.
187    ///
188    /// This method allows developers to re-enable cheatcode methods that were previously disabled.
189    /// Each cheatcode name must match a valid `surfnet_*` RPC method (e.g. `surfnet_setAccount`, `surfnet_timeTravel`).
190    ///
191    /// ## Parameters
192    /// - `cheatcodes`: A list of cheatcode method names to enable, as strings (e.g. `["surfnet_setAccount", "surfnet_timeTravel"]`).
193    ///
194    /// ## Returns
195    /// A `RpcResponse<()>` indicating whether the enable operation was successful.
196    ///
197    /// ## Example Request
198    /// ```json
199    /// {
200    ///   "jsonrpc": "2.0",
201    ///   "id": 1,
202    ///   "method": "surfnet_enableCheatcode",
203    ///   "params": [["surfnet_setAccount", "surfnet_timeTravel"]]
204    /// }
205    /// ```
206    ///
207    /// ## Example Response
208    /// ```json
209    /// {
210    ///   "jsonrpc": "2.0",
211    ///   "result": {},
212    ///   "id": 1
213    /// }
214    /// ```
215    ///
216    /// # Notes
217    /// Invalid cheatcode names return an error. Use `surfnet_disableCheatcode` to disable methods and `surfnet_lockout` to allow disabling `surfnet_enableCheatcode` and `surfnet_disableCheatcode` themselves.
218    ///
219    /// # See Also
220    /// - `surfnet_disableCheatcode`, `surfnet_disableAllCheatcodes`, `surfnet_lockout`
221    #[rpc(meta, name = "surfnet_enableCheatcode")]
222    fn enable_cheatcode(
223        &self,
224        meta: Self::Metadata,
225        cheatcodes_filter: CheatcodeFilter,
226    ) -> Result<RpcResponse<()>>;
227
228    /// Disables one or more Surfpool cheatcode RPC methods for the current session.
229    ///
230    /// This method allows developers to turn off specific cheatcode methods so they are no longer callable.
231    /// Each cheatcode name must match a valid `surfnet_*` RPC method. When lockout is not enabled,
232    /// `surfnet_enableCheatcode` and `surfnet_disableCheatcode` cannot be disabled.
233    ///
234    /// ## Parameters
235    /// - `cheatcodes`: A list of cheatcode method names to disable, as strings (e.g. `["surfnet_setAccount"]`).
236    ///
237    /// ## Returns
238    /// A `RpcResponse<()>` indicating whether the disable operation was successful.
239    ///
240    /// ## Example Request
241    /// ```json
242    /// {
243    ///   "jsonrpc": "2.0",
244    ///   "id": 1,
245    ///   "method": "surfnet_disableCheatcode",
246    ///   "params": [["surfnet_setAccount", "surfnet_timeTravel"]]
247    /// }
248    /// ```
249    ///
250    /// ## Example Response
251    /// ```json
252    /// {
253    ///   "jsonrpc": "2.0",
254    ///   "result": {},
255    ///   "id": 1
256    /// }
257    /// ```
258    ///
259    /// # Notes
260    /// Call `surfnet_lockout` first if you need to disable `surfnet_enableCheatcode` or `surfnet_disableCheatcode`.
261    ///
262    /// # See Also
263    /// - `surfnet_enableCheatcode`, `surfnet_disableAllCheatcodes`, `surfnet_lockout`
264    #[rpc(meta, name = "surfnet_disableCheatcode")]
265    fn disable_cheatcode(
266        &self,
267        meta: Self::Metadata,
268        cheatcodes_filter: CheatcodeFilter,
269        lockout: Option<CheatcodeControlConfig>,
270    ) -> Result<RpcResponse<()>>;
271
272    /// A "cheat code" method for developers to set or update a token account in Surfpool.
273    ///
274    /// This method allows developers to set or update various properties of a token account,
275    /// including the token amount, delegate, state, delegated amount, and close authority.
276    ///
277    /// ## Parameters
278    /// - `owner`: The base-58 encoded public key of the token account's owner.
279    /// - `mint`: The base-58 encoded public key of the token mint (e.g., the token type).
280    /// - `update`: The `TokenAccountUpdate` struct containing the fields to update the token account.
281    /// - `token_program`: The optional base-58 encoded address of the token program (defaults to the system token program).
282    ///
283    /// ## Returns
284    /// A `RpcResponse<()>` indicating whether the token account update was successful.
285    ///
286    /// ## Example Request
287    /// ```json
288    /// {
289    ///   "jsonrpc": "2.0",
290    ///   "id": 1,
291    ///   "method": "surfnet_setTokenAccount",
292    ///   "params": ["owner_pubkey", "mint_pubkey", {"amount": 1000, "state": "initialized"}]
293    /// }
294    /// ```
295    ///
296    /// ## Example Response
297    /// ```json
298    /// {
299    ///   "jsonrpc": "2.0",
300    ///   "result": {},
301    ///   "id": 1
302    /// }
303    /// ```
304    ///
305    /// # Notes
306    /// This method is designed to help developers quickly test or modify token account properties in Surfpool.
307    /// Developers can update attributes such as token amounts, delegates, and authorities for specific token accounts.
308    ///
309    /// # See Also
310    /// - `getTokenAccountInfo`, `getTokenAccountBalance`, `getTokenAccountDelegate`
311    #[rpc(meta, name = "surfnet_setTokenAccount")]
312    fn set_token_account(
313        &self,
314        meta: Self::Metadata,
315        owner: String,
316        mint: String,
317        update: TokenAccountUpdate,
318        token_program: Option<String>,
319    ) -> BoxFuture<Result<RpcResponse<()>>>;
320
321    #[rpc(meta, name = "surfnet_cloneProgramAccount")]
322    fn clone_program_account(
323        &self,
324        meta: Self::Metadata,
325        source_program_id: String,
326        destination_program_id: String,
327    ) -> BoxFuture<Result<RpcResponse<()>>>;
328
329    /// Estimates the compute units that a given transaction will consume.
330    ///
331    /// This method simulates the transaction without committing its state changes
332    /// and returns an estimation of the compute units used, along with logs and
333    /// potential errors.
334    ///
335    /// ## Parameters
336    /// - `transaction_data`: A base64 encoded string of the `VersionedTransaction`.
337    /// - `tag`: An optional tag for the transaction.
338    /// - `encoding`: An optional encoding for returned account data.
339    ///
340    /// ## Returns
341    /// A `RpcResponse<ProfileResult>` containing the estimation details and a snapshot of the accounts before and after execution.
342    ///
343    /// ## Example Request
344    /// ```json
345    /// {
346    ///   "jsonrpc": "2.0",
347    ///   "id": 1,
348    ///   "method": "surfnet_profileTransaction",
349    ///   "params": ["base64_encoded_transaction_string", "optional_tag"]
350    /// }
351    /// ```
352    #[rpc(meta, name = "surfnet_profileTransaction")]
353    fn profile_transaction(
354        &self,
355        meta: Self::Metadata,
356        transaction_data: String, // Base64 encoded VersionedTransaction
357        tag: Option<String>,      // Optional tag for the transaction
358        config: Option<RpcProfileResultConfig>,
359    ) -> BoxFuture<Result<RpcResponse<UiKeyedProfileResult>>>;
360
361    /// Retrieves all profiling results for a given tag.
362    ///
363    /// ## Parameters
364    /// - `tag`: The tag to retrieve profiling results for.
365    ///
366    /// ## Returns
367    /// A `RpcResponse<Vec<ProfileResult>>` containing the profiling results.
368    #[rpc(meta, name = "surfnet_getProfileResultsByTag")]
369    fn get_profile_results_by_tag(
370        &self,
371        meta: Self::Metadata,
372        tag: String,
373        config: Option<RpcProfileResultConfig>,
374    ) -> Result<RpcResponse<Option<Vec<UiKeyedProfileResult>>>>;
375
376    /// A "cheat code" method for developers to set or update the network supply information in Surfpool.
377    ///
378    /// This method allows developers to configure the total supply, circulating supply,
379    /// non-circulating supply, and non-circulating accounts list that will be returned
380    /// by the `getSupply` RPC method.
381    ///
382    /// ## Parameters
383    /// - `update`: The `SupplyUpdate` struct containing the optional fields to update:
384    ///   - `total`: Optional total supply in lamports
385    ///   - `circulating`: Optional circulating supply in lamports
386    ///   - `non_circulating`: Optional non-circulating supply in lamports
387    ///   - `non_circulating_accounts`: Optional list of non-circulating account addresses
388    ///
389    /// ## Returns
390    /// A `RpcResponse<()>` indicating whether the supply update was successful.
391    ///
392    /// ## Example Request
393    /// ```json
394    /// {
395    ///   "jsonrpc": "2.0",
396    ///   "id": 1,
397    ///   "method": "surfnet_setSupply",
398    ///   "params": [{
399    ///     "total": 1000000000000000,
400    ///     "circulating": 800000000000000,
401    ///     "non_circulating": 200000000000000,
402    ///     "non_circulating_accounts": ["Account1...", "Account2..."]
403    ///   }]
404    /// }
405    /// ```
406    ///
407    /// ## Example Response
408    /// ```json
409    /// {
410    ///   "jsonrpc": "2.0",
411    ///   "result": {},
412    ///   "id": 1
413    /// }
414    /// ```
415    ///
416    /// # Notes
417    /// This method is designed to help developers test supply-related functionality by
418    /// allowing them to configure the values returned by `getSupply` without needing
419    /// to connect to a real network or manipulate actual token supplies.
420    ///
421    /// # See Also
422    /// - `getSupply`
423    #[rpc(meta, name = "surfnet_setSupply")]
424    fn set_supply(
425        &self,
426        meta: Self::Metadata,
427        update: SupplyUpdate,
428    ) -> BoxFuture<Result<RpcResponse<()>>>;
429
430    /// A cheat code to set the upgrade authority of a program's ProgramData account.
431    ///
432    /// This method allows developers to directly patch the upgrade authority of a program's ProgramData account.
433    ///
434    /// ## Parameters
435    /// - `program_id`: The base-58 encoded public key of the program.
436    /// - `new_authority`: The base-58 encoded public key of the new authority. If omitted, the program will have no upgrade authority.
437    ///
438    /// ## Returns
439    /// A `RpcResponse<()>` indicating whether the authority update was successful.
440    ///
441    /// ## Example Request
442    /// ```json
443    /// {
444    ///   "jsonrpc": "2.0",
445    ///   "id": 1,
446    ///   "method": "surfnet_setProgramAuthority",
447    ///   "params": [
448    ///     "PROGRAM_ID_BASE58",
449    ///     "NEW_AUTHORITY_BASE58"
450    ///   ]
451    /// }
452    /// ```
453    ///
454    /// ## Example Response
455    /// ```json
456    /// {
457    ///   "jsonrpc": "2.0",
458    ///   "result": {},
459    ///   "id": 1
460    /// }
461    /// ```
462    #[rpc(meta, name = "surfnet_setProgramAuthority")]
463    fn set_program_authority(
464        &self,
465        meta: Self::Metadata,
466        program_id_str: String,
467        new_authority_str: Option<String>,
468    ) -> BoxFuture<Result<RpcResponse<()>>>;
469
470    /// A cheat code to get the transaction profile for a given signature or UUID.
471    ///
472    /// ## Parameters
473    /// - `signature_or_uuid`: The transaction signature (as a base-58 string) or a UUID (as a string) for which to retrieve the profile.
474    ///
475    /// ## Returns
476    /// A `RpcResponse<Option<ProfileResult>>` containing the transaction profile if found, or `None` if not found.
477    ///
478    /// ## Example Request
479    /// ```json
480    /// {
481    ///   "jsonrpc": "2.0",
482    ///   "id": 1,
483    ///   "method": "surfnet_getTransactionProfile",
484    ///   "params": [
485    ///     "5Nf3...TxSignatureOrUuidHere"
486    ///   ]
487    /// }
488    /// ```
489    ///
490    /// ## Example Response
491    /// ```json
492    /// {
493    ///   "jsonrpc": "2.0",
494    ///   "context": {
495    ///     "slot": 355684457,
496    ///     "apiVersion": "2.2.2"
497    ///   },
498    ///   "value": { /* ...ProfileResult object... */ },
499    ///   "id": 1
500    /// }
501    /// ```
502    #[rpc(meta, name = "surfnet_getTransactionProfile")]
503    fn get_transaction_profile(
504        &self,
505        meta: Self::Metadata,
506        signature_or_uuid: UuidOrSignature,
507        config: Option<RpcProfileResultConfig>,
508    ) -> Result<RpcResponse<Option<UiKeyedProfileResult>>>;
509
510    /// A cheat code to register an IDL for a given program in memory.
511    ///
512    /// ## Parameters
513    /// - `idl`: The full IDL object to be registered in memory. The `address` field should match the program's public key.
514    /// - `slot` (optional): The slot at which to register the IDL. If omitted, uses the latest slot.
515    ///
516    /// ## Returns
517    /// A `RpcResponse<()>` indicating whether the IDL registration was successful.
518    ///
519    /// ## Example Request (with slot)
520    /// ```json
521    /// {
522    ///   "jsonrpc": "2.0",
523    ///   "id": 1,
524    ///   "method": "surfnet_registerIdl",
525    ///   "params": [
526    ///     {
527    ///       "address": "4EXSeLGxVBpAZwq7vm6evLdewpcvE2H56fpqL2pPiLFa",
528    ///       "metadata": {
529    ///         "name": "test",
530    ///         "version": "0.1.0",
531    ///         "spec": "0.1.0",
532    ///         "description": "Created with Anchor"
533    ///       },
534    ///       "instructions": [
535    ///         {
536    ///           "name": "initialize",
537    ///           "discriminator": [175,175,109,31,13,152,155,237],
538    ///           "accounts": [],
539    ///           "args": []
540    ///         }
541    ///       ],
542    ///       "accounts": [],
543    ///       "types": [],
544    ///       "events": [],
545    ///       "errors": [],
546    ///       "constants": [],
547    ///       "state": null
548    ///     },
549    ///     355684457
550    ///   ]
551    /// }
552    /// ```
553    /// ## Example Request (without slot)
554    /// ```json
555    /// {
556    ///   "jsonrpc": "2.0",
557    ///   "id": 1,
558    ///   "method": "surfnet_registerIdl",
559    ///   "params": [
560    ///     {
561    ///       "address": "4EXSeLGxVBpAZwq7vm6evLdewpcvE2H56fpqL2pPiLFa",
562    ///       "metadata": {
563    ///         "name": "test",
564    ///         "version": "0.1.0",
565    ///         "spec": "0.1.0",
566    ///         "description": "Created with Anchor"
567    ///       },
568    ///       "instructions": [
569    ///         {
570    ///           "name": "initialize",
571    ///           "discriminator": [175,175,109,31,13,152,155,237],
572    ///           "accounts": [],
573    ///           "args": []
574    ///         }
575    ///       ],
576    ///       "accounts": [],
577    ///       "types": [],
578    ///       "events": [],
579    ///       "errors": [],
580    ///       "constants": [],
581    ///       "state": null
582    ///     }
583    ///   ]
584    /// }
585    /// ```
586    ///
587    /// ## Example Response
588    /// ```json
589    /// {
590    ///   "jsonrpc": "2.0",
591    ///   "context": {
592    ///     "slot": 355684457,
593    ///     "apiVersion": "2.2.2"
594    ///   },
595    ///   "value": null,
596    ///   "id": 1
597    /// }
598    /// ```
599    #[rpc(meta, name = "surfnet_registerIdl")]
600    fn register_idl(
601        &self,
602        meta: Self::Metadata,
603        idl: Idl,
604        slot: Option<Slot>,
605    ) -> Result<RpcResponse<()>>;
606
607    /// A cheat code to get the registered IDL for a given program ID.
608    ///
609    /// ## Parameters
610    /// - `program_id`: The base-58 encoded public key of the program whose IDL is being requested.
611    /// - `slot` (optional): The slot at which to query the IDL. If omitted, uses the latest slot.
612    ///
613    /// ## Returns
614    /// A `RpcResponse<Option<Idl>>` containing the IDL if it exists, or `None` if not found.
615    ///
616    /// ## Example Request (with slot)
617    /// ```json
618    /// {
619    ///   "jsonrpc": "2.0",
620    ///   "id": 1,
621    ///   "method": "surfnet_getIdl",
622    ///   "params": [
623    ///     "4EXSeLGxVBpAZwq7vm6evLdewpcvE2H56fpqL2pPiLFa",
624    ///     355684457
625    ///   ]
626    /// }
627    /// ```
628    /// ## Example Request (without slot)
629    /// ```json
630    /// {
631    ///   "jsonrpc": "2.0",
632    ///   "id": 1,
633    ///   "method": "surfnet_getIdl",
634    ///   "params": [
635    ///     "4EXSeLGxVBpAZwq7vm6evLdewpcvE2H56fpqL2pPiLFa"
636    ///   ]
637    /// }
638    /// ```
639    ///
640    /// ## Example Response
641    /// ```json
642    /// {
643    ///   "jsonrpc": "2.0",
644    ///   "context": {
645    ///     "slot": 355684457,
646    ///     "apiVersion": "2.2.2"
647    ///   },
648    ///   "value": { /* ...IDL object... */ },
649    ///   "id": 1
650    /// }
651    /// ```
652    #[rpc(meta, name = "surfnet_getActiveIdl")]
653    fn get_idl(
654        &self,
655        meta: Self::Metadata,
656        program_id: String,
657        slot: Option<Slot>,
658    ) -> Result<RpcResponse<Option<Idl>>>;
659
660    /// A cheat code to get the last 50 local signatures from the local network.
661    /// ## Example Request
662    /// ```json
663    /// {
664    ///   "jsonrpc": "2.0",
665    ///   "id": 1,
666    ///   "method": "surfnet_getLocalSignatures",
667    ///   "params": [ { "limit": 50 } ]
668    /// }
669    ///
670    /// ## Example Response
671    /// ```json
672    /// {
673    ///   "jsonrpc": "2.0",
674    ///   "result": {
675    ///     "signature": String,
676    ///     "err": Option<TransactionError>,
677    ///     "slot": u64,
678    ///   },
679    ///   "id": 1
680    /// }
681    #[rpc(meta, name = "surfnet_getLocalSignatures")]
682    fn get_local_signatures(
683        &self,
684        meta: Self::Metadata,
685        limit: Option<u64>,
686    ) -> BoxFuture<Result<RpcResponse<Vec<RpcLogsResponse>>>>;
687
688    /// A cheat code to jump forward or backward in time on the local network.
689    /// Useful for testing epoch-based or time-sensitive logic.
690    ///
691    /// ## Parameters
692    /// - `config` (optional): A `TimeTravelConfig` specifying how to modify the clock:
693    ///   - `absoluteTimestamp(u64)`: Moves time to the specified UNIX timestamp.
694    ///   - `absoluteSlot(u64)`: Moves to the specified absolute slot.
695    ///   - `absoluteEpoch(u64)`: Advances time to the specified epoch (each epoch = 432,000 slots).
696    ///
697    /// ## Returns
698    /// An `EpochInfo` object reflecting the updated clock state.
699    ///
700    /// ## Example Request
701    /// ```json
702    /// {
703    ///   "jsonrpc": "2.0",
704    ///   "id": 1,
705    ///   "method": "surfnet_timeTravel",
706    ///   "params": [ { "absoluteSlot": 512 } ]
707    /// }
708    /// ```
709    ///
710    /// ## Example Response
711    /// ```json
712    /// {
713    ///   "jsonrpc": "2.0",
714    ///   "result": {
715    ///     "epoch": 512,
716    ///     "slot_index": 0,
717    ///     "slots_in_epoch": 432000,
718    ///     "absolute_slot": 221184000,
719    ///     "block_height": 650000000,
720    ///     "transaction_count": 923472834
721    ///   },
722    ///   "id": 1
723    /// }
724    #[rpc(meta, name = "surfnet_timeTravel")]
725    fn time_travel(
726        &self,
727        meta: Self::Metadata,
728        config: Option<TimeTravelConfig>,
729    ) -> Result<EpochInfo>;
730
731    /// A cheat code to freeze the Surfnet clock on the local network.
732    /// All time progression halts until resumed.
733    ///
734    /// ## Returns
735    /// An `EpochInfo` object showing the current clock state at the moment of pause.
736    ///
737    /// ## Example Request
738    /// ```json
739    /// {
740    ///   "jsonrpc": "2.0",
741    ///   "id": 1,
742    ///   "method": "surfnet_pauseClock",
743    ///   "params": []
744    /// }
745    /// ```
746    ///
747    /// ## Example Response
748    /// ```json
749    /// {
750    ///   "jsonrpc": "2.0",
751    ///   "result": {
752    ///     "epoch": 512,
753    ///     "slot_index": 0,
754    ///     "slots_in_epoch": 432000,
755    ///     "absolute_slot": 221184000,
756    ///     "block_height": 650000000,
757    ///     "transaction_count": 923472834
758    ///   },
759    ///   "id": 1
760    /// }
761    /// ```
762    #[rpc(meta, name = "surfnet_pauseClock")]
763    fn pause_clock(&self, meta: Self::Metadata) -> Result<EpochInfo>;
764
765    /// A cheat code to resume Solana clock progression after it was paused.
766    /// The validator will start producing new slots again.
767    ///
768    /// ## Parameters
769    ///
770    /// ## Returns
771    /// An `EpochInfo` object reflecting the resumed clock state.
772    ///
773    /// ## Example Request
774    /// ```json
775    /// {
776    ///   "jsonrpc": "2.0",
777    ///   "id": 1,
778    ///   "method": "surfnet_resumeClock",
779    ///   "params": []
780    /// }
781    /// ```
782    ///
783    /// ## Example Response
784    /// ```json
785    /// {
786    ///   "jsonrpc": "2.0",
787    ///   "result": {
788    ///     "epoch": 512,
789    ///     "slot_index": 0,
790    ///     "slots_in_epoch": 432000,
791    ///     "absolute_slot": 221184000,
792    ///     "block_height": 650000000,
793    ///     "transaction_count": 923472834
794    ///   },
795    ///   "id": 1
796    /// }
797    /// ```
798    #[rpc(meta, name = "surfnet_resumeClock")]
799    fn resume_clock(&self, meta: Self::Metadata) -> Result<EpochInfo>;
800
801    /// A cheat code to reset an account on the local network.
802    ///
803    /// ## Parameters
804    /// - `pubkey_str`: The base-58 encoded public key of the account to reset.
805    /// - `config`: A `ResetAccountConfig` specifying how to reset the account. If omitted, the account will be reset without cascading to owned accounts.
806    ///
807    /// ## Returns
808    /// An `RpcResponse<()>` indicating whether the account reset was successful.
809    ///
810    /// ## Example Request
811    /// ```json
812    /// {
813    ///   "jsonrpc": "2.0",
814    ///   "id": 1,
815    ///   "method": "surfnet_resetAccount",
816    ///   "params": [ "4EXSeLGxVBpAZwq7vm6evLdewpcvE2H56fpqL2pPiLFa", { "includeOwnedAccounts": true } ]
817    /// }
818    /// ```
819    ///
820    /// ## Example Response
821    /// ```json
822    /// {
823    ///   "jsonrpc": "2.0",
824    ///   "result": {
825    ///     "context": {
826    ///       "slot": 123456789,
827    ///       "apiVersion": "2.3.8"
828    ///     },
829    ///     "value": null
830    ///   },
831    ///   "id": 1
832    /// }
833    /// ```
834    #[rpc(meta, name = "surfnet_resetAccount")]
835    fn reset_account(
836        &self,
837        meta: Self::Metadata,
838        pubkey_str: String,
839        config: Option<ResetAccountConfig>,
840    ) -> Result<RpcResponse<()>>;
841
842    /// A cheat code to reset a network.
843    ///
844    /// ## Returns
845    /// An `RpcResponse<()>` indicating whether the network reset was successful.
846    ///
847    /// ## Example Request
848    /// ```json
849    /// {
850    ///   "jsonrpc": "2.0",
851    ///   "id": 1,
852    ///   "method": "surfnet_resetNetwork",    /// }
853    /// ```
854    ///
855    /// ## Example Response
856    /// ```json
857    /// {
858    ///   "jsonrpc": "2.0",
859    ///   "result": {
860    ///     "context": {
861    ///       "slot": 123456789,
862    ///       "apiVersion": "2.3.8"
863    ///     },
864    ///     "value": null    ///   },
865    ///   "id": 1
866    /// }
867    /// ```
868    ///
869
870    #[rpc(meta, name = "surfnet_resetNetwork")]
871    fn reset_network(&self, meta: Self::Metadata) -> BoxFuture<Result<RpcResponse<()>>>;
872
873    /// A cheat code to prevent an account from being downloaded from the remote RPC.
874    ///
875    /// ## Parameters
876    /// - `pubkey_str`: The base-58 encoded public key of the account/program to block.
877    /// - `config`: A `OfflineAccountConfig` specifying whether to also mark accounts offline
878    ///   owned by this pubkey. If omitted, only the account itself is marked offline.
879    ///
880    /// ## Returns
881    /// An `RpcResponse<()>` indicating whether the download block registration was successful.
882    ///
883    /// ## Example Request
884    /// ```json
885    /// {
886    ///   "jsonrpc": "2.0",
887    ///   "id": 1,
888    ///   "method": "surfnet_offlineAccount",
889    ///   "params": [ "4EXSeLGxVBpAZwq7vm6evLdewpcvE2H56fpqL2pPiLFa", { "includeOwnedAccounts": true } ]
890    /// }
891    /// ```
892    ///
893    /// ## Example Response
894    /// ```json
895    /// {
896    ///   "jsonrpc": "2.0",
897    ///   "result": {
898    ///     "context": {
899    ///       "slot": 123456789,
900    ///       "apiVersion": "2.3.8"
901    ///     },
902    ///     "value": null
903    ///   },
904    ///   "id": 1
905    /// }
906    /// ```
907    #[rpc(meta, name = "surfnet_offlineAccount")]
908    fn offline_account(
909        &self,
910        meta: Self::Metadata,
911        pubkey_str: String,
912        config: Option<OfflineAccountConfig>,
913    ) -> BoxFuture<Result<RpcResponse<()>>>;
914
915    /// A cheat code to export a snapshot of all accounts in the Surfnet SVM.
916    ///
917    /// This method retrieves the current state of all accounts stored in the Surfnet Virtual Machine (SVM)
918    /// and returns them as a mapping of account public keys to their respective account snapshots.
919    ///
920    /// ## Parameters
921    /// - `config`: An optional `ExportSnapshotConfig` to customize the export behavior. The config fields are:
922    ///     - `includeParsedAccounts`: If true, includes parsed account data in the snapshot.
923    ///     - `filter`: An optional filter config to limit which accounts are included in the snapshot. Fields include:
924    ///         - `includeProgramAccounts`: A boolean indicating whether to include program accounts.
925    ///         - `includeAccounts`: A list of specific account public keys to include.
926    ///         - `excludeAccounts`: A list of specific account public keys to exclude.
927    ///     - `scope`: An optional scope to limit the accounts included in the snapshot. Options include:
928    ///         - `network`: Includes all accounts in the network.
929    ///         - `preTransaction`: Only includes accounts touched by the given transaction.
930    ///
931    ///
932    /// ## Returns
933    /// An `RpcResponse<BTreeMap<String, AccountSnapshot>>` containing the exported account snapshots.
934    ///
935    /// The keys of the map are the base-58 encoded public keys of the accounts,
936    /// and the values are the corresponding `AccountSnapshot` objects.
937    ///
938    /// ## Example Request
939    /// ```json
940    /// {
941    ///   "jsonrpc": "2.0",
942    ///   "id": 1,
943    ///   "method": "surfnet_exportSnapshot"
944    /// }
945    /// ```
946    ///
947    /// ## Example Response
948    /// ```json
949    /// {
950    ///   "jsonrpc": "2.0",
951    ///   "result": {
952    ///     "4EXSeLGxVBpAZwq7vm6evLdewpcvE2H56fpqL2pPiLFa": {
953    ///       "lamports": 1000000,
954    ///       "owner": "11111111111111111111111111111111",
955    ///       "executable": false,
956    ///       "rent_epoch": 0,
957    ///       "data": "base64_encoded_data_string"
958    ///     },
959    ///     "AnotherAccountPubkeyBase58": {
960    ///       "lamports": 500000,
961    ///       "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
962    ///       "executable": false,
963    ///       "rent_epoch": 0,
964    ///       "data": "base64_encoded_data_string"
965    ///     }
966    ///   },
967    ///   "id": 1
968    /// }
969    /// ```
970    ///
971    #[rpc(meta, name = "surfnet_exportSnapshot")]
972    fn export_snapshot(
973        &self,
974        meta: Self::Metadata,
975        config: Option<ExportSnapshotConfig>,
976    ) -> Result<RpcResponse<BTreeMap<String, AccountSnapshot>>>;
977
978    /// A cheat code to simulate account streaming.
979    /// When a transaction is processed, the accounts that are accessed are downloaded from the datasource and cached in the SVM.
980    /// With this method, you can simulate the streaming of accounts by providing a pubkey.
981    ///
982    /// ## Parameters
983    /// - `pubkey_str`: The base-58 encoded public key of the account to stream.
984    /// - `config`: A `StreamAccountConfig` specifying how to stream the account. If omitted, the account will be streamed without cascading to owned accounts.
985    ///
986    /// ## Returns
987    /// An `RpcResponse<()>` indicating whether the account stream registration was successful.
988    ///
989    /// ## Example Request
990    /// ```json
991    /// {
992    ///   "jsonrpc": "2.0",
993    ///   "id": 1,
994    ///   "method": "surfnet_streamAccount",
995    ///   "params": [ "4EXSeLGxVBpAZwq7vm6evLdewpcvE2H56fpqL2pPiLFa", { "includeOwnedAccounts": true } ]
996    /// }
997    /// ```
998    ///
999    /// ## Example Response
1000    /// ```json
1001    /// {
1002    ///   "jsonrpc": "2.0",
1003    ///   "result": {
1004    ///     "context": {
1005    ///       "slot": 123456789,
1006    ///       "apiVersion": "2.3.8"
1007    ///     },
1008    ///     "value": null
1009    ///   },
1010    ///   "id": 1
1011    /// }
1012    /// ```
1013    #[rpc(meta, name = "surfnet_streamAccount")]
1014    fn stream_account(
1015        &self,
1016        meta: Self::Metadata,
1017        pubkey_str: String,
1018        config: Option<StreamAccountConfig>,
1019    ) -> Result<RpcResponse<()>>;
1020
1021    /// A cheat code to simulate account streaming for multiple accounts at once.
1022    /// When a transaction is processed, the accounts that are accessed are downloaded from the datasource and cached in the SVM.
1023    /// With this method, you can simulate the streaming of multiple accounts by providing a list of entries.
1024    ///
1025    /// ## Parameters
1026    /// - `accounts`: A list of `StreamAccountsEntry` objects, each containing a `pubkey` (base-58 encoded) and an optional `includeOwnedAccounts` boolean.
1027    ///
1028    /// ## Returns
1029    /// An `RpcResponse<()>` indicating whether the account stream registrations were successful.
1030    ///
1031    /// ## Example Request
1032    /// ```json
1033    /// {
1034    ///   "jsonrpc": "2.0",
1035    ///   "id": 1,
1036    ///   "method": "surfnet_streamAccounts",
1037    ///   "params": [
1038    ///     [
1039    ///       { "pubkey": "4EXSeLGxVBpAZwq7vm6evLdewpcvE2H56fpqL2pPiLFa", "includeOwnedAccounts": true },
1040    ///       { "pubkey": "7nYBm5mk15oDNewVjNFmEqJ9VgMvT4F74UVoeYDCpScd" }
1041    ///     ]
1042    ///   ]
1043    /// }
1044    /// ```
1045    ///
1046    /// ## Example Response
1047    /// ```json
1048    /// {
1049    ///   "jsonrpc": "2.0",
1050    ///   "result": {
1051    ///     "context": {
1052    ///       "slot": 123456789,
1053    ///       "apiVersion": "2.3.8"
1054    ///     },
1055    ///     "value": null
1056    ///   },
1057    ///   "id": 1
1058    /// }
1059    /// ```
1060    #[rpc(meta, name = "surfnet_streamAccounts")]
1061    fn stream_accounts(
1062        &self,
1063        meta: Self::Metadata,
1064        accounts: Vec<StreamAccountsEntry>,
1065    ) -> Result<RpcResponse<()>>;
1066
1067    /// A cheat code to retrieve the streamed accounts.
1068    /// When a transaction is processed, the accounts that are accessed are downloaded from the datasource and cached in the SVM.
1069    /// With this method, you can simulate the streaming of accounts by providing a pubkey.
1070    ///
1071    /// ## Parameters
1072    ///
1073    /// ## Returns
1074    /// An `RpcResponse<()>` indicating whether the account stream registration was successful.
1075    ///
1076    /// ## Example Request
1077    /// ```json
1078    /// {
1079    ///   "jsonrpc": "2.0",
1080    ///   "id": 1,
1081    ///   "method": "surfnet_getStreamedAccounts",
1082    ///   "params": []
1083    /// }
1084    /// ```
1085    ///
1086    /// ## Example Response
1087    /// ```json
1088    /// {
1089    ///   "jsonrpc": "2.0",
1090    ///   "result": {
1091    ///     "context": {
1092    ///       "slot": 123456789,
1093    ///       "apiVersion": "2.3.8"
1094    ///     },
1095    ///     "value": [
1096    ///       "4EXSeLGxVBpAZwq7vm6evLdewpcvE2H56fpqL2pPiLFa"
1097    ///     ]
1098    ///    },
1099    ///   "id": 1
1100    /// }
1101    /// ```
1102    #[rpc(meta, name = "surfnet_getStreamedAccounts")]
1103    fn get_streamed_accounts(
1104        &self,
1105        meta: Self::Metadata,
1106    ) -> Result<RpcResponse<GetStreamedAccountsResponse>>;
1107
1108    /// A cheat code to get Surfnet network information.
1109    ///
1110    /// ## Returns
1111    /// A `RpcResponse<GetSurfnetInfoResponse>` containing the Surfnet network information.
1112    ///
1113    /// ## Example Request
1114    /// ```json
1115    /// {
1116    ///   "jsonrpc": "2.0",
1117    ///   "id": 1,
1118    ///   "method": "surfnet_getSurfnetInfo"
1119    /// }
1120    /// ```
1121    ///
1122    /// ## Example Response
1123    /// ```json
1124    /// {
1125    ///   "jsonrpc": "2.0",
1126    ///   "result": {
1127    ///     "context": {
1128    ///       "slot": 369027326,
1129    ///       "apiVersion": "2.3.8"
1130    ///     },
1131    ///     "value": {
1132    ///       "runbookExecutions": [
1133    ///         {
1134    ///           "startedAt": 1758747828,
1135    ///           "completedAt": 1758747828,
1136    ///           "runbookId": "deployment"
1137    ///         }
1138    ///       ]
1139    ///     }
1140    ///   },
1141    ///   "id": 1
1142    /// }
1143    /// ```
1144    ///
1145    #[rpc(meta, name = "surfnet_getSurfnetInfo")]
1146    fn get_surfnet_info(&self, meta: Self::Metadata)
1147    -> Result<RpcResponse<GetSurfnetInfoResponse>>;
1148
1149    /// A "cheat code" method for developers to write program data at a specified offset in Surfpool.
1150    ///
1151    /// This method allows developers to write large Solana programs by sending data in chunks,
1152    /// bypassing transaction size limits by using RPC size limits (5MB) instead.
1153    ///
1154    /// ## Parameters
1155    /// - `program_id`: The public key of the program account, as a base-58 encoded string.
1156    /// - `data`: Hex-encoded program data chunk to write.
1157    /// - `offset`: The byte offset at which to write this data chunk.
1158    /// - `authority` (optional): The base-58 encoded public key of the authority allowed to write to the program. If omitted, defaults to the system program.
1159    ///
1160    /// ## Returns
1161    /// A `RpcResponse<()>` indicating whether the write was successful.
1162    ///
1163    /// ## Example Request
1164    /// ```json
1165    /// {
1166    ///   "jsonrpc": "2.0",
1167    ///   "id": 1,
1168    ///   "method": "surfnet_writeProgram",
1169    ///   "params": [
1170    ///     "program_pubkey",
1171    ///     "deadbeef...",
1172    ///     0
1173    ///   ]
1174    /// }
1175    /// ```
1176    ///
1177    /// ## Example Response
1178    /// ```json
1179    /// {
1180    ///   "jsonrpc": "2.0",
1181    ///   "result": {},
1182    ///   "id": 1
1183    /// }
1184    /// ```
1185    ///
1186    /// # Notes
1187    /// This method is designed to help developers deploy large programs by writing data incrementally.
1188    /// Multiple calls can be made with different offsets to build the complete program.
1189    /// The program account and program data account will be created automatically if they don't exist.
1190    #[rpc(meta, name = "surfnet_writeProgram")]
1191    fn write_program(
1192        &self,
1193        meta: Self::Metadata,
1194        program_id: String,
1195        data: String,
1196        offset: usize,
1197        authority: Option<String>,
1198    ) -> BoxFuture<Result<RpcResponse<()>>>;
1199
1200    /// A cheat code to register a scenario with account overrides.
1201    ///
1202    /// ## Parameters
1203    /// - `scenario`: The Scenario object containing:
1204    ///   - `id`: Unique identifier for the scenario
1205    ///   - `name`: Human-readable name
1206    ///   - `description`: Description of the scenario
1207    ///   - `overrides`: Array of OverrideInstance objects, each containing:
1208    ///     - `id`: Unique identifier for this override instance
1209    ///     - `templateId`: Reference to the override template
1210    ///     - `values`: HashMap of field paths to override values
1211    ///     - `scenarioRelativeSlot`: The relative slot offset (from base slot) when this override should be applied
1212    ///     - `label`: Optional label for this override
1213    ///     - `enabled`: Whether this override is active
1214    ///     - `fetchBeforeUse`: If true, fetch fresh account data just before transaction execution (useful for price feeds, oracle updates, and dynamic balances)
1215    ///     - `account`: Account address (either `{ "pubkey": "..." }` or `{ "pda": { "programId": "...", "seeds": [...] } }`)
1216    ///   - `tags`: Array of tags for categorization
1217    /// - `slot` (optional): The base slot from which relative slot offsets are calculated. If omitted, uses the current slot.
1218    ///
1219    /// ## Returns
1220    /// A `RpcResponse<()>` indicating whether the Scenario registration was successful.
1221    ///
1222    /// ## Example Request (with slot)
1223    /// ```json
1224    /// {
1225    ///   "jsonrpc": "2.0",
1226    ///   "id": 1,
1227    ///   "method": "surfnet_registerScenario",
1228    ///   "params": [
1229    ///     {
1230    ///       "id": "scenario-1",
1231    ///       "name": "Price Feed Override",
1232    ///       "description": "Override Pyth BTC/USD price at specific slots",
1233    ///       "overrides": [
1234    ///         {
1235    ///           "id": "override-1",
1236    ///           "templateId": "pyth_btcusd",
1237    ///           "values": {
1238    ///             "price_message.price_value": 67500,
1239    ///             "price_message.conf": 100,
1240    ///             "price_message.expo": -8
1241    ///           },
1242    ///           "scenarioRelativeSlot": 100,
1243    ///           "label": "Set BTC price to $67,500",
1244    ///           "enabled": true,
1245    ///           "fetchBeforeUse": false,
1246    ///           "account": {
1247    ///             "pubkey": "H6ARHf6YXhGYeQfUzQNGk6rDNnLBQKrenN712K4QJNYH"
1248    ///           }
1249    ///         }
1250    ///       ],
1251    ///       "tags": ["defi", "price-feed"]
1252    ///     },
1253    ///     355684457
1254    ///   ]
1255    /// }
1256    /// ```
1257    /// ## Example Request (without slot)
1258    /// ```json
1259    /// {
1260    ///   "jsonrpc": "2.0",
1261    ///   "id": 1,
1262    ///   "method": "surfnet_registerScenario",
1263    ///   "params": [
1264    ///     {
1265    ///       "id": "scenario-1",
1266    ///       "name": "Price Feed Override",
1267    ///       "description": "Override Pyth BTC/USD price",
1268    ///       "overrides": [
1269    ///         {
1270    ///           "id": "override-1",
1271    ///           "templateId": "pyth_btcusd",
1272    ///           "values": {
1273    ///             "price_message.price_value": 67500
1274    ///           },
1275    ///           "scenarioRelativeSlot": 100,
1276    ///           "label": "Set BTC price",
1277    ///           "enabled": true,
1278    ///           "fetchBeforeUse": true,
1279    ///           "account": {
1280    ///             "pubkey": "H6ARHf6YXhGYeQfUzQNGk6rDNnLBQKrenN712K4QJNYH"
1281    ///           }
1282    ///         }
1283    ///       ],
1284    ///       "tags": []
1285    ///     }
1286    ///   ]
1287    /// }
1288    /// ```
1289    ///
1290    /// ## Example Response
1291    /// ```json
1292    /// {
1293    ///   "jsonrpc": "2.0",
1294    ///   "context": {
1295    ///     "slot": 355684457,
1296    ///     "apiVersion": "2.2.2"
1297    ///   },
1298    ///   "value": null,
1299    ///   "id": 1
1300    /// }
1301    /// ```
1302    #[rpc(meta, name = "surfnet_registerScenario")]
1303    fn register_scenario(
1304        &self,
1305        meta: Self::Metadata,
1306        scenario: Scenario,
1307        slot: Option<Slot>,
1308    ) -> Result<RpcResponse<()>>;
1309}
1310
1311#[derive(Clone)]
1312pub struct SurfnetCheatcodesRpc {
1313    pub registered_methods: Arc<RwLock<Vec<String>>>,
1314}
1315impl SurfnetCheatcodesRpc {
1316    pub fn empty() -> Self {
1317        Self {
1318            registered_methods: Arc::new(RwLock::new(vec![])),
1319        }
1320    }
1321    pub fn is_available_cheatcode(&self, cheatcode: &String) -> bool {
1322        let Ok(methods) = self.registered_methods.read() else {
1323            return false;
1324        };
1325        methods.contains(cheatcode)
1326    }
1327}
1328
1329impl SurfnetCheatcodes for SurfnetCheatcodesRpc {
1330    type Metadata = Option<RunloopContext>;
1331
1332    fn set_account(
1333        &self,
1334        meta: Self::Metadata,
1335        pubkey_str: String,
1336        update: AccountUpdate,
1337    ) -> BoxFuture<Result<RpcResponse<()>>> {
1338        let pubkey = match verify_pubkey(&pubkey_str) {
1339            Ok(res) => res,
1340            Err(e) => return e.into(),
1341        };
1342        let account_update_opt = match update.to_account_ext() {
1343            Err(e) => return Box::pin(future::err(e)),
1344            Ok(res) => res,
1345        };
1346
1347        let SurfnetRpcContext {
1348            svm_locker,
1349            remote_ctx,
1350        } = match meta.get_rpc_context(CommitmentConfig::confirmed()) {
1351            Ok(res) => res,
1352            Err(e) => return e.into(),
1353        };
1354
1355        Box::pin(async move {
1356            let (account_to_set, latest_absolute_slot) = if let Some(account) = account_update_opt {
1357                (
1358                    GetAccountResult::FoundAccount(pubkey, account, true),
1359                    svm_locker.get_latest_absolute_slot(),
1360                )
1361            } else {
1362                // otherwise, we need to fetch the account and apply the update
1363                let SvmAccessContext {
1364                    slot, inner: mut account_result_to_update,
1365                    ..
1366                } = svm_locker.get_account(&remote_ctx, &pubkey, Some(Box::new(move |svm_locker| {
1367
1368                            // if the account does not exist locally or in the remote, create a new account with default values
1369                            let _ = svm_locker.simnet_events_tx().send(SimnetEvent::info(format!(
1370                                "Account {pubkey} not found, creating a new account from default values"
1371                            )));
1372                            GetAccountResult::FoundAccount(
1373                                pubkey,
1374                                solana_account::Account {
1375                                    lamports: 0,
1376                                    owner: system_program::id(),
1377                                    executable: false,
1378                                    rent_epoch: 0,
1379                                    data: vec![],
1380                                },
1381                                true, // indicate that the account should be updated in the SVM, since it's new
1382                            )
1383                }))).await?;
1384
1385                update.apply_ext(&mut account_result_to_update)?;
1386                (account_result_to_update, slot)
1387            };
1388
1389            svm_locker.write_account_update(account_to_set);
1390
1391            Ok(RpcResponse {
1392                context: RpcResponseContext::new(latest_absolute_slot),
1393                value: (),
1394            })
1395        })
1396    }
1397
1398    fn disable_cheatcode(
1399        &self,
1400        meta: Self::Metadata,
1401        cheatcodes_filter: CheatcodeFilter,
1402        control_config: Option<CheatcodeControlConfig>,
1403    ) -> Result<RpcResponse<()>> {
1404        let svm_locker = match meta.get_svm_locker() {
1405            Ok(locker) => locker,
1406            Err(e) => return Err(e.into()),
1407        };
1408
1409        let CheatcodeControlConfig { lockout } = control_config.unwrap_or_default();
1410        let lockout = lockout.unwrap_or_default();
1411
1412        if let Some(runloop_ctx) = meta {
1413            let Ok(mut cheatcode_ctx) = runloop_ctx.cheatcode_config.lock() else {
1414                return Err(jsonrpc_core::Error::internal_error());
1415            };
1416
1417            if lockout {
1418                cheatcode_ctx.lockout();
1419            }
1420
1421            match cheatcodes_filter {
1422                CheatcodeFilter::All(all) => {
1423                    if all.ne("all") {
1424                        return Err(SurfpoolError::disable_cheatcode(
1425                            "Invalid option provided for disabling all cheatcodes. Try using 'all' or providing an array of specific cheatcodes".to_string(),
1426                        )
1427                        .into());
1428                    }
1429
1430                    let Ok(available_cheatcodes) = self.registered_methods.read() else {
1431                        return Err(jsonrpc_core::Error::internal_error());
1432                    };
1433                    cheatcode_ctx.disable_all(lockout, (*available_cheatcodes).clone());
1434                }
1435                CheatcodeFilter::List(cheatcodes) => {
1436                    // Validate all cheatcodes before disabling any (atomic processing)
1437                    for cheatcode in &cheatcodes {
1438                        if !lockout
1439                            && (cheatcode.eq("surfnet_enableCheatcode")
1440                                || cheatcode.eq("surfnet_disableCheatcode"))
1441                        {
1442                            return Err(SurfpoolError::disable_cheatcode(
1443                                "Cannot disable surfnet_enableCheatcode or surfnet_disableCheatcode rpc method when lockout is not enabled".to_string(),
1444                            )
1445                            .into());
1446                        }
1447                        if !self.is_available_cheatcode(cheatcode) {
1448                            return Err(SurfpoolError::disable_cheatcode(
1449                                "Invalid cheatcode rpc method".to_string(),
1450                            )
1451                            .into());
1452                        }
1453                    }
1454                    // Apply all after validation passes
1455                    for cheatcode in cheatcodes {
1456                        if let Err(e) = cheatcode_ctx.disable_cheatcode(&cheatcode) {
1457                            return Err(SurfpoolError::disable_cheatcode(e).into());
1458                        }
1459                    }
1460                }
1461            }
1462        }
1463
1464        Ok(RpcResponse {
1465            value: (),
1466            context: RpcResponseContext::new(svm_locker.get_latest_absolute_slot()),
1467        })
1468    }
1469
1470    fn enable_cheatcode(
1471        &self,
1472        meta: Self::Metadata,
1473        cheatcodes_filter: CheatcodeFilter,
1474    ) -> Result<RpcResponse<()>> {
1475        let svm_locker = match meta.get_svm_locker() {
1476            Ok(locker) => locker,
1477            Err(e) => return Err(e.into()),
1478        };
1479        if let Some(runloop_ctx) = meta {
1480            let Ok(mut cheatcode_ctx) = runloop_ctx.cheatcode_config.lock() else {
1481                return Err(jsonrpc_core::Error::internal_error());
1482            };
1483            match cheatcodes_filter {
1484                CheatcodeFilter::All(all) => {
1485                    if all.ne("all") {
1486                        return Err(SurfpoolError::enable_cheatcode(
1487                            "Invalid option provided for enabling all cheatcodes. Try using 'all' or providing an array of specific cheatcodes".to_string(),
1488                        )
1489                        .into());
1490                    }
1491
1492                    // we probably don't need to check whether lockout == true because surfnet_enableCheatcode won't be called if it's disabled
1493                    cheatcode_ctx.filter = CheatcodeFilter::List(vec![]);
1494                }
1495                CheatcodeFilter::List(cheatcodes) => {
1496                    for ref cheatcode in cheatcodes {
1497                        debug!("enabling cheatcode: {cheatcode}");
1498                        if !self.is_available_cheatcode(cheatcode) {
1499                            return Err(SurfpoolError::enable_cheatcode(
1500                                "Invalid cheatcode rpc method".to_string(),
1501                            )
1502                            .into());
1503                        }
1504
1505                        if let Err(e) = cheatcode_ctx.enable_cheatcode(cheatcode) {
1506                            return Err(SurfpoolError::enable_cheatcode(e).into());
1507                        }
1508                    }
1509                }
1510            }
1511        }
1512
1513        Ok(RpcResponse {
1514            value: (),
1515            context: RpcResponseContext::new(svm_locker.get_latest_absolute_slot()),
1516        })
1517    }
1518
1519    fn set_token_account(
1520        &self,
1521        meta: Self::Metadata,
1522        owner_str: String,
1523        mint_str: String,
1524        update: TokenAccountUpdate,
1525        some_token_program_str: Option<String>,
1526    ) -> BoxFuture<Result<RpcResponse<()>>> {
1527        let owner = match verify_pubkey(&owner_str) {
1528            Ok(res) => res,
1529            Err(e) => return e.into(),
1530        };
1531
1532        let mint = match verify_pubkey(&mint_str) {
1533            Ok(res) => res,
1534            Err(e) => return e.into(),
1535        };
1536
1537        let is_native_mint = mint == spl_token_interface::native_mint::id();
1538        let token_amount = update.amount.unwrap_or(0);
1539
1540        let token_program_id = match some_token_program_str {
1541            Some(token_program_str) => match verify_pubkey(&token_program_str) {
1542                Ok(res) => res,
1543                Err(e) => return e.into(),
1544            },
1545            None => spl_token_interface::id(),
1546        };
1547
1548        let associated_token_account =
1549            get_associated_token_address_with_program_id(&owner, &mint, &token_program_id);
1550
1551        let SurfnetRpcContext {
1552            svm_locker,
1553            remote_ctx,
1554        } = match meta.get_rpc_context(CommitmentConfig::confirmed()) {
1555            Ok(res) => res,
1556            Err(e) => return e.into(),
1557        };
1558
1559        Box::pin(async move {
1560            let get_mint_result = svm_locker
1561                .get_account(&remote_ctx, &mint, None)
1562                .await?
1563                .inner;
1564            svm_locker.write_account_update(get_mint_result);
1565
1566            let minimum_rent = svm_locker.with_svm_reader(|svm_reader| {
1567                svm_reader.inner.minimum_balance_for_rent_exemption(
1568                    TokenAccount::get_packed_len_for_token_program_id(&token_program_id),
1569                )
1570            });
1571
1572            let (rent_exempt_reserve, initial_lamports) = if is_native_mint {
1573                // For native mint, we need to allocate enough lamports to cover the wrapped SOL amount
1574                (Some(minimum_rent), minimum_rent + token_amount) // 1 SOL wrapped
1575            } else {
1576                (None, minimum_rent)
1577            };
1578
1579            let SvmAccessContext {
1580                slot,
1581                inner: mut token_account,
1582                ..
1583            } = svm_locker
1584                .get_account(
1585                    &remote_ctx,
1586                    &associated_token_account,
1587                    Some(Box::new(move |_| {
1588                        let default =
1589                            TokenAccount::new(&token_program_id, owner, mint, rent_exempt_reserve);
1590                        let data = default.pack_into_vec();
1591                        GetAccountResult::FoundAccount(
1592                            associated_token_account,
1593                            Account {
1594                                lamports: initial_lamports,
1595                                owner: token_program_id,
1596                                executable: false,
1597                                rent_epoch: 0,
1598                                data,
1599                            },
1600                            true, // indicate that the account should be updated in the SVM, since it's new
1601                        )
1602                    })),
1603                )
1604                .await?;
1605
1606            let mut token_account_data = TokenAccount::unpack(token_account.expected_data())
1607                .map_err(|e| {
1608                    Error::invalid_params(format!("Failed to unpack token account data: {}", e))
1609                })?;
1610
1611            update.apply(&mut token_account_data)?;
1612
1613            let final_account_bytes = token_account_data.pack_into_vec();
1614            token_account.apply_update(|account| {
1615                // If this is a native mint, we need to adjust the lamports to match the wrapped SOL amount + rent
1616                account.lamports = initial_lamports;
1617                account.data = final_account_bytes.clone();
1618                Ok(())
1619            })?;
1620            svm_locker.write_account_update(token_account);
1621
1622            Ok(RpcResponse {
1623                context: RpcResponseContext::new(slot),
1624                value: (),
1625            })
1626        })
1627    }
1628
1629    /// Clones a program account from one program ID to another.
1630    /// A program account contains a pointer to a program data account, which is a PDA derived from the program ID.
1631    /// So, when cloning a program account, we need to clone the program data account as well.
1632    ///
1633    /// This method will:
1634    ///  1. Get the program account for the source program ID.
1635    ///  2. Get the program data account for the source program ID.
1636    ///  3. Calculate the program data address for the destination program ID.
1637    ///  4. Set the destination program account's data to point to the calculated destination program address.
1638    ///  5. Copy the source program data account to the destination program data account.
1639    fn clone_program_account(
1640        &self,
1641        meta: Self::Metadata,
1642        source_program_id: String,
1643        destination_program_id: String,
1644    ) -> BoxFuture<Result<RpcResponse<()>>> {
1645        let source_program_id = match verify_pubkey(&source_program_id) {
1646            Ok(res) => res,
1647            Err(e) => return e.into(),
1648        };
1649        let destination_program_id = match verify_pubkey(&destination_program_id) {
1650            Ok(res) => res,
1651            Err(e) => return e.into(),
1652        };
1653
1654        let SurfnetRpcContext {
1655            svm_locker,
1656            remote_ctx,
1657        } = match meta.get_rpc_context(CommitmentConfig::confirmed()) {
1658            Ok(res) => res,
1659            Err(e) => return e.into(),
1660        };
1661
1662        Box::pin(async move {
1663            let SvmAccessContext { slot, .. } = svm_locker
1664                .clone_program_account(&remote_ctx, &source_program_id, &destination_program_id)
1665                .await?;
1666
1667            Ok(RpcResponse {
1668                context: RpcResponseContext::new(slot),
1669                value: (),
1670            })
1671        })
1672    }
1673
1674    fn profile_transaction(
1675        &self,
1676        meta: Self::Metadata,
1677        transaction_data_b64: String,
1678        tag: Option<String>,
1679        config: Option<RpcProfileResultConfig>,
1680    ) -> BoxFuture<Result<RpcResponse<UiKeyedProfileResult>>> {
1681        Box::pin(async move {
1682            let transaction_bytes = STANDARD
1683                .decode(&transaction_data_b64)
1684                .map_err(|e| SurfpoolError::invalid_base64_data("transaction", e))?;
1685            let transaction: VersionedTransaction = bincode::deserialize(&transaction_bytes)
1686                .map_err(|e| SurfpoolError::deserialize_error("transaction", e))?;
1687
1688            let SurfnetRpcContext {
1689                svm_locker,
1690                remote_ctx,
1691            } = meta.get_rpc_context(CommitmentConfig::confirmed())?;
1692
1693            let SvmAccessContext {
1694                slot, inner: uuid, ..
1695            } = svm_locker
1696                .profile_transaction(&remote_ctx, transaction, tag.clone())
1697                .await?;
1698
1699            let key = UuidOrSignature::Uuid(uuid);
1700
1701            let config = config.unwrap_or_default();
1702            let ui_result = svm_locker
1703                .get_profile_result(key, &config)?
1704                .ok_or(SurfpoolError::expected_profile_not_found(&key))?;
1705
1706            Ok(RpcResponse {
1707                context: RpcResponseContext::new(slot),
1708                value: ui_result,
1709            })
1710        })
1711    }
1712
1713    fn get_profile_results_by_tag(
1714        &self,
1715        meta: Self::Metadata,
1716        tag: String,
1717        config: Option<RpcProfileResultConfig>,
1718    ) -> Result<RpcResponse<Option<Vec<UiKeyedProfileResult>>>> {
1719        let config = config.unwrap_or_default();
1720        let svm_locker = meta.get_svm_locker()?;
1721        let profiles = svm_locker.get_profile_results_by_tag(tag, &config)?;
1722        let slot = svm_locker.get_latest_absolute_slot();
1723        Ok(RpcResponse {
1724            context: RpcResponseContext::new(slot),
1725            value: profiles,
1726        })
1727    }
1728
1729    fn set_supply(
1730        &self,
1731        meta: Self::Metadata,
1732        update: SupplyUpdate,
1733    ) -> BoxFuture<Result<RpcResponse<()>>> {
1734        let svm_locker = match meta.get_svm_locker() {
1735            Ok(locker) => locker,
1736            Err(e) => return e.into(),
1737        };
1738
1739        // validate non-circulating accounts are valid pubkeys
1740        if let Some(ref non_circulating_accounts) = update.non_circulating_accounts {
1741            if let Err(e) = verify_pubkeys(non_circulating_accounts) {
1742                return e.into();
1743            }
1744        }
1745
1746        Box::pin(async move {
1747            let latest_absolute_slot = svm_locker.with_svm_writer(|svm_writer| {
1748                // update the supply fields if provided
1749                if let Some(total) = update.total {
1750                    svm_writer.total_supply = total;
1751                }
1752
1753                if let Some(circulating) = update.circulating {
1754                    svm_writer.circulating_supply = circulating;
1755                }
1756
1757                if let Some(non_circulating) = update.non_circulating {
1758                    svm_writer.non_circulating_supply = non_circulating;
1759                }
1760
1761                if let Some(ref accounts) = update.non_circulating_accounts {
1762                    svm_writer.non_circulating_accounts = accounts.clone();
1763                }
1764
1765                svm_writer.get_latest_absolute_slot()
1766            });
1767
1768            Ok(RpcResponse {
1769                context: RpcResponseContext::new(latest_absolute_slot),
1770                value: (),
1771            })
1772        })
1773    }
1774
1775    fn set_program_authority(
1776        &self,
1777        meta: Self::Metadata,
1778        program_id_str: String,
1779        new_authority_str: Option<String>,
1780    ) -> BoxFuture<Result<RpcResponse<()>>> {
1781        let program_id = match verify_pubkey(&program_id_str) {
1782            Ok(res) => res,
1783            Err(e) => return e.into(),
1784        };
1785        let new_authority = if let Some(ref new_authority_str) = new_authority_str {
1786            match verify_pubkey(new_authority_str) {
1787                Ok(res) => Some(res),
1788                Err(e) => return e.into(),
1789            }
1790        } else {
1791            None
1792        };
1793
1794        let SurfnetRpcContext {
1795            svm_locker,
1796            remote_ctx,
1797        } = match meta.get_rpc_context(CommitmentConfig::confirmed()) {
1798            Ok(res) => res,
1799            Err(e) => return e.into(),
1800        };
1801        Box::pin(async move {
1802            let SvmAccessContext { slot, .. } = svm_locker
1803                .set_program_authority(&remote_ctx, program_id, new_authority)
1804                .await?;
1805
1806            Ok(RpcResponse {
1807                context: RpcResponseContext::new(slot),
1808                value: (),
1809            })
1810        })
1811    }
1812
1813    fn get_transaction_profile(
1814        &self,
1815        meta: Self::Metadata,
1816        signature_or_uuid: UuidOrSignature,
1817        config: Option<RpcProfileResultConfig>,
1818    ) -> Result<RpcResponse<Option<UiKeyedProfileResult>>> {
1819        let config = config.unwrap_or_default();
1820        let svm_locker = meta.get_svm_locker()?;
1821        let profile_result = svm_locker.get_profile_result(signature_or_uuid, &config)?;
1822        let context_slot = profile_result
1823            .as_ref()
1824            .map(|pr| pr.slot)
1825            .unwrap_or_else(|| svm_locker.get_latest_absolute_slot());
1826        Ok(RpcResponse {
1827            context: RpcResponseContext::new(context_slot),
1828            value: profile_result,
1829        })
1830    }
1831
1832    fn register_idl(
1833        &self,
1834        meta: Self::Metadata,
1835        idl: Idl,
1836        slot: Option<Slot>,
1837    ) -> Result<RpcResponse<()>> {
1838        let svm_locker = match meta.get_svm_locker() {
1839            Ok(locker) => locker,
1840            Err(e) => return Err(e.into()),
1841        };
1842        svm_locker.register_idl(idl, slot)?;
1843        Ok(RpcResponse {
1844            context: RpcResponseContext::new(svm_locker.get_latest_absolute_slot()),
1845            value: (),
1846        })
1847    }
1848
1849    fn get_idl(
1850        &self,
1851        meta: Self::Metadata,
1852        program_id: String,
1853        slot: Option<Slot>,
1854    ) -> Result<RpcResponse<Option<Idl>>> {
1855        let svm_locker = match meta.get_svm_locker() {
1856            Ok(locker) => locker,
1857            Err(e) => return Err(e.into()),
1858        };
1859        let program_id = match verify_pubkey(&program_id) {
1860            Ok(pk) => pk,
1861            Err(e) => return Err(e.into()),
1862        };
1863        let idl = svm_locker.get_idl(&program_id, slot);
1864        let slot = slot.unwrap_or_else(|| svm_locker.get_latest_absolute_slot());
1865        Ok(RpcResponse {
1866            context: RpcResponseContext::new(slot),
1867            value: idl,
1868        })
1869    }
1870
1871    fn get_local_signatures(
1872        &self,
1873        meta: Self::Metadata,
1874        limit: Option<u64>,
1875    ) -> BoxFuture<Result<RpcResponse<Vec<RpcLogsResponse>>>> {
1876        let svm_locker = match meta.get_svm_locker() {
1877            Ok(locker) => locker,
1878            Err(e) => return e.into(),
1879        };
1880
1881        let limit = limit.unwrap_or(50);
1882        let latest = svm_locker.get_latest_absolute_slot();
1883        if limit == 0 {
1884            return Box::pin(async move {
1885                Ok(RpcResponse {
1886                    context: RpcResponseContext::new(latest),
1887                    value: Vec::new(),
1888                })
1889            });
1890        }
1891
1892        let mut items: Vec<(
1893            String,
1894            Slot,
1895            Option<solana_transaction_error::TransactionError>,
1896            Vec<String>,
1897        )> = svm_locker.with_svm_reader(|svm_reader| {
1898            svm_reader
1899                .transactions
1900                .into_iter()
1901                .map(|iter| {
1902                    iter.map(|(sig, status)| {
1903                        let (transaction_with_status_meta, _) = status.expect_processed();
1904                        (
1905                            sig,
1906                            transaction_with_status_meta.slot,
1907                            transaction_with_status_meta.meta.status.clone().err(),
1908                            transaction_with_status_meta
1909                                .meta
1910                                .log_messages
1911                                .clone()
1912                                .unwrap_or_default(),
1913                        )
1914                    })
1915                    .collect()
1916                })
1917                .unwrap_or_default()
1918        });
1919
1920        items.sort_by(|a, b| b.1.cmp(&a.1));
1921        items.truncate(limit as usize);
1922
1923        let value: Vec<RpcLogsResponse> = items
1924            .into_iter()
1925            .map(|(signature, _slot, err, logs)| RpcLogsResponse {
1926                signature,
1927                err: err.map(|e| e.into()),
1928                logs,
1929            })
1930            .collect();
1931
1932        Box::pin(async move {
1933            Ok(RpcResponse {
1934                context: RpcResponseContext::new(latest),
1935                value,
1936            })
1937        })
1938    }
1939
1940    fn pause_clock(&self, meta: Self::Metadata) -> Result<EpochInfo> {
1941        let key = meta.as_ref().map(|ctx| ctx.id.clone()).unwrap_or_default();
1942        let surfnet_command_tx: crossbeam_channel::Sender<SimnetCommand> =
1943            meta.get_surfnet_command_tx()?;
1944
1945        // Create a channel to receive confirmation
1946        let (response_tx, response_rx) = crossbeam_channel::bounded(1);
1947
1948        // Send pause command with confirmation
1949        let _ = surfnet_command_tx.send(SimnetCommand::CommandClock(
1950            key,
1951            ClockCommand::PauseWithConfirmation(response_tx),
1952        ));
1953
1954        // Wait for confirmation with timeout
1955        response_rx
1956            .recv_timeout(std::time::Duration::from_secs(2))
1957            .map_err(|e| jsonrpc_core::Error {
1958                code: jsonrpc_core::ErrorCode::InternalError,
1959                message: format!("Failed to confirm clock pause: {}", e),
1960                data: None,
1961            })
1962    }
1963
1964    fn resume_clock(&self, meta: Self::Metadata) -> Result<EpochInfo> {
1965        let key = meta.as_ref().map(|ctx| ctx.id.clone()).unwrap_or_default();
1966        let surfnet_command_tx: crossbeam_channel::Sender<SimnetCommand> =
1967            meta.get_surfnet_command_tx()?;
1968        let _ = surfnet_command_tx.send(SimnetCommand::CommandClock(key, ClockCommand::Resume));
1969        meta.with_svm_reader(|svm_reader| svm_reader.latest_epoch_info.clone())
1970            .map_err(Into::into)
1971    }
1972
1973    fn time_travel(
1974        &self,
1975        meta: Self::Metadata,
1976        config: Option<TimeTravelConfig>,
1977    ) -> Result<EpochInfo> {
1978        let key = meta.as_ref().map(|ctx| ctx.id.clone()).unwrap_or_default();
1979        let time_travel_config = config.unwrap_or_default();
1980        let simnet_command_tx = meta.get_surfnet_command_tx()?;
1981        let svm_locker = meta.get_svm_locker()?;
1982
1983        let epoch_info = svm_locker.time_travel(key, simnet_command_tx, time_travel_config)?;
1984
1985        Ok(epoch_info)
1986    }
1987
1988    fn reset_account(
1989        &self,
1990        meta: Self::Metadata,
1991        pubkey: String,
1992        config: Option<ResetAccountConfig>,
1993    ) -> Result<RpcResponse<()>> {
1994        let svm_locker = meta.get_svm_locker()?;
1995        let pubkey = verify_pubkey(&pubkey)?;
1996        let config = config.unwrap_or_default();
1997        let include_owned_accounts = config.include_owned_accounts.unwrap_or_default();
1998        svm_locker.reset_account(pubkey, include_owned_accounts)?;
1999        Ok(RpcResponse {
2000            context: RpcResponseContext::new(svm_locker.get_latest_absolute_slot()),
2001            value: (),
2002        })
2003    }
2004
2005    fn reset_network(&self, meta: Self::Metadata) -> BoxFuture<Result<RpcResponse<()>>> {
2006        let SurfnetRpcContext {
2007            svm_locker,
2008            remote_ctx,
2009        } = match meta.get_rpc_context(CommitmentConfig::confirmed()) {
2010            Ok(res) => res,
2011            Err(e) => return e.into(),
2012        };
2013
2014        // Extract just the remote client from the tuple (ignore commitment config)
2015        let remote_client = remote_ctx.as_ref().map(|(client, _)| client.clone());
2016
2017        Box::pin(async move {
2018            svm_locker.reset_network(&remote_client).await?;
2019            Ok(RpcResponse {
2020                context: RpcResponseContext::new(svm_locker.get_latest_absolute_slot()),
2021                value: (),
2022            })
2023        })
2024    }
2025
2026    fn offline_account(
2027        &self,
2028        meta: Self::Metadata,
2029        pubkey_str: String,
2030        config: Option<OfflineAccountConfig>,
2031    ) -> BoxFuture<Result<RpcResponse<()>>> {
2032        let SurfnetRpcContext { svm_locker, .. } =
2033            match meta.get_rpc_context(CommitmentConfig::confirmed()) {
2034                Ok(res) => res,
2035                Err(e) => return e.into(),
2036            };
2037        let pubkey = match verify_pubkey(&pubkey_str) {
2038            Ok(res) => res,
2039            Err(e) => return e.into(),
2040        };
2041        let config = config.unwrap_or_default();
2042        let include_owned_accounts = config.include_owned_accounts.unwrap_or_default();
2043
2044        Box::pin(async move {
2045            svm_locker
2046                .insert_offline_account(pubkey, include_owned_accounts)
2047                .await?;
2048            Ok(RpcResponse {
2049                context: RpcResponseContext::new(svm_locker.get_latest_absolute_slot()),
2050                value: (),
2051            })
2052        })
2053    }
2054
2055    fn stream_account(
2056        &self,
2057        meta: Self::Metadata,
2058        pubkey_str: String,
2059        config: Option<StreamAccountConfig>,
2060    ) -> Result<RpcResponse<()>> {
2061        let svm_locker = meta.get_svm_locker()?;
2062        let pubkey = verify_pubkey(&pubkey_str)?;
2063        let config = config.unwrap_or_default();
2064        let include_owned_accounts = config.include_owned_accounts.unwrap_or_default();
2065        svm_locker.stream_account(pubkey, include_owned_accounts)?;
2066        Ok(RpcResponse {
2067            context: RpcResponseContext::new(svm_locker.get_latest_absolute_slot()),
2068            value: (),
2069        })
2070    }
2071
2072    fn stream_accounts(
2073        &self,
2074        meta: Self::Metadata,
2075        accounts: Vec<StreamAccountsEntry>,
2076    ) -> Result<RpcResponse<()>> {
2077        let svm_locker = meta.get_svm_locker()?;
2078        for entry in accounts {
2079            let pubkey = verify_pubkey(&entry.pubkey)?;
2080            let include_owned_accounts = entry.include_owned_accounts.unwrap_or_default();
2081            svm_locker.stream_account(pubkey, include_owned_accounts)?;
2082        }
2083        Ok(RpcResponse {
2084            context: RpcResponseContext::new(svm_locker.get_latest_absolute_slot()),
2085            value: (),
2086        })
2087    }
2088
2089    fn get_streamed_accounts(
2090        &self,
2091        meta: Self::Metadata,
2092    ) -> Result<RpcResponse<GetStreamedAccountsResponse>> {
2093        let svm_locker = meta.get_svm_locker()?;
2094
2095        let value = svm_locker.with_svm_reader(|svm_reader| {
2096            let accounts: Vec<_> = svm_reader
2097                .streamed_accounts
2098                .into_iter()
2099                .map(|iter| iter.collect())
2100                .unwrap_or_default();
2101            GetStreamedAccountsResponse::from_iter(accounts)
2102        });
2103
2104        Ok(RpcResponse {
2105            context: RpcResponseContext::new(svm_locker.get_latest_absolute_slot()),
2106            value,
2107        })
2108    }
2109
2110    fn get_surfnet_info(
2111        &self,
2112        meta: Self::Metadata,
2113    ) -> Result<RpcResponse<GetSurfnetInfoResponse>> {
2114        let svm_locker = meta.get_svm_locker()?;
2115        let runbook_executions = svm_locker.runbook_executions();
2116        Ok(RpcResponse {
2117            context: RpcResponseContext::new(svm_locker.get_latest_absolute_slot()),
2118            value: GetSurfnetInfoResponse::new(runbook_executions),
2119        })
2120    }
2121
2122    fn write_program(
2123        &self,
2124        meta: Self::Metadata,
2125        program_id_str: String,
2126        data_hex: String,
2127        offset: usize,
2128        authority: Option<String>,
2129    ) -> BoxFuture<Result<RpcResponse<()>>> {
2130        // Validate program_id
2131        let program_id = match verify_pubkey(&program_id_str) {
2132            Ok(res) => res,
2133            Err(e) => return e.into(),
2134        };
2135
2136        let authority = if let Some(ref authority_str) = authority {
2137            match verify_pubkey(authority_str) {
2138                Ok(res) => Some(res),
2139                Err(e) => return e.into(),
2140            }
2141        } else {
2142            None
2143        };
2144
2145        // Decode hex data
2146        let data = match hex::decode(&data_hex) {
2147            Ok(data) => data,
2148            Err(e) => {
2149                return Box::pin(future::err(Error::invalid_params(format!(
2150                    "Invalid hex data provided: {}",
2151                    e
2152                ))));
2153            }
2154        };
2155
2156        // Validate offset doesn't cause overflow
2157        if offset.checked_add(data.len()).is_none() {
2158            return Box::pin(future::err(Error::invalid_params(
2159                "Offset + data length causes integer overflow",
2160            )));
2161        }
2162
2163        let SurfnetRpcContext {
2164            svm_locker,
2165            remote_ctx,
2166        } = match meta.get_rpc_context(CommitmentConfig::confirmed()) {
2167            Ok(res) => res,
2168            Err(e) => return e.into(),
2169        };
2170
2171        Box::pin(async move {
2172            let slot = svm_locker.get_latest_absolute_slot();
2173
2174            svm_locker
2175                .write_program(program_id, authority, offset, &data, &remote_ctx)
2176                .await?;
2177
2178            Ok(RpcResponse {
2179                context: RpcResponseContext::new(slot),
2180                value: (),
2181            })
2182        })
2183    }
2184
2185    fn export_snapshot(
2186        &self,
2187        meta: Self::Metadata,
2188        config: Option<ExportSnapshotConfig>,
2189    ) -> Result<RpcResponse<BTreeMap<String, AccountSnapshot>>> {
2190        let config = config.unwrap_or_default();
2191        let svm_locker = meta.get_svm_locker()?;
2192        let snapshot = svm_locker.export_snapshot(config)?;
2193        Ok(RpcResponse {
2194            context: RpcResponseContext::new(svm_locker.get_latest_absolute_slot()),
2195            value: snapshot,
2196        })
2197    }
2198
2199    fn register_scenario(
2200        &self,
2201        meta: Self::Metadata,
2202        scenario: Scenario,
2203        slot: Option<Slot>,
2204    ) -> Result<RpcResponse<()>> {
2205        let svm_locker = meta.get_svm_locker()?;
2206        svm_locker.register_scenario(scenario, slot)?;
2207        Ok(RpcResponse {
2208            context: RpcResponseContext::new(svm_locker.get_latest_absolute_slot()),
2209            value: (),
2210        })
2211    }
2212}
2213
2214#[cfg(test)]
2215mod tests {
2216    use solana_account_decoder::{
2217        UiAccountData, UiAccountEncoding, parse_account_data::ParsedAccount,
2218    };
2219    use solana_keypair::Keypair;
2220    use solana_program_pack::Pack;
2221    use solana_pubkey::Pubkey;
2222    use solana_signer::Signer;
2223    use solana_system_interface::instruction::create_account;
2224    use solana_transaction::{Transaction, versioned::VersionedTransaction};
2225    use spl_associated_token_account_interface::{
2226        address::get_associated_token_address_with_program_id,
2227        instruction::create_associated_token_account,
2228    };
2229    use spl_token_2022_interface::instruction::{initialize_mint2, mint_to, transfer_checked};
2230    use spl_token_interface::state::Mint;
2231    use surfpool_types::{
2232        ExportSnapshotFilter, ExportSnapshotScope, RpcProfileDepth, UiAccountChange,
2233        UiAccountProfileState,
2234    };
2235
2236    use super::*;
2237    use crate::{rpc::surfnet_cheatcodes::SurfnetCheatcodesRpc, tests::helpers::TestSetup};
2238
2239    #[tokio::test(flavor = "multi_thread")]
2240    async fn test_get_transaction_profile() {
2241        // Create connection to local validator
2242        let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
2243        let recent_blockhash = client
2244            .context
2245            .svm_locker
2246            .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
2247
2248        // Generate a new keypair for the fee payer
2249        let payer = Keypair::new();
2250
2251        let owner = Keypair::new();
2252
2253        // Generate a second keypair for the token recipient
2254        let recipient = Keypair::new();
2255
2256        // Airdrop 1 SOL to fee payer
2257        client
2258            .context
2259            .svm_locker
2260            .airdrop(&payer.pubkey(), 1_000_000_000)
2261            .unwrap()
2262            .unwrap();
2263
2264        // Airdrop 1 SOL to recipient for rent exemption
2265        client
2266            .context
2267            .svm_locker
2268            .airdrop(&recipient.pubkey(), 1_000_000_000)
2269            .unwrap()
2270            .unwrap();
2271
2272        // Generate keypair to use as address of mint
2273        let mint = Keypair::new();
2274
2275        // Get default mint account size (in bytes), no extensions enabled
2276        let mint_space = Mint::LEN;
2277        let mint_rent = client.context.svm_locker.with_svm_reader(|svm_reader| {
2278            svm_reader
2279                .inner
2280                .minimum_balance_for_rent_exemption(mint_space)
2281        });
2282
2283        // Instruction to create new account for mint (token 2022 program)
2284        let create_account_instruction = create_account(
2285            &payer.pubkey(),                 // payer
2286            &mint.pubkey(),                  // new account (mint)
2287            mint_rent,                       // lamports
2288            mint_space as u64,               // space
2289            &spl_token_2022_interface::id(), // program id
2290        );
2291
2292        // Instruction to initialize mint account data
2293        let initialize_mint_instruction = initialize_mint2(
2294            &spl_token_2022_interface::id(),
2295            &mint.pubkey(),        // mint
2296            &payer.pubkey(),       // mint authority
2297            Some(&payer.pubkey()), // freeze authority
2298            2,                     // decimals
2299        )
2300        .unwrap();
2301
2302        // Calculate the associated token account address for fee_payer
2303        let source_ata = get_associated_token_address_with_program_id(
2304            &owner.pubkey(),                 // owner
2305            &mint.pubkey(),                  // mint
2306            &spl_token_2022_interface::id(), // program_id
2307        );
2308
2309        // Instruction to create associated token account for fee_payer
2310        let create_source_ata_instruction = create_associated_token_account(
2311            &payer.pubkey(),                 // funding address
2312            &owner.pubkey(),                 // wallet address
2313            &mint.pubkey(),                  // mint address
2314            &spl_token_2022_interface::id(), // program id
2315        );
2316
2317        // Calculate the associated token account address for recipient
2318        let destination_ata = get_associated_token_address_with_program_id(
2319            &recipient.pubkey(),             // owner
2320            &mint.pubkey(),                  // mint
2321            &spl_token_2022_interface::id(), // program_id
2322        );
2323
2324        // Instruction to create associated token account for recipient
2325        let create_destination_ata_instruction = create_associated_token_account(
2326            &payer.pubkey(),                 // funding address
2327            &recipient.pubkey(),             // wallet address
2328            &mint.pubkey(),                  // mint address
2329            &spl_token_2022_interface::id(), // program id
2330        );
2331
2332        // Amount of tokens to mint (100 tokens with 2 decimal places)
2333        let amount = 10_000;
2334
2335        // Create mint_to instruction to mint tokens to the source token account
2336        let mint_to_instruction = mint_to(
2337            &spl_token_2022_interface::id(),
2338            &mint.pubkey(),     // mint
2339            &source_ata,        // destination
2340            &payer.pubkey(),    // authority
2341            &[&payer.pubkey()], // signer
2342            amount,             // amount
2343        )
2344        .unwrap();
2345
2346        // Create transaction and add instructions
2347        let transaction = Transaction::new_signed_with_payer(
2348            &[
2349                create_account_instruction,
2350                initialize_mint_instruction,
2351                create_source_ata_instruction,
2352                create_destination_ata_instruction,
2353                mint_to_instruction,
2354            ],
2355            Some(&payer.pubkey()),
2356            &[&payer, &mint],
2357            recent_blockhash,
2358        );
2359
2360        let signature = transaction.signatures[0];
2361
2362        let (status_tx, _status_rx) = crossbeam_channel::unbounded();
2363        client
2364            .context
2365            .svm_locker
2366            .process_transaction(&None, transaction.into(), status_tx.clone(), false, true)
2367            .await
2368            .unwrap();
2369
2370        // get profile and verify data
2371        {
2372            let ui_profile_result = client
2373                .rpc
2374                .get_transaction_profile(
2375                    Some(client.context.clone()),
2376                    UuidOrSignature::Signature(signature),
2377                    Some(RpcProfileResultConfig {
2378                        depth: Some(RpcProfileDepth::Instruction),
2379                        ..Default::default()
2380                    }),
2381                )
2382                .unwrap()
2383                .value
2384                .expect("missing profile result for processed transaction");
2385
2386            // instruction 1: create_account
2387            {
2388                let ix_profile = ui_profile_result
2389                    .instruction_profiles
2390                    .as_ref()
2391                    .unwrap()
2392                    .first()
2393                    .expect("instruction profile should exist");
2394                assert!(
2395                    ix_profile.error_message.is_none(),
2396                    "Profile should succeed, found error: {}",
2397                    ix_profile.error_message.as_ref().unwrap()
2398                );
2399                assert_eq!(ix_profile.compute_units_consumed, 150);
2400                assert!(ix_profile.error_message.is_none());
2401                let account_states = &ix_profile.account_states;
2402
2403                let UiAccountProfileState::Writable(sender_account_change) = account_states
2404                    .get(&payer.pubkey())
2405                    .expect("Payer account state should be present")
2406                else {
2407                    panic!("Expected account state to be Writable");
2408                };
2409
2410                match sender_account_change {
2411                    UiAccountChange::Update(before, after) => {
2412                        assert_eq!(
2413                            after.lamports,
2414                            before.lamports - mint_rent - (2 * 5000), // two signers, so 2 * 5000 for fees
2415                            "Payer account should be original balance minus rent"
2416                        );
2417                    }
2418                    other => {
2419                        panic!("Expected account state to be an Update, got: {:?}", other);
2420                    }
2421                }
2422
2423                let UiAccountProfileState::Writable(mint_account_change) = account_states
2424                    .get(&mint.pubkey())
2425                    .expect("Mint account state should be present")
2426                else {
2427                    panic!("Expected mint account state to be Writable");
2428                };
2429                match mint_account_change {
2430                    UiAccountChange::Create(mint_account) => {
2431                        assert_eq!(
2432                            mint_account.lamports, mint_rent,
2433                            "Mint account should have the correct rent amount"
2434                        );
2435                        assert_eq!(
2436                            mint_account.owner,
2437                            spl_token_2022_interface::id().to_string(),
2438                            "Mint account should be owned by the SPL Token program"
2439                        );
2440                        // initialized account data should be empty bytes
2441                        assert_eq!(
2442                        mint_account.data,
2443                        UiAccountData::Binary(
2444                            "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==".into(),
2445                            UiAccountEncoding::Base64
2446                        ),
2447                    );
2448                    }
2449                    other => {
2450                        panic!("Expected account state to be an Update, got: {:?}", other);
2451                    }
2452                }
2453            }
2454
2455            // instruction 2: initialize mint
2456            {
2457                let ix_profile = ui_profile_result
2458                    .instruction_profiles
2459                    .as_ref()
2460                    .unwrap()
2461                    .get(1)
2462                    .expect("instruction profile should exist");
2463
2464                assert!(
2465                    ix_profile.error_message.is_none(),
2466                    "Profile should succeed, found error: {}",
2467                    ix_profile.error_message.as_ref().unwrap()
2468                );
2469                assert_eq!(ix_profile.compute_units_consumed, 1230);
2470                let account_states = &ix_profile.account_states;
2471
2472                assert!(account_states.get(&payer.pubkey()).is_none());
2473
2474                let UiAccountProfileState::Writable(mint_account_change) = account_states
2475                    .get(&mint.pubkey())
2476                    .expect("Mint account state should be present")
2477                else {
2478                    panic!("Expected mint account state to be Writable");
2479                };
2480                match mint_account_change {
2481                    UiAccountChange::Update(_before, after) => {
2482                        assert_eq!(
2483                            after.lamports, mint_rent,
2484                            "Mint account should have the correct rent amount"
2485                        );
2486                        assert_eq!(
2487                            after.owner,
2488                            spl_token_2022_interface::id().to_string(),
2489                            "Mint account should be owned by the SPL Token program"
2490                        );
2491                        // initialized account data should be empty bytes
2492                        assert_eq!(
2493                            after.data,
2494                            UiAccountData::Json(ParsedAccount {
2495                                program: "spl-token-2022".to_string(),
2496                                parsed: json!({
2497                                    "info": {
2498                                        "decimals": 2,
2499                                        "freezeAuthority": payer.pubkey().to_string(),
2500                                        "mintAuthority": payer.pubkey().to_string(),
2501                                        "isInitialized": true,
2502                                        "supply": "0",
2503                                    },
2504                                    "type": "mint"
2505                                }),
2506                                space: 82,
2507                            }),
2508                        );
2509                    }
2510                    other => {
2511                        panic!("Expected account state to be an Update, got: {:?}", other);
2512                    }
2513                }
2514            }
2515
2516            // instruction 3: create token account
2517            {
2518                let ix_profile = ui_profile_result
2519                    .instruction_profiles
2520                    .as_ref()
2521                    .unwrap()
2522                    .get(2)
2523                    .expect("instruction profile should exist");
2524                assert!(
2525                    ix_profile.error_message.is_none(),
2526                    "Profile should succeed, found error: {}",
2527                    ix_profile.error_message.as_ref().unwrap()
2528                );
2529
2530                let account_states = &ix_profile.account_states;
2531
2532                let UiAccountProfileState::Writable(sender_account_change) = account_states
2533                    .get(&payer.pubkey())
2534                    .expect("Payer account state should be present")
2535                else {
2536                    panic!("Expected account state to be Writable");
2537                };
2538
2539                match sender_account_change {
2540                    UiAccountChange::Update(before, after) => {
2541                        assert_eq!(
2542                            after.lamports,
2543                            before.lamports - 2074080,
2544                            "Payer account should be original balance minus rent"
2545                        );
2546                    }
2547                    other => {
2548                        panic!("Expected account state to be an Update, got: {:?}", other);
2549                    }
2550                }
2551
2552                let UiAccountProfileState::Writable(mint_account_change) = account_states
2553                    .get(&mint.pubkey())
2554                    .expect("Mint account state should be present")
2555                else {
2556                    panic!("Expected mint account state to be Writable");
2557                };
2558                match mint_account_change {
2559                    UiAccountChange::Unchanged(mint_account) => {
2560                        assert!(mint_account.is_some())
2561                    }
2562                    other => {
2563                        panic!("Expected account state to be Unchanged, got: {:?}", other);
2564                    }
2565                }
2566
2567                let UiAccountProfileState::Writable(source_ata_change) = account_states
2568                    .get(&source_ata)
2569                    .expect("account state should be present")
2570                else {
2571                    panic!("Expected account state to be Writable");
2572                };
2573
2574                match source_ata_change {
2575                    UiAccountChange::Create(new) => {
2576                        assert_eq!(
2577                            new.lamports, 2074080,
2578                            "Source ATA should have the correct lamports after creation"
2579                        );
2580                        assert_eq!(
2581                            new.owner,
2582                            spl_token_2022_interface::id().to_string(),
2583                            "Source ATA should be owned by the SPL Token program"
2584                        );
2585
2586                        match &new.data {
2587                            UiAccountData::Json(parsed) => {
2588                                assert_eq!(
2589                                    parsed,
2590                                    &ParsedAccount {
2591                                        program: "spl-token-2022".into(),
2592                                        parsed: json!({
2593                                            "info": {
2594                                                "extensions": [
2595                                                    {
2596                                                        "extension": "immutableOwner"
2597                                                    }
2598                                                ],
2599                                                "isNative": false,
2600                                                "mint": mint.pubkey().to_string(),
2601                                                "owner": owner.pubkey().to_string(),
2602                                                "state": "initialized",
2603                                                "tokenAmount": {
2604                                                    "amount": "0",
2605                                                    "decimals": 2,
2606                                                    "uiAmount": 0.0,
2607                                                    "uiAmountString": "0"
2608                                                }
2609                                            },
2610                                            "type": "account"
2611                                        }),
2612                                        space: 170
2613                                    }
2614                                );
2615                            }
2616                            _ => panic!("Expected source ATA data to be JSON"),
2617                        }
2618                    }
2619                    other => {
2620                        panic!("Expected account state to be Create, got: {:?}", other);
2621                    }
2622                }
2623            }
2624
2625            // instruction 4: create destination ATA
2626            {
2627                let ix_profile = ui_profile_result
2628                    .instruction_profiles
2629                    .as_ref()
2630                    .unwrap()
2631                    .get(3)
2632                    .expect("instruction profile should exist");
2633                assert!(
2634                    ix_profile.error_message.is_none(),
2635                    "Profile should succeed, found error: {}",
2636                    ix_profile.error_message.as_ref().unwrap()
2637                );
2638
2639                let account_states = &ix_profile.account_states;
2640
2641                let UiAccountProfileState::Writable(sender_account_change) = account_states
2642                    .get(&payer.pubkey())
2643                    .expect("Payer account state should be present")
2644                else {
2645                    panic!("Expected account state to be Writable");
2646                };
2647
2648                match sender_account_change {
2649                    UiAccountChange::Update(before, after) => {
2650                        assert_eq!(
2651                            after.lamports,
2652                            before.lamports - 2074080,
2653                            "Payer account should be original balance minus rent"
2654                        );
2655                    }
2656                    other => {
2657                        panic!("Expected account state to be an Update, got: {:?}", other);
2658                    }
2659                }
2660
2661                let UiAccountProfileState::Writable(mint_account_change) = account_states
2662                    .get(&mint.pubkey())
2663                    .expect("Mint account state should be present")
2664                else {
2665                    panic!("Expected mint account state to be Writable");
2666                };
2667                match mint_account_change {
2668                    UiAccountChange::Unchanged(mint_account) => {
2669                        assert!(mint_account.is_some())
2670                    }
2671                    other => {
2672                        panic!("Expected account state to be Unchanged, got: {:?}", other);
2673                    }
2674                }
2675
2676                let UiAccountProfileState::Writable(destination_ata_change) = account_states
2677                    .get(&destination_ata)
2678                    .expect("account state should be present")
2679                else {
2680                    panic!("Expected account state to be Writable");
2681                };
2682
2683                match destination_ata_change {
2684                    UiAccountChange::Create(new) => {
2685                        assert_eq!(
2686                            new.lamports, 2074080,
2687                            "Source ATA should have the correct lamports after creation"
2688                        );
2689                        assert_eq!(
2690                            new.owner,
2691                            spl_token_2022_interface::id().to_string(),
2692                            "Source ATA should be owned by the SPL Token program"
2693                        );
2694                        match &new.data {
2695                            UiAccountData::Json(parsed) => {
2696                                assert_eq!(
2697                                    parsed,
2698                                    &ParsedAccount {
2699                                        program: "spl-token-2022".into(),
2700                                        parsed: json!({
2701                                            "info": {
2702                                                "extensions": [
2703                                                    {
2704                                                        "extension": "immutableOwner"
2705                                                    }
2706                                                ],
2707                                                "isNative": false,
2708                                                "mint": mint.pubkey().to_string(),
2709                                                "owner": recipient.pubkey().to_string(),
2710                                                "state": "initialized",
2711                                                "tokenAmount": {
2712                                                    "amount": "0",
2713                                                    "decimals": 2,
2714                                                    "uiAmount": 0.0,
2715                                                    "uiAmountString": "0"
2716                                                }
2717                                            },
2718                                            "type": "account"
2719                                        }),
2720                                        space: 170
2721                                    }
2722                                );
2723                            }
2724                            _ => panic!("Expected source ATA data to be JSON"),
2725                        }
2726                    }
2727                    other => {
2728                        panic!("Expected account state to be Create, got: {:?}", other);
2729                    }
2730                }
2731            }
2732
2733            // instruction 5: mint to
2734            {
2735                let ix_profile = ui_profile_result
2736                    .instruction_profiles
2737                    .as_ref()
2738                    .unwrap()
2739                    .get(4)
2740                    .expect("instruction profile should exist");
2741                assert!(
2742                    ix_profile.error_message.is_none(),
2743                    "Profile should succeed, found error: {}",
2744                    ix_profile.error_message.as_ref().unwrap()
2745                );
2746
2747                let account_states = &ix_profile.account_states;
2748
2749                let UiAccountProfileState::Writable(sender_account_change) = account_states
2750                    .get(&payer.pubkey())
2751                    .expect("Payer account state should be present")
2752                else {
2753                    panic!("Expected account state to be Writable");
2754                };
2755
2756                match sender_account_change {
2757                    UiAccountChange::Unchanged(unchanged) => {
2758                        assert!(unchanged.is_some(), "Payer account should remain unchanged");
2759                    }
2760                    other => {
2761                        panic!("Expected account state to be Unchanged, got: {:?}", other);
2762                    }
2763                }
2764
2765                let UiAccountProfileState::Writable(mint_account_change) = account_states
2766                    .get(&mint.pubkey())
2767                    .expect("Mint account state should be present")
2768                else {
2769                    panic!("Expected mint account state to be Writable");
2770                };
2771                match mint_account_change {
2772                    UiAccountChange::Update(before, after) => {
2773                        assert_eq!(
2774                            after.lamports, before.lamports,
2775                            "Lamports should stay the same for mint account"
2776                        );
2777                        assert_eq!(
2778                            after.data,
2779                            UiAccountData::Json(ParsedAccount {
2780                                program: "spl-token-2022".into(),
2781                                parsed: json!({
2782                                    "info": {
2783                                        "decimals": 2,
2784                                        "freezeAuthority": payer.pubkey().to_string(),
2785                                        "isInitialized": true,
2786                                        "mintAuthority": payer.pubkey().to_string(),
2787                                        "supply": "10000",
2788                                    },
2789                                    "type": "mint"
2790                                }),
2791                                space: 82
2792                            }),
2793                            "Data should stay the same for mint account"
2794                        );
2795                    }
2796                    other => {
2797                        panic!("Expected account state to be Update, got: {:?}", other);
2798                    }
2799                }
2800            }
2801
2802            assert_eq!(
2803                ui_profile_result.transaction_profile.compute_units_consumed,
2804                ui_profile_result
2805                    .instruction_profiles
2806                    .as_ref()
2807                    .unwrap()
2808                    .iter()
2809                    .map(|ix| ix.compute_units_consumed)
2810                    .sum::<u64>(),
2811            )
2812        }
2813        // Get the latest blockhash for the transfer transaction
2814        let recent_blockhash = client
2815            .context
2816            .svm_locker
2817            .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
2818
2819        // Amount of tokens to transfer (0.50 tokens with 2 decimals)
2820        let transfer_amount = 50;
2821
2822        // Create transfer_checked instruction to send tokens from source to destination
2823        let transfer_instruction = transfer_checked(
2824            &spl_token_2022_interface::id(),     // program id
2825            &source_ata,                         // source
2826            &mint.pubkey(),                      // mint
2827            &destination_ata,                    // destination
2828            &owner.pubkey(),                     // owner of source
2829            &[&payer.pubkey(), &owner.pubkey()], // signers
2830            transfer_amount,                     // amount
2831            2,                                   // decimals
2832        )
2833        .unwrap();
2834
2835        // Create transaction for transferring tokens
2836        let transaction = Transaction::new_signed_with_payer(
2837            &[transfer_instruction],
2838            Some(&payer.pubkey()),
2839            &[&payer, &owner],
2840            recent_blockhash,
2841        );
2842        let signature = transaction.signatures[0];
2843        let (status_tx, _status_rx) = crossbeam_channel::unbounded();
2844        // Send and confirm transaction
2845        client
2846            .context
2847            .svm_locker
2848            .process_transaction(&None, transaction.clone().into(), status_tx, true, true)
2849            .await
2850            .unwrap();
2851
2852        {
2853            let profile_result = client
2854                .rpc
2855                .get_transaction_profile(
2856                    Some(client.context.clone()),
2857                    UuidOrSignature::Signature(signature),
2858                    Some(RpcProfileResultConfig {
2859                        depth: Some(RpcProfileDepth::Instruction),
2860                        ..Default::default()
2861                    }),
2862                )
2863                .unwrap()
2864                .value
2865                .expect("missing profile result for processed transaction");
2866
2867            assert!(
2868                profile_result.transaction_profile.error_message.is_none(),
2869                "Transaction should succeed, found error: {}",
2870                profile_result
2871                    .transaction_profile
2872                    .error_message
2873                    .as_ref()
2874                    .unwrap()
2875            );
2876
2877            assert_eq!(
2878                profile_result.instruction_profiles.as_ref().unwrap().len(),
2879                1
2880            );
2881
2882            let ix_profile = profile_result
2883                .instruction_profiles
2884                .as_ref()
2885                .unwrap()
2886                .first()
2887                .expect("instruction profile should exist");
2888            assert!(
2889                ix_profile.error_message.is_none(),
2890                "Profile should succeed, found error: {}",
2891                ix_profile.error_message.as_ref().unwrap()
2892            );
2893
2894            let mut account_states = ix_profile.account_states.clone();
2895
2896            let UiAccountProfileState::Writable(owner_account_change) = account_states
2897                .swap_remove(&owner.pubkey())
2898                .expect("account state should be present")
2899            else {
2900                panic!("Expected account state to be Writable");
2901            };
2902
2903            let UiAccountChange::Unchanged(unchanged) = owner_account_change else {
2904                panic!(
2905                    "Expected account state to be Unchanged, got: {:?}",
2906                    owner_account_change
2907                );
2908            };
2909            assert!(unchanged.is_none(), "Owner account shouldn't exist");
2910
2911            let UiAccountProfileState::Writable(sender_account_change) = account_states
2912                .swap_remove(&payer.pubkey())
2913                .expect("Payer account state should be present")
2914            else {
2915                panic!("Expected account state to be Writable");
2916            };
2917
2918            match sender_account_change {
2919                UiAccountChange::Update(before, after) => {
2920                    assert_eq!(after.lamports, before.lamports - 10000);
2921                }
2922                other => {
2923                    panic!("Expected account state to be an Update, got: {:?}", other);
2924                }
2925            }
2926
2927            let UiAccountProfileState::Readonly = account_states
2928                .swap_remove(&mint.pubkey())
2929                .expect("Mint account state should be present")
2930            else {
2931                panic!("Expected mint account state to be Readonly");
2932            };
2933            let UiAccountProfileState::Readonly = account_states
2934                .swap_remove(&spl_token_2022_interface::ID)
2935                .expect("account state should be present")
2936            else {
2937                panic!("Expected account state to be Readonly");
2938            };
2939
2940            let UiAccountProfileState::Writable(source_ata_change) = account_states
2941                .swap_remove(&source_ata)
2942                .expect("account state should be present")
2943            else {
2944                panic!("Expected account state to be Writable");
2945            };
2946
2947            match source_ata_change {
2948                UiAccountChange::Update(before, after) => {
2949                    assert_eq!(
2950                        after.lamports, before.lamports,
2951                        "Source ATA lamports should remain unchanged"
2952                    );
2953                    assert_eq!(
2954                        after.data,
2955                        UiAccountData::Json(ParsedAccount {
2956                            program: "spl-token-2022".into(),
2957                            parsed: json!({
2958                                "info": {
2959                                    "extensions": [
2960                                        {
2961                                            "extension": "immutableOwner"
2962                                        }
2963                                    ],
2964                                    "isNative": false,
2965                                    "mint": mint.pubkey().to_string(),
2966                                    "owner": owner.pubkey().to_string(),
2967                                    "state": "initialized",
2968                                    "tokenAmount": {
2969                                        "amount": "9950",
2970                                        "decimals": 2,
2971                                        "uiAmount": 99.5,
2972                                        "uiAmountString": "99.5"
2973                                    }
2974                                },
2975                                "type": "account"
2976                            }),
2977                            space: 170
2978                        }),
2979                        "Source ATA data should be updated after transfer"
2980                    );
2981                }
2982                other => {
2983                    panic!("Expected account state to be Update, got: {:?}", other);
2984                }
2985            }
2986
2987            let UiAccountProfileState::Writable(destination_ata_change) = account_states
2988                .swap_remove(&destination_ata)
2989                .expect("account state should be present")
2990            else {
2991                panic!("Expected account state to be Writable");
2992            };
2993
2994            match destination_ata_change {
2995                UiAccountChange::Update(before, after) => {
2996                    assert_eq!(
2997                        after.lamports, before.lamports,
2998                        "Destination ATA lamports should remain unchanged"
2999                    );
3000                    assert_eq!(
3001                        after.data,
3002                        UiAccountData::Json(ParsedAccount {
3003                            program: "spl-token-2022".into(),
3004                            parsed: json!({
3005                                "info": {
3006                                    "extensions": [
3007                                        {
3008                                            "extension": "immutableOwner"
3009                                        }
3010                                    ],
3011                                    "isNative": false,
3012                                    "mint": mint.pubkey().to_string(),
3013                                    "owner": recipient.pubkey().to_string(),
3014                                    "state": "initialized",
3015                                    "tokenAmount": {
3016                                        "amount": transfer_amount.to_string(),
3017                                        "decimals": 2,
3018                                        "uiAmount": 0.5,
3019                                        "uiAmountString": "0.5"
3020                                    }
3021                                },
3022                                "type": "account"
3023                            }),
3024                            space: 170
3025                        }),
3026                        "Destination ATA data should be updated after transfer"
3027                    );
3028                }
3029                other => {
3030                    panic!("Expected account state to be Update, got: {:?}", other);
3031                }
3032            }
3033
3034            assert!(
3035                account_states.is_empty(),
3036                "All account states should have been processed, found: {:?}",
3037                account_states
3038            );
3039        }
3040    }
3041
3042    fn set_account(client: &TestSetup<SurfnetCheatcodesRpc>, pubkey: &Pubkey, account: &Account) {
3043        client
3044            .context
3045            .svm_locker
3046            .with_svm_writer(|svm| svm.inner.set_account(*pubkey, account.clone()))
3047            .expect("Failed to set account");
3048    }
3049
3050    fn verify_snapshot_account(
3051        snapshot: &BTreeMap<String, AccountSnapshot>,
3052        expected_account_pubkey: &Pubkey,
3053        expected_account: &Account,
3054    ) {
3055        let account = snapshot
3056            .get(&expected_account_pubkey.to_string())
3057            .unwrap_or_else(|| {
3058                panic!(
3059                    "Account fixture not found for pubkey {}",
3060                    expected_account_pubkey
3061                )
3062            });
3063        assert_eq!(expected_account.lamports, account.lamports);
3064        assert_eq!(
3065            base64::engine::general_purpose::STANDARD.encode(&expected_account.data),
3066            account.data
3067        );
3068        assert_eq!(expected_account.owner.to_string(), account.owner);
3069        assert_eq!(expected_account.executable, account.executable);
3070        assert_eq!(expected_account.rent_epoch, account.rent_epoch);
3071    }
3072
3073    #[test]
3074    fn test_export_snapshot() {
3075        let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3076
3077        let pubkey1 = Pubkey::new_unique();
3078        let account1 = Account {
3079            lamports: 1_000_000,
3080            data: vec![1, 2, 3, 4],
3081            owner: system_program::id(),
3082            executable: false,
3083            rent_epoch: 0,
3084        };
3085
3086        set_account(&client, &pubkey1, &account1);
3087
3088        let pubkey2 = Pubkey::new_unique();
3089        let account2 = Account {
3090            lamports: 2_000_000,
3091            data: vec![5, 6, 7, 8, 9],
3092            owner: system_program::id(),
3093            executable: false,
3094            rent_epoch: 0,
3095        };
3096
3097        set_account(&client, &pubkey2, &account2);
3098
3099        let snapshot = client
3100            .rpc
3101            .export_snapshot(Some(client.context.clone()), None)
3102            .expect("Failed to export snapshot")
3103            .value;
3104
3105        verify_snapshot_account(&snapshot, &pubkey1, &account1);
3106        verify_snapshot_account(&snapshot, &pubkey2, &account2);
3107    }
3108
3109    #[test]
3110    fn test_export_snapshot_json_parsed() {
3111        let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3112
3113        let pubkey1 = Pubkey::new_unique();
3114        println!("Pubkey1: {}", pubkey1);
3115        let account1 = Account {
3116            lamports: 1_000_000,
3117            data: vec![1, 2, 3, 4],
3118            owner: system_program::id(),
3119            executable: false,
3120            rent_epoch: 0,
3121        };
3122
3123        set_account(&client, &pubkey1, &account1);
3124
3125        let mint_pubkey = Pubkey::new_unique();
3126        println!("Mint Pubkey: {}", mint_pubkey);
3127        let mint_authority = Pubkey::new_unique();
3128
3129        let mut mint_data = [0u8; Mint::LEN];
3130        let mint = Mint {
3131            mint_authority: COption::Some(mint_authority),
3132            supply: 1000,
3133            decimals: 6,
3134            is_initialized: true,
3135            freeze_authority: COption::None,
3136        };
3137        mint.pack_into_slice(&mut mint_data);
3138
3139        let mint_account = Account {
3140            lamports: 1_000_000,
3141            data: mint_data.to_vec(),
3142            owner: spl_token_interface::id(),
3143            executable: false,
3144            rent_epoch: 0,
3145        };
3146
3147        set_account(&client, &mint_pubkey, &mint_account);
3148
3149        let snapshot = client
3150            .rpc
3151            .export_snapshot(
3152                Some(client.context.clone()),
3153                Some(ExportSnapshotConfig {
3154                    include_parsed_accounts: Some(true),
3155                    filter: None,
3156                    scope: ExportSnapshotScope::Network,
3157                }),
3158            )
3159            .expect("Failed to export snapshot")
3160            .value;
3161
3162        verify_snapshot_account(&snapshot, &pubkey1, &account1);
3163        let actual_account1 = snapshot
3164            .get(&pubkey1.to_string())
3165            .expect("Account fixture not found");
3166        assert!(
3167            actual_account1.parsed_data.is_none(),
3168            "Account1 should not have parsed data"
3169        );
3170
3171        verify_snapshot_account(&snapshot, &mint_pubkey, &mint_account);
3172        let mint_snapshot = snapshot
3173            .get(&mint_pubkey.to_string())
3174            .expect("Mint account snapshot not found");
3175        let parsed = mint_snapshot
3176            .parsed_data
3177            .as_ref()
3178            .expect("Parsed data should be present");
3179
3180        assert_eq!(parsed.program, "spl-token");
3181        assert_eq!(parsed.space, Mint::LEN as u64);
3182
3183        let parsed_info = parsed
3184            .parsed
3185            .as_object()
3186            .expect("Parsed data should be an object");
3187        let info = parsed_info
3188            .get("info")
3189            .expect("Parsed data should have info field")
3190            .as_object()
3191            .expect("Info field should be an object");
3192        assert_eq!(
3193            info.get("mintAuthority")
3194                .and_then(|v| v.as_str())
3195                .expect("mintAuthority should be a string"),
3196            mint_authority.to_string()
3197        );
3198    }
3199
3200    #[test]
3201    fn test_export_snapshot_pre_transaction() {
3202        use std::collections::HashMap;
3203
3204        use solana_signature::Signature;
3205        use surfpool_types::{ProfileResult, types::KeyedProfileResult};
3206
3207        let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3208
3209        // Create several accounts in the network
3210        let account1_pubkey = Pubkey::new_unique();
3211        let account1 = Account {
3212            lamports: 1_000_000,
3213            data: vec![1, 2, 3, 4],
3214            owner: system_program::id(),
3215            executable: false,
3216            rent_epoch: 0,
3217        };
3218        set_account(&client, &account1_pubkey, &account1);
3219
3220        let account2_pubkey = Pubkey::new_unique();
3221        let account2 = Account {
3222            lamports: 2_000_000,
3223            data: vec![5, 6, 7, 8],
3224            owner: system_program::id(),
3225            executable: false,
3226            rent_epoch: 0,
3227        };
3228        set_account(&client, &account2_pubkey, &account2);
3229
3230        let account3_pubkey = Pubkey::new_unique();
3231        let account3 = Account {
3232            lamports: 3_000_000,
3233            data: vec![9, 10, 11, 12],
3234            owner: system_program::id(),
3235            executable: false,
3236            rent_epoch: 0,
3237        };
3238        set_account(&client, &account3_pubkey, &account3);
3239
3240        // Create a mock transaction profile that only touches account1 and account2
3241        let signature = Signature::new_unique();
3242        let mut pre_execution_capture = BTreeMap::new();
3243        pre_execution_capture.insert(account1_pubkey, Some(account1.clone()));
3244        pre_execution_capture.insert(account2_pubkey, Some(account2.clone()));
3245
3246        let mut post_execution_capture = BTreeMap::new();
3247        let mut modified_account1 = account1.clone();
3248        modified_account1.lamports = 500_000;
3249        post_execution_capture.insert(account1_pubkey, Some(modified_account1.clone()));
3250        let mut modified_account2 = account2.clone();
3251        modified_account2.lamports = 2_500_000;
3252        post_execution_capture.insert(account2_pubkey, Some(modified_account2.clone()));
3253
3254        let profile = ProfileResult {
3255            pre_execution_capture,
3256            post_execution_capture,
3257            compute_units_consumed: 1000,
3258            log_messages: None,
3259            error_message: None,
3260        };
3261
3262        let keyed_profile = KeyedProfileResult::new(
3263            1,
3264            UuidOrSignature::Signature(signature),
3265            None,
3266            profile,
3267            HashMap::new(),
3268        );
3269
3270        // Insert the profile into executed_transaction_profiles
3271        client.context.svm_locker.with_svm_writer(|svm| {
3272            svm.executed_transaction_profiles
3273                .store(signature.to_string(), keyed_profile)
3274                .unwrap();
3275        });
3276
3277        // Export snapshot with PreTransaction scope
3278        let snapshot = client
3279            .rpc
3280            .export_snapshot(
3281                Some(client.context.clone()),
3282                Some(ExportSnapshotConfig {
3283                    include_parsed_accounts: Some(false),
3284                    filter: None,
3285                    scope: ExportSnapshotScope::PreTransaction(signature.to_string()),
3286                }),
3287            )
3288            .expect("Failed to export snapshot")
3289            .value;
3290
3291        // Verify that only account1 and account2 are in the snapshot
3292        assert!(
3293            snapshot.contains_key(&account1_pubkey.to_string()),
3294            "Snapshot should contain account1 (touched by transaction)"
3295        );
3296        assert!(
3297            snapshot.contains_key(&account2_pubkey.to_string()),
3298            "Snapshot should contain account2 (touched by transaction)"
3299        );
3300        assert!(
3301            !snapshot.contains_key(&account3_pubkey.to_string()),
3302            "Snapshot should NOT contain account3 (not touched by transaction)"
3303        );
3304
3305        // Verify the accounts have the PRE-EXECUTION state (original values, not modified)
3306        verify_snapshot_account(&snapshot, &account1_pubkey, &account1);
3307        verify_snapshot_account(&snapshot, &account2_pubkey, &account2);
3308
3309        // Double-check that we're NOT getting the post-execution values
3310        let snapshot_account1 = snapshot
3311            .get(&account1_pubkey.to_string())
3312            .expect("Account1 should be in snapshot");
3313        assert_eq!(
3314            snapshot_account1.lamports, 1_000_000,
3315            "Account1 should have pre-execution lamports (1M), not post-execution (500K)"
3316        );
3317
3318        let snapshot_account2 = snapshot
3319            .get(&account2_pubkey.to_string())
3320            .expect("Account2 should be in snapshot");
3321        assert_eq!(
3322            snapshot_account2.lamports, 2_000_000,
3323            "Account2 should have pre-execution lamports (2M), not post-execution (2.5M)"
3324        );
3325
3326        // Verify account count
3327        // Note: The snapshot may contain more accounts if system accounts are included
3328        // but we verify that at least our touched accounts are there and untouched ones are not
3329        println!(
3330            "Snapshot contains {} accounts (expected at least 2)",
3331            snapshot.len()
3332        );
3333    }
3334
3335    #[test]
3336    fn test_export_snapshot_filtering() {
3337        let system_account_pubkey = Pubkey::new_unique();
3338        println!("System Account Pubkey: {}", system_account_pubkey);
3339        let excluded_system_account_pubkey = Pubkey::new_unique();
3340        println!(
3341            "Excluded System Account Pubkey: {}",
3342            excluded_system_account_pubkey
3343        );
3344        let program_account_pubkey = Pubkey::new_unique();
3345        println!("Program Account Pubkey: {}", program_account_pubkey);
3346        let included_program_account_pubkey = Pubkey::new_unique();
3347        println!(
3348            "Included Program Account Pubkey: {}",
3349            included_program_account_pubkey
3350        );
3351
3352        let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3353
3354        let system_account = Account {
3355            lamports: 1_000_000,
3356            data: vec![1, 2, 3, 4],
3357            owner: system_program::id(),
3358            executable: false,
3359            rent_epoch: 0,
3360        };
3361        set_account(&client, &system_account_pubkey, &system_account);
3362        set_account(&client, &excluded_system_account_pubkey, &system_account);
3363
3364        let program_account = Account {
3365            lamports: 2_000_000,
3366            data: vec![5, 6, 7, 8, 9],
3367            owner: solana_sdk_ids::bpf_loader_upgradeable::id(),
3368            executable: false,
3369            rent_epoch: 0,
3370        };
3371        set_account(&client, &program_account_pubkey, &program_account);
3372        set_account(&client, &included_program_account_pubkey, &program_account);
3373
3374        let snapshot = client
3375            .rpc
3376            .export_snapshot(Some(client.context.clone()), None)
3377            .expect("Failed to export snapshot")
3378            .value;
3379        assert!(
3380            !snapshot.contains_key(&program_account_pubkey.to_string()),
3381            "Program account should be excluded by default"
3382        );
3383        assert!(
3384            !snapshot.contains_key(&included_program_account_pubkey.to_string()),
3385            "Program account should be excluded by default"
3386        );
3387        let snapshot = client
3388            .rpc
3389            .export_snapshot(
3390                Some(client.context.clone()),
3391                Some(ExportSnapshotConfig {
3392                    filter: Some(ExportSnapshotFilter {
3393                        include_accounts: Some(vec![included_program_account_pubkey.to_string()]),
3394                        ..Default::default()
3395                    }),
3396                    ..Default::default()
3397                }),
3398            )
3399            .expect("Failed to export snapshot")
3400            .value;
3401        assert!(
3402            !snapshot.contains_key(&program_account_pubkey.to_string()),
3403            "Program account should be excluded by default"
3404        );
3405        assert!(
3406            snapshot.contains_key(&included_program_account_pubkey.to_string()),
3407            "Program account should be included when explicitly listed"
3408        );
3409
3410        let snapshot = client
3411            .rpc
3412            .export_snapshot(
3413                Some(client.context.clone()),
3414                Some(ExportSnapshotConfig {
3415                    filter: Some(ExportSnapshotFilter {
3416                        include_program_accounts: Some(true),
3417                        exclude_accounts: Some(vec![excluded_system_account_pubkey.to_string()]),
3418                        ..Default::default()
3419                    }),
3420                    ..Default::default()
3421                }),
3422            )
3423            .expect("Failed to export snapshot")
3424            .value;
3425
3426        assert!(
3427            snapshot.contains_key(&program_account_pubkey.to_string()),
3428            "Program account should be included when filter is set"
3429        );
3430        assert!(
3431            snapshot.contains_key(&included_program_account_pubkey.to_string()),
3432            "Included program account should be present"
3433        );
3434        assert!(
3435            snapshot.contains_key(&system_account_pubkey.to_string()),
3436            "System account should be present"
3437        );
3438        assert!(
3439            !snapshot.contains_key(&excluded_system_account_pubkey.to_string()),
3440            "Excluded system account should not be present"
3441        );
3442    }
3443
3444    #[tokio::test(flavor = "multi_thread")]
3445    async fn test_write_program_creates_accounts_automatically() {
3446        // Test that both program and program data accounts are created if they don't exist
3447        let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3448        let program_id = Keypair::new();
3449
3450        // Verify accounts don't exist initially
3451        let program_data_address =
3452            solana_loader_v3_interface::get_program_data_address(&program_id.pubkey());
3453
3454        let program_account_before = client.context.svm_locker.with_svm_reader(|svm_reader| {
3455            svm_reader.inner.get_account(&program_id.pubkey()).unwrap()
3456        });
3457        assert!(
3458            program_account_before.is_none(),
3459            "Program account should not exist initially"
3460        );
3461
3462        // Write some data
3463        let program_data = vec![0xDE, 0xAD, 0xBE, 0xEF];
3464        let result = client
3465            .rpc
3466            .write_program(
3467                Some(client.context.clone()),
3468                program_id.pubkey().to_string(),
3469                hex::encode(&program_data),
3470                0,
3471                None,
3472            )
3473            .await;
3474
3475        assert!(
3476            result.is_ok(),
3477            "Failed to write program: {:?}",
3478            result.err()
3479        );
3480
3481        // Verify program account was created
3482        let program_account = client.context.svm_locker.with_svm_reader(|svm_reader| {
3483            svm_reader.inner.get_account(&program_id.pubkey()).unwrap()
3484        });
3485        assert!(
3486            program_account.is_some(),
3487            "Program account should be created"
3488        );
3489
3490        let program_account = program_account.unwrap();
3491        assert_eq!(
3492            program_account.owner,
3493            solana_sdk_ids::bpf_loader_upgradeable::id()
3494        );
3495        assert!(
3496            program_account.executable,
3497            "Program account should be executable"
3498        );
3499
3500        // Verify program data account was created
3501        let program_data_account = client.context.svm_locker.with_svm_reader(|svm_reader| {
3502            svm_reader.inner.get_account(&program_data_address).unwrap()
3503        });
3504        assert!(
3505            program_data_account.is_some(),
3506            "Program data account should be created"
3507        );
3508
3509        let program_data_account = program_data_account.unwrap();
3510        assert_eq!(
3511            program_data_account.owner,
3512            solana_sdk_ids::bpf_loader_upgradeable::id()
3513        );
3514        assert!(
3515            !program_data_account.executable,
3516            "Program data account should not be executable"
3517        );
3518
3519        println!("✅ Both accounts created successfully");
3520    }
3521
3522    #[tokio::test(flavor = "multi_thread")]
3523    async fn test_write_program_single_chunk_small() {
3524        // Test writing a small program in a single write
3525        let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3526        let program_id = Keypair::new();
3527
3528        let program_data = vec![0x01, 0x02, 0x03, 0x04, 0x05];
3529        let data_hex = hex::encode(&program_data);
3530
3531        let result = client
3532            .rpc
3533            .write_program(
3534                Some(client.context.clone()),
3535                program_id.pubkey().to_string(),
3536                data_hex,
3537                0,
3538                None,
3539            )
3540            .await;
3541
3542        assert!(
3543            result.is_ok(),
3544            "Failed to write program: {:?}",
3545            result.err()
3546        );
3547
3548        // Verify the data was written correctly
3549        let program_data_address =
3550            solana_loader_v3_interface::get_program_data_address(&program_id.pubkey());
3551        let account = client.context.svm_locker.with_svm_reader(|svm_reader| {
3552            svm_reader
3553                .inner
3554                .get_account(&program_data_address)
3555                .unwrap()
3556                .unwrap()
3557        });
3558
3559        let metadata_size =
3560            solana_loader_v3_interface::state::UpgradeableLoaderState::size_of_programdata_metadata(
3561            );
3562        let written_data = &account.data[metadata_size..metadata_size + program_data.len()];
3563        assert_eq!(
3564            written_data, &program_data,
3565            "Written data should match input"
3566        );
3567
3568        println!("✅ Small program written correctly");
3569    }
3570
3571    #[tokio::test(flavor = "multi_thread")]
3572    async fn test_write_program_single_chunk_large() {
3573        // Test writing a large program (1MB) in a single write
3574        let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3575        let program_id = Keypair::new();
3576
3577        let program_data = vec![0xAB; 1024 * 1024]; // 1 MB
3578        let data_hex = hex::encode(&program_data);
3579
3580        let result = client
3581            .rpc
3582            .write_program(
3583                Some(client.context.clone()),
3584                program_id.pubkey().to_string(),
3585                data_hex,
3586                0,
3587                None,
3588            )
3589            .await;
3590
3591        assert!(
3592            result.is_ok(),
3593            "Failed to write large program: {:?}",
3594            result.err()
3595        );
3596
3597        // Verify the data was written
3598        let program_data_address =
3599            solana_loader_v3_interface::get_program_data_address(&program_id.pubkey());
3600        let account = client.context.svm_locker.with_svm_reader(|svm_reader| {
3601            svm_reader
3602                .inner
3603                .get_account(&program_data_address)
3604                .unwrap()
3605                .unwrap()
3606        });
3607
3608        let metadata_size =
3609            solana_loader_v3_interface::state::UpgradeableLoaderState::size_of_programdata_metadata(
3610            );
3611        assert_eq!(
3612            account.data.len(),
3613            metadata_size + program_data.len(),
3614            "Account should have correct size"
3615        );
3616
3617        println!("✅ Large program (1MB) written successfully");
3618    }
3619
3620    #[tokio::test(flavor = "multi_thread")]
3621    async fn test_write_program_multiple_sequential_chunks() {
3622        // Test writing a program in multiple sequential chunks
3623        let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3624        let program_id = Keypair::new();
3625
3626        let chunks = vec![
3627            (vec![0x01; 1024], 0),    // First 1KB at offset 0
3628            (vec![0x02; 1024], 1024), // Second 1KB at offset 1024
3629            (vec![0x03; 1024], 2048), // Third 1KB at offset 2048
3630            (vec![0x04; 512], 3072),  // Last 512 bytes at offset 3072
3631        ];
3632
3633        for (i, (chunk_data, offset)) in chunks.iter().enumerate() {
3634            let result = client
3635                .rpc
3636                .write_program(
3637                    Some(client.context.clone()),
3638                    program_id.pubkey().to_string(),
3639                    hex::encode(chunk_data),
3640                    *offset,
3641                    None,
3642                )
3643                .await;
3644
3645            assert!(
3646                result.is_ok(),
3647                "Failed to write chunk {} at offset {}: {:?}",
3648                i,
3649                offset,
3650                result.err()
3651            );
3652        }
3653
3654        // Verify all chunks were written correctly
3655        let program_data_address =
3656            solana_loader_v3_interface::get_program_data_address(&program_id.pubkey());
3657        let account = client.context.svm_locker.with_svm_reader(|svm_reader| {
3658            svm_reader
3659                .inner
3660                .get_account(&program_data_address)
3661                .unwrap()
3662                .unwrap()
3663        });
3664
3665        let metadata_size =
3666            solana_loader_v3_interface::state::UpgradeableLoaderState::size_of_programdata_metadata(
3667            );
3668
3669        // Verify each chunk
3670        for (chunk_data, offset) in chunks {
3671            let start = metadata_size + offset as usize;
3672            let end = start + chunk_data.len();
3673            let written = &account.data[start..end];
3674            assert_eq!(
3675                written,
3676                &chunk_data[..],
3677                "Chunk at offset {} should match",
3678                offset
3679            );
3680        }
3681
3682        println!("✅ Multiple sequential chunks written correctly");
3683    }
3684
3685    #[tokio::test(flavor = "multi_thread")]
3686    async fn test_write_program_non_sequential_chunks() {
3687        // Test writing chunks out of order (backwards)
3688        let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3689        let program_id = Keypair::new();
3690
3691        let chunks = vec![
3692            (vec![0x03; 512], 2048),  // Write third chunk first
3693            (vec![0x01; 1024], 0),    // Write first chunk second
3694            (vec![0x02; 1024], 1024), // Write second chunk last
3695        ];
3696
3697        for (chunk_data, offset) in chunks.iter() {
3698            let result = client
3699                .rpc
3700                .write_program(
3701                    Some(client.context.clone()),
3702                    program_id.pubkey().to_string(),
3703                    hex::encode(chunk_data),
3704                    *offset,
3705                    None,
3706                )
3707                .await;
3708
3709            assert!(
3710                result.is_ok(),
3711                "Failed at offset {}: {:?}",
3712                offset,
3713                result.err()
3714            );
3715        }
3716
3717        // Verify data integrity
3718        let program_data_address =
3719            solana_loader_v3_interface::get_program_data_address(&program_id.pubkey());
3720        let account = client.context.svm_locker.with_svm_reader(|svm_reader| {
3721            svm_reader
3722                .inner
3723                .get_account(&program_data_address)
3724                .unwrap()
3725                .unwrap()
3726        });
3727
3728        let metadata_size =
3729            solana_loader_v3_interface::state::UpgradeableLoaderState::size_of_programdata_metadata(
3730            );
3731
3732        // Check first chunk
3733        assert_eq!(
3734            &account.data[metadata_size..metadata_size + 1024],
3735            &vec![0x01; 1024][..]
3736        );
3737        // Check second chunk
3738        assert_eq!(
3739            &account.data[metadata_size + 1024..metadata_size + 2048],
3740            &vec![0x02; 1024][..]
3741        );
3742        // Check third chunk
3743        assert_eq!(
3744            &account.data[metadata_size + 2048..metadata_size + 2560],
3745            &vec![0x03; 512][..]
3746        );
3747
3748        println!("✅ Non-sequential chunks written correctly");
3749    }
3750
3751    #[tokio::test(flavor = "multi_thread")]
3752    async fn test_write_program_overlapping_writes() {
3753        // Test that overlapping writes correctly overwrite previous data
3754        let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3755        let program_id = Keypair::new();
3756
3757        // Write initial data
3758        let initial_data = vec![0xFF; 1024];
3759        client
3760            .rpc
3761            .write_program(
3762                Some(client.context.clone()),
3763                program_id.pubkey().to_string(),
3764                hex::encode(&initial_data),
3765                0,
3766                None,
3767            )
3768            .await
3769            .unwrap();
3770
3771        // Overwrite middle section
3772        let overwrite_data = vec![0xAA; 512];
3773        client
3774            .rpc
3775            .write_program(
3776                Some(client.context.clone()),
3777                program_id.pubkey().to_string(),
3778                hex::encode(&overwrite_data),
3779                256, // Start in the middle
3780                None,
3781            )
3782            .await
3783            .unwrap();
3784
3785        // Verify the result
3786        let program_data_address =
3787            solana_loader_v3_interface::get_program_data_address(&program_id.pubkey());
3788        let account = client.context.svm_locker.with_svm_reader(|svm_reader| {
3789            svm_reader
3790                .inner
3791                .get_account(&program_data_address)
3792                .unwrap()
3793                .unwrap()
3794        });
3795
3796        let metadata_size =
3797            solana_loader_v3_interface::state::UpgradeableLoaderState::size_of_programdata_metadata(
3798            );
3799
3800        // First 256 bytes should be 0xFF
3801        assert_eq!(
3802            &account.data[metadata_size..metadata_size + 256],
3803            &vec![0xFF; 256][..]
3804        );
3805        // Middle 512 bytes should be 0xAA
3806        assert_eq!(
3807            &account.data[metadata_size + 256..metadata_size + 768],
3808            &vec![0xAA; 512][..]
3809        );
3810        // Last 256 bytes should be 0xFF
3811        assert_eq!(
3812            &account.data[metadata_size + 768..metadata_size + 1024],
3813            &vec![0xFF; 256][..]
3814        );
3815
3816        println!("✅ Overlapping writes handled correctly");
3817    }
3818
3819    #[tokio::test(flavor = "multi_thread")]
3820    async fn test_write_program_zero_offset() {
3821        // Test writing at offset 0 (should write immediately after metadata)
3822        let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3823        let program_id = Keypair::new();
3824
3825        let data = vec![0x42; 128];
3826        let result = client
3827            .rpc
3828            .write_program(
3829                Some(client.context.clone()),
3830                program_id.pubkey().to_string(),
3831                hex::encode(&data),
3832                0,
3833                None,
3834            )
3835            .await;
3836
3837        assert!(result.is_ok());
3838
3839        let program_data_address =
3840            solana_loader_v3_interface::get_program_data_address(&program_id.pubkey());
3841        let account = client.context.svm_locker.with_svm_reader(|svm_reader| {
3842            svm_reader
3843                .inner
3844                .get_account(&program_data_address)
3845                .unwrap()
3846                .unwrap()
3847        });
3848
3849        let metadata_size =
3850            solana_loader_v3_interface::state::UpgradeableLoaderState::size_of_programdata_metadata(
3851            );
3852        assert_eq!(&account.data[metadata_size..metadata_size + 128], &data[..]);
3853
3854        println!("✅ Write at offset 0 works correctly");
3855    }
3856
3857    #[tokio::test(flavor = "multi_thread")]
3858    async fn test_write_program_large_offset() {
3859        // Test writing at a large offset (account should expand)
3860        let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3861        let program_id = Keypair::new();
3862
3863        let large_offset = 1024 * 1024; // 1 MB offset
3864        let data = vec![0x99; 256];
3865
3866        let result = client
3867            .rpc
3868            .write_program(
3869                Some(client.context.clone()),
3870                program_id.pubkey().to_string(),
3871                hex::encode(&data),
3872                large_offset,
3873                None,
3874            )
3875            .await;
3876
3877        assert!(result.is_ok(), "Should handle large offset");
3878
3879        let program_data_address =
3880            solana_loader_v3_interface::get_program_data_address(&program_id.pubkey());
3881        let account = client.context.svm_locker.with_svm_reader(|svm_reader| {
3882            svm_reader
3883                .inner
3884                .get_account(&program_data_address)
3885                .unwrap()
3886                .unwrap()
3887        });
3888
3889        let metadata_size =
3890            solana_loader_v3_interface::state::UpgradeableLoaderState::size_of_programdata_metadata(
3891            );
3892        let expected_size = metadata_size + large_offset as usize + data.len();
3893        assert_eq!(
3894            account.data.len(),
3895            expected_size,
3896            "Account should expand to fit data"
3897        );
3898
3899        // Verify data at the large offset
3900        let start = metadata_size + large_offset as usize;
3901        assert_eq!(&account.data[start..start + 256], &data[..]);
3902
3903        println!("✅ Large offset handled with account expansion");
3904    }
3905
3906    #[tokio::test(flavor = "multi_thread")]
3907    async fn test_write_program_empty_data() {
3908        // Test writing empty data (should succeed but not write anything)
3909        let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3910        let program_id = Keypair::new();
3911
3912        let result = client
3913            .rpc
3914            .write_program(
3915                Some(client.context.clone()),
3916                program_id.pubkey().to_string(),
3917                hex::encode(&[]),
3918                0,
3919                None,
3920            )
3921            .await;
3922
3923        assert!(result.is_ok(), "Should handle empty data");
3924
3925        println!("✅ Empty data handled correctly");
3926    }
3927
3928    #[tokio::test(flavor = "multi_thread")]
3929    async fn test_write_program_single_byte() {
3930        // Test writing a single byte
3931        let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3932        let program_id = Keypair::new();
3933
3934        let data = vec![0x42];
3935        let result = client
3936            .rpc
3937            .write_program(
3938                Some(client.context.clone()),
3939                program_id.pubkey().to_string(),
3940                hex::encode(&data),
3941                0,
3942                None,
3943            )
3944            .await;
3945
3946        assert!(result.is_ok());
3947
3948        let program_data_address =
3949            solana_loader_v3_interface::get_program_data_address(&program_id.pubkey());
3950        let account = client.context.svm_locker.with_svm_reader(|svm_reader| {
3951            svm_reader
3952                .inner
3953                .get_account(&program_data_address)
3954                .unwrap()
3955                .unwrap()
3956        });
3957
3958        let metadata_size =
3959            solana_loader_v3_interface::state::UpgradeableLoaderState::size_of_programdata_metadata(
3960            );
3961        assert_eq!(account.data[metadata_size], 0x42);
3962
3963        println!("✅ Single byte write works");
3964    }
3965
3966    #[tokio::test(flavor = "multi_thread")]
3967    async fn test_write_program_invalid_program_id() {
3968        // Test with invalid program ID
3969        let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3970
3971        let result = client
3972            .rpc
3973            .write_program(
3974                Some(client.context.clone()),
3975                "not_a_valid_pubkey".to_string(),
3976                "deadbeef".to_string(),
3977                0,
3978                None,
3979            )
3980            .await;
3981
3982        assert!(result.is_err(), "Should fail with invalid program ID");
3983        assert!(
3984            result.unwrap_err().to_string().contains("Invalid pubkey"),
3985            "Error should mention invalid pubkey"
3986        );
3987
3988        println!("✅ Invalid program ID rejected");
3989    }
3990
3991    #[tokio::test(flavor = "multi_thread")]
3992    async fn test_write_program_invalid_hex_data() {
3993        // Test with invalid hex encoding
3994        let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3995        let program_id = Keypair::new();
3996
3997        let invalid_hex_strings = vec![
3998            "not_hex_at_all",
3999            "GHIJKLMN",
4000            "0x123", // odd length
4001            "12 34", // contains space
4002        ];
4003
4004        for invalid_hex in invalid_hex_strings {
4005            let result = client
4006                .rpc
4007                .write_program(
4008                    Some(client.context.clone()),
4009                    program_id.pubkey().to_string(),
4010                    invalid_hex.to_string(),
4011                    0,
4012                    None,
4013                )
4014                .await;
4015
4016            assert!(
4017                result.is_err(),
4018                "Should fail with invalid hex: {}",
4019                invalid_hex
4020            );
4021            assert!(
4022                result.unwrap_err().to_string().contains("Invalid hex"),
4023                "Error should mention invalid hex"
4024            );
4025        }
4026
4027        println!("✅ Invalid hex data rejected");
4028    }
4029
4030    #[tokio::test(flavor = "multi_thread")]
4031    async fn test_write_program_rent_exemption() {
4032        // Test that rent exemption is maintained when account expands
4033        let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
4034        let program_id = Keypair::new();
4035
4036        // Write initial small data
4037        let small_data = vec![0x01; 128];
4038        client
4039            .rpc
4040            .write_program(
4041                Some(client.context.clone()),
4042                program_id.pubkey().to_string(),
4043                hex::encode(&small_data),
4044                0,
4045                None,
4046            )
4047            .await
4048            .unwrap();
4049
4050        let program_data_address =
4051            solana_loader_v3_interface::get_program_data_address(&program_id.pubkey());
4052
4053        let initial_lamports = client.context.svm_locker.with_svm_reader(|svm_reader| {
4054            svm_reader
4055                .inner
4056                .get_account(&program_data_address)
4057                .unwrap()
4058                .unwrap()
4059                .lamports
4060        });
4061
4062        // Write at large offset to expand account
4063        let large_data = vec![0x02; 1024];
4064        client
4065            .rpc
4066            .write_program(
4067                Some(client.context.clone()),
4068                program_id.pubkey().to_string(),
4069                hex::encode(&large_data),
4070                10240, // Large offset
4071                None,
4072            )
4073            .await
4074            .unwrap();
4075
4076        let final_lamports = client.context.svm_locker.with_svm_reader(|svm_reader| {
4077            svm_reader
4078                .inner
4079                .get_account(&program_data_address)
4080                .unwrap()
4081                .unwrap()
4082                .lamports
4083        });
4084
4085        assert!(
4086            final_lamports > initial_lamports,
4087            "Lamports should increase to maintain rent exemption"
4088        );
4089
4090        // Verify rent exemption
4091        let account = client.context.svm_locker.with_svm_reader(|svm_reader| {
4092            svm_reader
4093                .inner
4094                .get_account(&program_data_address)
4095                .unwrap()
4096                .unwrap()
4097        });
4098
4099        let required_lamports = client.context.svm_locker.with_svm_reader(|svm_reader| {
4100            svm_reader
4101                .inner
4102                .minimum_balance_for_rent_exemption(account.data.len())
4103        });
4104
4105        assert_eq!(
4106            account.lamports, required_lamports,
4107            "Account should have exact rent-exempt lamports"
4108        );
4109
4110        println!("✅ Rent exemption maintained during expansion");
4111    }
4112
4113    #[tokio::test(flavor = "multi_thread")]
4114    async fn test_write_program_account_ownership() {
4115        // Test that created accounts have correct ownership
4116        let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
4117        let program_id = Keypair::new();
4118        let authority = Keypair::new();
4119
4120        let data = vec![0xAB; 64];
4121        client
4122            .rpc
4123            .write_program(
4124                Some(client.context.clone()),
4125                program_id.pubkey().to_string(),
4126                hex::encode(&data),
4127                0,
4128                Some(authority.pubkey().to_string()),
4129            )
4130            .await
4131            .unwrap();
4132
4133        let program_data_address =
4134            solana_loader_v3_interface::get_program_data_address(&program_id.pubkey());
4135
4136        // Check program account ownership
4137        let program_account = client.context.svm_locker.with_svm_reader(|svm_reader| {
4138            svm_reader
4139                .inner
4140                .get_account(&program_id.pubkey())
4141                .unwrap()
4142                .unwrap()
4143        });
4144        assert_eq!(
4145            program_account.owner,
4146            solana_sdk_ids::bpf_loader_upgradeable::id(),
4147            "Program account should be owned by upgradeable loader"
4148        );
4149        assert!(
4150            program_account.executable,
4151            "Program account should be executable"
4152        );
4153
4154        // Check program data account ownership
4155        let program_data_account = client.context.svm_locker.with_svm_reader(|svm_reader| {
4156            svm_reader
4157                .inner
4158                .get_account(&program_data_address)
4159                .unwrap()
4160                .unwrap()
4161        });
4162        assert_eq!(
4163            program_data_account.owner,
4164            solana_sdk_ids::bpf_loader_upgradeable::id(),
4165            "Program data account should be owned by upgradeable loader"
4166        );
4167        assert!(
4168            !program_data_account.executable,
4169            "Program data account should not be executable"
4170        );
4171
4172        // Check program authority
4173
4174        let Ok(solana_loader_v3_interface::state::UpgradeableLoaderState::ProgramData {
4175            slot: _,
4176            upgrade_authority_address,
4177        }) = bincode::deserialize::<solana_loader_v3_interface::state::UpgradeableLoaderState>(
4178            &program_data_account.data,
4179        )
4180        else {
4181            panic!("Program data account has incorrect state");
4182        };
4183
4184        assert_eq!(
4185            upgrade_authority_address,
4186            Some(authority.pubkey()),
4187            "Upgrade authority should match"
4188        );
4189
4190        println!("✅ Account ownership is correct");
4191    }
4192
4193    #[tokio::test(flavor = "multi_thread")]
4194    async fn test_write_program_metadata_preservation() {
4195        // Test that program data account metadata is preserved across writes
4196        let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
4197        let program_id = Keypair::new();
4198
4199        // First write
4200        client
4201            .rpc
4202            .write_program(
4203                Some(client.context.clone()),
4204                program_id.pubkey().to_string(),
4205                hex::encode(&vec![0x01; 128]),
4206                0,
4207                None,
4208            )
4209            .await
4210            .unwrap();
4211
4212        let program_data_address =
4213            solana_loader_v3_interface::get_program_data_address(&program_id.pubkey());
4214
4215        // Get initial metadata
4216        let initial_account = client.context.svm_locker.with_svm_reader(|svm_reader| {
4217            svm_reader
4218                .inner
4219                .get_account(&program_data_address)
4220                .unwrap()
4221                .unwrap()
4222        });
4223
4224        let metadata_size =
4225            solana_loader_v3_interface::state::UpgradeableLoaderState::size_of_programdata_metadata(
4226            );
4227        let initial_metadata = initial_account.data[..metadata_size].to_vec();
4228
4229        // Second write (should preserve metadata)
4230        client
4231            .rpc
4232            .write_program(
4233                Some(client.context.clone()),
4234                program_id.pubkey().to_string(),
4235                hex::encode(&vec![0x02; 256]),
4236                128,
4237                None,
4238            )
4239            .await
4240            .unwrap();
4241
4242        // Verify metadata is preserved
4243        let final_account = client.context.svm_locker.with_svm_reader(|svm_reader| {
4244            svm_reader
4245                .inner
4246                .get_account(&program_data_address)
4247                .unwrap()
4248                .unwrap()
4249        });
4250
4251        let final_metadata = final_account.data[..metadata_size].to_vec();
4252        assert_eq!(
4253            initial_metadata, final_metadata,
4254            "Metadata should be preserved"
4255        );
4256
4257        println!("✅ Metadata preserved across writes");
4258    }
4259
4260    #[tokio::test(flavor = "multi_thread")]
4261    async fn test_write_program_idempotent() {
4262        // Test that writing the same data twice produces the same result
4263        let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
4264        let program_id = Keypair::new();
4265
4266        let data = vec![0x55; 512];
4267        let offset = 100;
4268
4269        // First write
4270        client
4271            .rpc
4272            .write_program(
4273                Some(client.context.clone()),
4274                program_id.pubkey().to_string(),
4275                hex::encode(&data),
4276                offset,
4277                None,
4278            )
4279            .await
4280            .unwrap();
4281
4282        let program_data_address =
4283            solana_loader_v3_interface::get_program_data_address(&program_id.pubkey());
4284
4285        let first_account = client.context.svm_locker.with_svm_reader(|svm_reader| {
4286            svm_reader
4287                .inner
4288                .get_account(&program_data_address)
4289                .unwrap()
4290                .unwrap()
4291        });
4292
4293        // Second write (same data, same offset)
4294        client
4295            .rpc
4296            .write_program(
4297                Some(client.context.clone()),
4298                program_id.pubkey().to_string(),
4299                hex::encode(&data),
4300                offset,
4301                None,
4302            )
4303            .await
4304            .unwrap();
4305
4306        let second_account = client.context.svm_locker.with_svm_reader(|svm_reader| {
4307            svm_reader
4308                .inner
4309                .get_account(&program_data_address)
4310                .unwrap()
4311                .unwrap()
4312        });
4313
4314        assert_eq!(
4315            first_account.data, second_account.data,
4316            "Data should be identical"
4317        );
4318        assert_eq!(
4319            first_account.lamports, second_account.lamports,
4320            "Lamports should be identical"
4321        );
4322
4323        println!("✅ Writes are idempotent");
4324    }
4325
4326    #[tokio::test(flavor = "multi_thread")]
4327    async fn test_write_program_context_slot() {
4328        // Test that response context contains valid slot
4329        let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
4330        let program_id = Keypair::new();
4331
4332        let result = client
4333            .rpc
4334            .write_program(
4335                Some(client.context.clone()),
4336                program_id.pubkey().to_string(),
4337                hex::encode(&vec![0x42; 64]),
4338                0,
4339                None,
4340            )
4341            .await
4342            .unwrap();
4343
4344        assert!(result.context.slot > 0, "Context slot should be valid");
4345
4346        println!("✅ Response context is valid");
4347    }
4348
4349    #[tokio::test(flavor = "multi_thread")]
4350    async fn test_write_program_small_no_minimum_program_artifacts() {
4351        // Regression test: writing a program smaller than minimum_program.so (3312 bytes)
4352        // should not leave leftover minimum_program.so bytes in the account.
4353        let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
4354        let program_id = Keypair::new();
4355
4356        let program_data = vec![0xAB; 100];
4357        let result = client
4358            .rpc
4359            .write_program(
4360                Some(client.context.clone()),
4361                program_id.pubkey().to_string(),
4362                hex::encode(&program_data),
4363                0,
4364                None,
4365            )
4366            .await;
4367
4368        assert!(
4369            result.is_ok(),
4370            "Failed to write program: {:?}",
4371            result.err()
4372        );
4373
4374        let program_data_address =
4375            solana_loader_v3_interface::get_program_data_address(&program_id.pubkey());
4376        let account = client.context.svm_locker.with_svm_reader(|svm_reader| {
4377            svm_reader
4378                .inner
4379                .get_account(&program_data_address)
4380                .unwrap()
4381                .unwrap()
4382        });
4383
4384        let metadata_size =
4385            solana_loader_v3_interface::state::UpgradeableLoaderState::size_of_programdata_metadata(
4386            );
4387
4388        // Account data length should be exactly metadata + program data, not metadata + 3312
4389        assert_eq!(
4390            account.data.len(),
4391            metadata_size + 100,
4392            "Account data length should be metadata_size ({}) + 100 = {}, but was {}",
4393            metadata_size,
4394            metadata_size + 100,
4395            account.data.len()
4396        );
4397
4398        // Verify written content matches exactly
4399        let written_data = &account.data[metadata_size..];
4400        assert_eq!(
4401            written_data, &program_data,
4402            "Written data should match exactly"
4403        );
4404
4405        // Verify no trailing non-zero bytes beyond written data
4406        assert!(
4407            account.data[metadata_size + 100..].is_empty(),
4408            "There should be no trailing bytes beyond the written data"
4409        );
4410
4411        println!("✅ Small program has no minimum_program.so artifacts");
4412    }
4413
4414    #[tokio::test(flavor = "multi_thread")]
4415    async fn test_write_program_exact_account_size() {
4416        // Regression test: verify account size is exactly correct for various data sizes,
4417        // including sizes around the minimum_program.so boundary (3312 bytes).
4418        let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
4419
4420        let metadata_size =
4421            solana_loader_v3_interface::state::UpgradeableLoaderState::size_of_programdata_metadata(
4422            );
4423
4424        for data_len in [1usize, 100, 3311, 3312, 3313, 5000] {
4425            let program_id = Keypair::new();
4426            let program_data: Vec<u8> = (0..data_len).map(|i| (i % 256) as u8).collect();
4427
4428            let result = client
4429                .rpc
4430                .write_program(
4431                    Some(client.context.clone()),
4432                    program_id.pubkey().to_string(),
4433                    hex::encode(&program_data),
4434                    0,
4435                    None,
4436                )
4437                .await;
4438
4439            assert!(
4440                result.is_ok(),
4441                "Failed to write program of size {}: {:?}",
4442                data_len,
4443                result.err()
4444            );
4445
4446            let program_data_address =
4447                solana_loader_v3_interface::get_program_data_address(&program_id.pubkey());
4448            let account = client.context.svm_locker.with_svm_reader(|svm_reader| {
4449                svm_reader
4450                    .inner
4451                    .get_account(&program_data_address)
4452                    .unwrap()
4453                    .unwrap()
4454            });
4455
4456            assert_eq!(
4457                account.data.len(),
4458                metadata_size + data_len,
4459                "For data_len={}, account data length should be {} but was {}",
4460                data_len,
4461                metadata_size + data_len,
4462                account.data.len()
4463            );
4464
4465            let written_data = &account.data[metadata_size..metadata_size + data_len];
4466            assert_eq!(
4467                written_data, &program_data,
4468                "For data_len={}, written content should match exactly",
4469                data_len
4470            );
4471        }
4472
4473        println!("✅ All program sizes produce exact account sizes");
4474    }
4475
4476    #[tokio::test(flavor = "multi_thread")]
4477    async fn test_write_program_execution_uses_written_bytes_not_noop() {
4478        // Regression test: after write_program, executing the program should use
4479        // the written bytes, not the noop placeholder from init_programdata_account.
4480        let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
4481        let program_id = Keypair::new();
4482
4483        // Create an "error program" ELF: identical to noop but returns r0=1 (error)
4484        // instead of r0=0 (success). This lets us distinguish noop vs real execution.
4485        let mut error_program_elf = crate::surfnet::noop_program::NOOP_PROGRAM_ELF.to_vec();
4486        // Byte 124 is the first byte of the imm field in `mov64 r0, 0` at .text offset.
4487        // Changing it to 1 makes the instruction `mov64 r0, 1` (program returns error).
4488        error_program_elf[124] = 0x01;
4489
4490        // Write the error program
4491        let result = client
4492            .rpc
4493            .write_program(
4494                Some(client.context.clone()),
4495                program_id.pubkey().to_string(),
4496                hex::encode(&error_program_elf),
4497                0,
4498                None,
4499            )
4500            .await;
4501        assert!(
4502            result.is_ok(),
4503            "Failed to write program: {:?}",
4504            result.err()
4505        );
4506
4507        // Create a payer and fund it
4508        let payer = Keypair::new();
4509        client
4510            .context
4511            .svm_locker
4512            .airdrop(&payer.pubkey(), 1_000_000_000)
4513            .unwrap()
4514            .unwrap();
4515
4516        // Build a transaction invoking the written program
4517        let recent_blockhash = client
4518            .context
4519            .svm_locker
4520            .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
4521        // Build a minimal instruction that invokes the written program
4522        let invoke_ix = solana_instruction::Instruction {
4523            program_id: program_id.pubkey(),
4524            accounts: vec![],
4525            data: vec![],
4526        };
4527        let message = solana_message::Message::new_with_blockhash(
4528            &[invoke_ix],
4529            Some(&payer.pubkey()),
4530            &recent_blockhash,
4531        );
4532        let tx = VersionedTransaction::try_new(
4533            solana_message::VersionedMessage::Legacy(message),
4534            &[&payer],
4535        )
4536        .unwrap();
4537
4538        // Simulate the transaction
4539        let sim_result = client.context.svm_locker.simulate_transaction(tx, false);
4540
4541        // The error program returns r0=1, which should cause an InstructionError.
4542        // If the noop (r0=0) is still cached, this would incorrectly succeed.
4543        assert!(
4544            sim_result.is_err(),
4545            "Transaction should fail because the written program returns error (r0=1). \
4546             If it succeeded, the noop placeholder is still being executed instead of \
4547             the written program bytes."
4548        );
4549    }
4550
4551    #[test]
4552    fn test_stream_accounts_registers_multiple() {
4553        let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
4554
4555        let pubkey1 = Pubkey::new_unique();
4556        let pubkey2 = Pubkey::new_unique();
4557        let pubkey3 = Pubkey::new_unique();
4558
4559        let entries = vec![
4560            StreamAccountsEntry {
4561                pubkey: pubkey1.to_string(),
4562                include_owned_accounts: Some(true),
4563            },
4564            StreamAccountsEntry {
4565                pubkey: pubkey2.to_string(),
4566                include_owned_accounts: Some(false),
4567            },
4568            StreamAccountsEntry {
4569                pubkey: pubkey3.to_string(),
4570                include_owned_accounts: None,
4571            },
4572        ];
4573
4574        let result = client
4575            .rpc
4576            .stream_accounts(Some(client.context.clone()), entries);
4577        assert!(result.is_ok(), "stream_accounts should succeed");
4578
4579        // Verify all accounts are registered via get_streamed_accounts
4580        let streamed = client
4581            .rpc
4582            .get_streamed_accounts(Some(client.context.clone()))
4583            .expect("Failed to get streamed accounts")
4584            .value;
4585
4586        let accounts = serde_json::to_value(&streamed).unwrap();
4587        let accounts_arr = accounts["accounts"].as_array().unwrap();
4588        assert_eq!(accounts_arr.len(), 3, "Should have 3 streamed accounts");
4589
4590        // Check individual entries
4591        let find = |pk: &str| {
4592            accounts_arr
4593                .iter()
4594                .find(|a| a["pubkey"].as_str().unwrap() == pk)
4595                .unwrap()
4596                .clone()
4597        };
4598        assert_eq!(find(&pubkey1.to_string())["includeOwnedAccounts"], true);
4599        assert_eq!(find(&pubkey2.to_string())["includeOwnedAccounts"], false);
4600        assert_eq!(find(&pubkey3.to_string())["includeOwnedAccounts"], false);
4601    }
4602
4603    #[test]
4604    fn test_stream_accounts_empty_list() {
4605        let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
4606
4607        let result = client
4608            .rpc
4609            .stream_accounts(Some(client.context.clone()), vec![]);
4610        assert!(
4611            result.is_ok(),
4612            "stream_accounts with empty list should succeed"
4613        );
4614
4615        let streamed = client
4616            .rpc
4617            .get_streamed_accounts(Some(client.context.clone()))
4618            .expect("Failed to get streamed accounts")
4619            .value;
4620
4621        let accounts = serde_json::to_value(&streamed).unwrap();
4622        let accounts_arr = accounts["accounts"].as_array().unwrap();
4623        assert_eq!(
4624            accounts_arr.len(),
4625            0,
4626            "Should have no streamed accounts after empty call"
4627        );
4628    }
4629
4630    #[test]
4631    fn test_stream_accounts_invalid_pubkey() {
4632        let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
4633
4634        let entries = vec![StreamAccountsEntry {
4635            pubkey: "not-a-valid-pubkey".to_string(),
4636            include_owned_accounts: None,
4637        }];
4638
4639        let result = client
4640            .rpc
4641            .stream_accounts(Some(client.context.clone()), entries);
4642        assert!(
4643            result.is_err(),
4644            "stream_accounts with invalid pubkey should fail"
4645        );
4646    }
4647}