Skip to main content

surfpool_core/rpc/
surfnet_cheatcodes.rs

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