Skip to main content

surfpool_types/
types.rs

1#[cfg(feature = "prometheus")]
2use std::time::SystemTime;
3use std::{
4    cmp::Ordering,
5    collections::{BTreeMap, HashMap},
6    fmt,
7    path::PathBuf,
8    str::FromStr,
9    sync::{Arc, Mutex},
10};
11
12use blake3::Hash;
13use chrono::{DateTime, Local};
14use crossbeam_channel::Sender;
15use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Visitor};
16use serde_with::{BytesOrString, serde_as};
17use solana_account::Account;
18use solana_account_decoder_client_types::{ParsedAccount, UiAccount, UiAccountEncoding};
19use solana_clock::{Clock, Epoch, Slot};
20use solana_epoch_info::EpochInfo;
21use solana_message::inner_instruction::InnerInstructionsList;
22use solana_pubkey::Pubkey;
23use solana_signature::Signature;
24use solana_transaction::versioned::VersionedTransaction;
25use solana_transaction_context::TransactionReturnData;
26use solana_transaction_error::TransactionError;
27use txtx_addon_kit::indexmap::IndexMap;
28use uuid::Uuid;
29
30use crate::DEFAULT_MAINNET_RPC_URL;
31
32pub const DEFAULT_RPC_PORT: u16 = 8899;
33pub const DEFAULT_WS_PORT: u16 = 8900;
34pub const DEFAULT_STUDIO_PORT: u16 = 8488;
35pub const CHANGE_TO_DEFAULT_STUDIO_PORT_ONCE_SUPERVISOR_MERGED: u16 = 18488;
36pub const DEFAULT_NETWORK_HOST: &str = "127.0.0.1";
37pub const DEFAULT_SLOT_TIME_MS: u64 = 400;
38pub type Idl = anchor_lang_idl::types::Idl;
39pub const DEFAULT_PROFILING_MAP_CAPACITY: usize = 200;
40
41#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
42pub struct TransactionMetadata {
43    pub signature: Signature,
44    pub logs: Vec<String>,
45    pub inner_instructions: InnerInstructionsList,
46    pub compute_units_consumed: u64,
47    pub return_data: TransactionReturnData,
48    pub fee: u64,
49}
50
51#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(rename_all = "camelCase")]
53pub enum TransactionConfirmationStatus {
54    Processed,
55    Confirmed,
56    Finalized,
57}
58
59#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
60pub enum BlockProductionMode {
61    #[default]
62    Clock,
63    Transaction,
64    Manual,
65}
66
67impl fmt::Display for BlockProductionMode {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        match self {
70            BlockProductionMode::Clock => write!(f, "clock"),
71            BlockProductionMode::Transaction => write!(f, "transaction"),
72            BlockProductionMode::Manual => write!(f, "manual"),
73        }
74    }
75}
76
77impl FromStr for BlockProductionMode {
78    type Err = String;
79
80    fn from_str(s: &str) -> Result<Self, Self::Err> {
81        match s.to_lowercase().as_str() {
82            "clock" => Ok(BlockProductionMode::Clock),
83            "transaction" => Ok(BlockProductionMode::Transaction),
84            "manual" => Ok(BlockProductionMode::Manual),
85            _ => Err(format!(
86                "Invalid block production mode: {}. Valid values are: clock, transaction, manual",
87                s
88            )),
89        }
90    }
91}
92
93#[derive(Debug)]
94pub enum SubgraphEvent {
95    EndpointReady,
96    InfoLog(DateTime<Local>, String),
97    ErrorLog(DateTime<Local>, String),
98    WarnLog(DateTime<Local>, String),
99    DebugLog(DateTime<Local>, String),
100    Shutdown,
101}
102
103impl SubgraphEvent {
104    pub fn info<S>(msg: S) -> Self
105    where
106        S: Into<String>,
107    {
108        Self::InfoLog(Local::now(), msg.into())
109    }
110
111    pub fn warn<S>(msg: S) -> Self
112    where
113        S: Into<String>,
114    {
115        Self::WarnLog(Local::now(), msg.into())
116    }
117
118    pub fn error<S>(msg: S) -> Self
119    where
120        S: Into<String>,
121    {
122        Self::ErrorLog(Local::now(), msg.into())
123    }
124
125    pub fn debug<S>(msg: S) -> Self
126    where
127        S: Into<String>,
128    {
129        Self::DebugLog(Local::now(), msg.into())
130    }
131}
132
133/// Result structure for compute units estimation.
134#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
135#[serde(rename_all = "camelCase")]
136pub struct ComputeUnitsEstimationResult {
137    pub success: bool,
138    pub compute_units_consumed: u64,
139    pub log_messages: Option<Vec<String>>,
140    pub error_message: Option<String>,
141}
142
143/// The struct for storing the profiling results.
144#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
145pub struct KeyedProfileResult {
146    pub slot: u64,
147    pub key: UuidOrSignature,
148    pub instruction_profiles: Option<Vec<ProfileResult>>,
149    pub transaction_profile: ProfileResult,
150    #[serde(with = "pubkey_account_map")]
151    pub readonly_account_states: HashMap<Pubkey, Account>,
152}
153
154impl KeyedProfileResult {
155    pub fn new(
156        slot: u64,
157        key: UuidOrSignature,
158        instruction_profiles: Option<Vec<ProfileResult>>,
159        transaction_profile: ProfileResult,
160        readonly_account_states: HashMap<Pubkey, Account>,
161    ) -> Self {
162        Self {
163            slot,
164            key,
165            instruction_profiles,
166            transaction_profile,
167            readonly_account_states,
168        }
169    }
170}
171
172#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
173pub struct ProfileResult {
174    #[serde(with = "pubkey_option_account_map")]
175    pub pre_execution_capture: ExecutionCapture,
176    #[serde(with = "pubkey_option_account_map")]
177    pub post_execution_capture: ExecutionCapture,
178    pub compute_units_consumed: u64,
179    pub log_messages: Option<Vec<String>>,
180    pub error_message: Option<String>,
181}
182
183pub type ExecutionCapture = BTreeMap<Pubkey, Option<Account>>;
184
185impl ProfileResult {
186    pub fn new(
187        pre_execution_capture: ExecutionCapture,
188        post_execution_capture: ExecutionCapture,
189        compute_units_consumed: u64,
190        log_messages: Option<Vec<String>>,
191        error_message: Option<String>,
192    ) -> Self {
193        Self {
194            pre_execution_capture,
195            post_execution_capture,
196            compute_units_consumed,
197            log_messages,
198            error_message,
199        }
200    }
201}
202
203#[derive(Debug, Clone, PartialEq)]
204pub enum AccountProfileState {
205    Readonly,
206    Writable(AccountChange),
207}
208
209impl AccountProfileState {
210    pub fn new(
211        pubkey: Pubkey,
212        pre_account: Option<Account>,
213        post_account: Option<Account>,
214        readonly_accounts: &[Pubkey],
215    ) -> Self {
216        if readonly_accounts.contains(&pubkey) {
217            return AccountProfileState::Readonly;
218        }
219
220        match (pre_account, post_account) {
221            (None, Some(post_account)) => {
222                AccountProfileState::Writable(AccountChange::Create(post_account))
223            }
224            (Some(pre_account), None) => {
225                AccountProfileState::Writable(AccountChange::Delete(pre_account))
226            }
227            (Some(pre_account), Some(post_account)) if pre_account == post_account => {
228                AccountProfileState::Writable(AccountChange::Unchanged(Some(pre_account)))
229            }
230            (Some(pre_account), Some(post_account)) => {
231                AccountProfileState::Writable(AccountChange::Update(pre_account, post_account))
232            }
233            (None, None) => AccountProfileState::Writable(AccountChange::Unchanged(None)),
234        }
235    }
236}
237
238#[derive(Debug, Clone, PartialEq)]
239pub enum AccountChange {
240    Create(Account),
241    Update(Account, Account),
242    Delete(Account),
243    Unchanged(Option<Account>),
244}
245
246#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
247#[serde(rename_all = "camelCase")]
248pub struct RpcProfileResultConfig {
249    pub encoding: Option<UiAccountEncoding>,
250    pub depth: Option<RpcProfileDepth>,
251}
252
253impl Default for RpcProfileResultConfig {
254    fn default() -> Self {
255        Self {
256            encoding: Some(UiAccountEncoding::JsonParsed),
257            depth: Some(RpcProfileDepth::default()),
258        }
259    }
260}
261
262#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
263#[serde(rename_all = "camelCase")]
264pub enum RpcProfileDepth {
265    Transaction,
266    #[default]
267    Instruction,
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
271#[serde(rename_all = "camelCase")]
272pub struct UiKeyedProfileResult {
273    pub slot: u64,
274    pub key: UuidOrSignature,
275    #[serde(skip_serializing_if = "Option::is_none")]
276    pub instruction_profiles: Option<Vec<UiProfileResult>>,
277    pub transaction_profile: UiProfileResult,
278    #[serde(with = "profile_state_map")]
279    pub readonly_account_states: IndexMap<Pubkey, UiAccount>,
280}
281
282#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
283#[serde(rename_all = "camelCase")]
284pub struct UiProfileResult {
285    #[serde(with = "profile_state_map")]
286    pub account_states: IndexMap<Pubkey, UiAccountProfileState>,
287    pub compute_units_consumed: u64,
288    pub log_messages: Option<Vec<String>>,
289    pub error_message: Option<String>,
290}
291
292#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
293#[serde(rename_all = "camelCase", tag = "type", content = "accountChange")]
294#[allow(clippy::large_enum_variant)]
295pub enum UiAccountProfileState {
296    Readonly,
297    Writable(UiAccountChange),
298}
299
300#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
301#[serde(rename_all = "camelCase", tag = "type", content = "data")]
302pub enum UiAccountChange {
303    Create(UiAccount),
304    Update(UiAccount, UiAccount),
305    Delete(UiAccount),
306    /// The account didn't change. If [Some], this is the initial state. If [None], the account didn't exist before/after execution.
307    Unchanged(Option<UiAccount>),
308}
309
310/// P starts with 300 lamports
311/// Ix 1 Transfers 100 lamports to P
312/// Ix 2 Transfers 100 lamports to P
313///
314/// Profile result 1 is from executing just Ix 1
315/// AccountProfileState::Writable(P, AccountChange::Update( UiAccount { lamports: 300, ...}, UiAccount { lamports: 400, ... }))
316///
317/// Profile result 2 is from executing Ix 1 and Ix 2
318/// AccountProfileState::Writable(P, AccountChange::Update( UiAccount { lamports: 400, ...}, UiAccount { lamports: 500, ... }))
319pub mod profile_state_map {
320    use super::*;
321
322    pub fn serialize<S, T>(map: &IndexMap<Pubkey, T>, serializer: S) -> Result<S::Ok, S::Error>
323    where
324        S: Serializer,
325        T: Serialize,
326    {
327        let str_map: IndexMap<String, &T> = map.iter().map(|(k, v)| (k.to_string(), v)).collect();
328        str_map.serialize(serializer)
329    }
330
331    pub fn deserialize<'de, D, T>(deserializer: D) -> Result<IndexMap<Pubkey, T>, D::Error>
332    where
333        D: Deserializer<'de>,
334        T: Deserialize<'de>,
335    {
336        let str_map: IndexMap<String, T> = IndexMap::deserialize(deserializer)?;
337        str_map
338            .into_iter()
339            .map(|(k, v)| {
340                Pubkey::from_str(&k)
341                    .map(|pk| (pk, v))
342                    .map_err(serde::de::Error::custom)
343            })
344            .collect()
345    }
346}
347
348/// Serialization module for HashMap<Pubkey, Account>
349pub mod pubkey_account_map {
350    use super::*;
351
352    pub fn serialize<S>(map: &HashMap<Pubkey, Account>, serializer: S) -> Result<S::Ok, S::Error>
353    where
354        S: Serializer,
355    {
356        let str_map: HashMap<String, &Account> =
357            map.iter().map(|(k, v)| (k.to_string(), v)).collect();
358        str_map.serialize(serializer)
359    }
360
361    pub fn deserialize<'de, D>(deserializer: D) -> Result<HashMap<Pubkey, Account>, D::Error>
362    where
363        D: Deserializer<'de>,
364    {
365        let str_map: HashMap<String, Account> = HashMap::deserialize(deserializer)?;
366        str_map
367            .into_iter()
368            .map(|(k, v)| {
369                Pubkey::from_str(&k)
370                    .map(|pk| (pk, v))
371                    .map_err(serde::de::Error::custom)
372            })
373            .collect()
374    }
375}
376
377/// Serialization module for BTreeMap<Pubkey, Option<Account>>
378pub mod pubkey_option_account_map {
379    use super::*;
380
381    pub fn serialize<S>(
382        map: &BTreeMap<Pubkey, Option<Account>>,
383        serializer: S,
384    ) -> Result<S::Ok, S::Error>
385    where
386        S: Serializer,
387    {
388        let str_map: BTreeMap<String, &Option<Account>> =
389            map.iter().map(|(k, v)| (k.to_string(), v)).collect();
390        str_map.serialize(serializer)
391    }
392
393    pub fn deserialize<'de, D>(
394        deserializer: D,
395    ) -> Result<BTreeMap<Pubkey, Option<Account>>, D::Error>
396    where
397        D: Deserializer<'de>,
398    {
399        let str_map: BTreeMap<String, Option<Account>> = BTreeMap::deserialize(deserializer)?;
400        str_map
401            .into_iter()
402            .map(|(k, v)| {
403                Pubkey::from_str(&k)
404                    .map(|pk| (pk, v))
405                    .map_err(serde::de::Error::custom)
406            })
407            .collect()
408    }
409}
410
411#[derive(Debug)]
412pub enum SimnetEvent {
413    /// Surfnet is ready, with the initial count of processed transactions from storage
414    Ready(u64),
415    Connected(String),
416    Aborted(String),
417    Shutdown,
418    SystemClockUpdated(Clock),
419    ClockUpdate(ClockCommand),
420    EpochInfoUpdate(EpochInfo),
421    BlockHashExpired,
422    InfoLog(DateTime<Local>, String),
423    ErrorLog(DateTime<Local>, String),
424    WarnLog(DateTime<Local>, String),
425    DebugLog(DateTime<Local>, String),
426    PluginLoaded(String),
427    TransactionReceived(DateTime<Local>, VersionedTransaction),
428    TransactionProcessed(
429        DateTime<Local>,
430        TransactionMetadata,
431        Option<TransactionError>,
432    ),
433    AccountUpdate(DateTime<Local>, Pubkey),
434    TaggedProfile {
435        result: KeyedProfileResult,
436        tag: String,
437        timestamp: DateTime<Local>,
438    },
439    RunbookStarted(String),
440    RunbookCompleted(String, Option<Vec<String>>),
441}
442
443impl SimnetEvent {
444    pub fn info<S>(msg: S) -> Self
445    where
446        S: Into<String>,
447    {
448        Self::InfoLog(Local::now(), msg.into())
449    }
450
451    pub fn warn<S>(msg: S) -> Self
452    where
453        S: Into<String>,
454    {
455        Self::WarnLog(Local::now(), msg.into())
456    }
457
458    pub fn error<S>(msg: S) -> Self
459    where
460        S: Into<String>,
461    {
462        Self::ErrorLog(Local::now(), msg.into())
463    }
464
465    pub fn debug<S>(msg: S) -> Self
466    where
467        S: Into<String>,
468    {
469        Self::DebugLog(Local::now(), msg.into())
470    }
471
472    pub fn transaction_processed(meta: TransactionMetadata, err: Option<TransactionError>) -> Self {
473        Self::TransactionProcessed(Local::now(), meta, err)
474    }
475
476    pub fn transaction_received(tx: VersionedTransaction) -> Self {
477        Self::TransactionReceived(Local::now(), tx)
478    }
479
480    pub fn account_update(pubkey: Pubkey) -> Self {
481        Self::AccountUpdate(Local::now(), pubkey)
482    }
483
484    pub fn tagged_profile(result: KeyedProfileResult, tag: String) -> Self {
485        Self::TaggedProfile {
486            result,
487            tag,
488            timestamp: Local::now(),
489        }
490    }
491
492    pub fn account_update_msg(&self) -> String {
493        match self {
494            SimnetEvent::AccountUpdate(_, pubkey) => {
495                format!("Account {} updated.", pubkey)
496            }
497            _ => unreachable!("This function should only be called for AccountUpdate events"),
498        }
499    }
500
501    pub fn epoch_info_update_msg(&self) -> String {
502        match self {
503            SimnetEvent::EpochInfoUpdate(epoch_info) => {
504                format!(
505                    "Datasource connection successful. Epoch {} / Slot index {} / Slot {}.",
506                    epoch_info.epoch, epoch_info.slot_index, epoch_info.absolute_slot
507                )
508            }
509            _ => unreachable!("This function should only be called for EpochInfoUpdate events"),
510        }
511    }
512
513    pub fn plugin_loaded_msg(&self) -> String {
514        match self {
515            SimnetEvent::PluginLoaded(plugin_name) => {
516                format!("Plugin {} successfully loaded.", plugin_name)
517            }
518            _ => unreachable!("This function should only be called for PluginLoaded events"),
519        }
520    }
521
522    pub fn clock_update_msg(&self) -> String {
523        match self {
524            SimnetEvent::SystemClockUpdated(clock) => {
525                format!("Clock ticking (epoch {}, slot {})", clock.epoch, clock.slot)
526            }
527            _ => {
528                unreachable!("This function should only be called for SystemClockUpdated events")
529            }
530        }
531    }
532}
533
534#[derive(Debug)]
535pub enum TransactionStatusEvent {
536    Success(TransactionConfirmationStatus),
537    SimulationFailure((TransactionError, TransactionMetadata)),
538    ExecutionFailure((TransactionError, TransactionMetadata)),
539    VerificationFailure(String),
540}
541
542#[derive(Debug)]
543pub enum SimnetCommand {
544    SlotForward(Option<Hash>),
545    SlotBackward(Option<Hash>),
546    CommandClock(Option<(Hash, String)>, ClockCommand),
547    UpdateInternalClock(Option<(Hash, String)>, Clock),
548    UpdateInternalClockWithConfirmation(Option<(Hash, String)>, Clock, Sender<EpochInfo>),
549    UpdateBlockProductionMode(BlockProductionMode),
550    ProcessTransaction(
551        Option<(Hash, String)>,
552        VersionedTransaction,
553        Sender<TransactionStatusEvent>,
554        bool,
555        Option<bool>,
556    ),
557    Terminate(Option<(Hash, String)>),
558    StartRunbookExecution(String),
559    CompleteRunbookExecution(String, Option<Vec<String>>),
560    FetchRemoteAccounts(Vec<Pubkey>, String),
561    AirdropProcessed,
562}
563
564#[derive(Debug)]
565pub enum ClockCommand {
566    Pause,
567    /// Pause with confirmation - sends epoch info back when actually paused
568    PauseWithConfirmation(Sender<EpochInfo>),
569    Resume,
570    Toggle,
571    UpdateSlotInterval(u64),
572}
573
574pub enum ClockEvent {
575    Tick,
576    ExpireBlockHash,
577}
578
579#[derive(Clone, Debug, Default, Serialize)]
580pub struct SanitizedConfig {
581    pub rpc_url: String,
582    pub ws_url: String,
583    pub rpc_datasource_url: Option<String>,
584    pub studio_url: String,
585    pub graphql_query_route_url: String,
586    pub version: String,
587    pub workspace: Option<String>,
588}
589
590#[derive(Clone, Debug, Default, Serialize, Deserialize)]
591pub struct SurfpoolConfig {
592    pub simnets: Vec<SimnetConfig>,
593    pub rpc: RpcConfig,
594    pub subgraph: SubgraphConfig,
595    pub studio: StudioConfig,
596    pub plugin_config_path: Vec<PathBuf>,
597}
598
599#[derive(Clone, Debug, Serialize, Deserialize)]
600pub struct SimnetConfig {
601    pub offline_mode: bool,
602    pub remote_rpc_url: Option<String>,
603    pub slot_time: u64,
604    pub block_production_mode: BlockProductionMode,
605    pub airdrop_addresses: Vec<Pubkey>,
606    pub airdrop_token_amount: u64,
607    pub expiry: Option<u64>,
608    pub instruction_profiling_enabled: bool,
609    pub max_profiles: usize,
610    pub log_bytes_limit: Option<usize>,
611    pub skip_signature_verification: bool,
612    /// Unique identifier for this surfnet instance. Used to isolate database storage
613    /// when multiple surfnets share the same database. Defaults to "default".
614    pub surfnet_id: String,
615    /// Snapshot accounts to preload at startup.
616    /// Keys are pubkey strings, values can be None to fetch from remote RPC.
617    pub snapshot: BTreeMap<String, Option<AccountSnapshot>>,
618}
619
620impl Default for SimnetConfig {
621    fn default() -> Self {
622        Self {
623            offline_mode: false,
624            remote_rpc_url: Some(DEFAULT_MAINNET_RPC_URL.to_string()),
625            slot_time: DEFAULT_SLOT_TIME_MS, // Default to 400ms to match CLI default
626            block_production_mode: BlockProductionMode::Clock,
627            airdrop_addresses: vec![],
628            airdrop_token_amount: 0,
629            expiry: None,
630            instruction_profiling_enabled: true,
631            max_profiles: DEFAULT_PROFILING_MAP_CAPACITY,
632            log_bytes_limit: Some(10_000),
633            skip_signature_verification: false,
634            surfnet_id: "default".to_string(),
635            snapshot: BTreeMap::new(),
636        }
637    }
638}
639
640impl SimnetConfig {
641    /// Returns a sanitized version of the datasource URL safe for display.
642    /// Only returns scheme and host (e.g., "https://example.com") to prevent
643    /// leaking API keys in paths or query parameters.
644    pub fn get_sanitized_datasource_url(&self) -> Option<String> {
645        let raw = self.remote_rpc_url.as_ref()?;
646
647        if let Ok(url) = url::Url::parse(raw) {
648            let scheme = url.scheme();
649            let host = url.host_str()?;
650            Some(format!("{}://{}", scheme, host))
651        } else {
652            None
653        }
654    }
655}
656
657#[derive(Clone, Debug, Default, Serialize, Deserialize)]
658pub struct SubgraphConfig {}
659
660pub const DEFAULT_GOSSIP_PORT: u16 = 8001;
661pub const DEFAULT_TPU_PORT: u16 = 8003;
662pub const DEFAULT_TPU_QUIC_PORT: u16 = 8004;
663
664#[derive(Clone, Debug, Serialize, Deserialize)]
665pub struct RpcConfig {
666    pub bind_host: String,
667    pub bind_port: u16,
668    pub ws_port: u16,
669    pub gossip_port: u16,
670    pub tpu_port: u16,
671    pub tpu_quic_port: u16,
672}
673
674impl RpcConfig {
675    pub fn get_rpc_base_url(&self) -> String {
676        format!("{}:{}", self.bind_host, self.bind_port)
677    }
678    pub fn get_ws_base_url(&self) -> String {
679        format!("{}:{}", self.bind_host, self.ws_port)
680    }
681}
682
683impl Default for RpcConfig {
684    fn default() -> Self {
685        Self {
686            bind_host: DEFAULT_NETWORK_HOST.to_string(),
687            bind_port: DEFAULT_RPC_PORT,
688            ws_port: DEFAULT_WS_PORT,
689            gossip_port: DEFAULT_GOSSIP_PORT,
690            tpu_port: DEFAULT_TPU_PORT,
691            tpu_quic_port: DEFAULT_TPU_QUIC_PORT,
692        }
693    }
694}
695
696#[derive(Clone, Debug, Serialize, Deserialize)]
697pub struct StudioConfig {
698    pub bind_host: String,
699    pub bind_port: u16,
700}
701
702impl StudioConfig {
703    pub fn get_studio_base_url(&self) -> String {
704        format!("{}:{}", self.bind_host, self.bind_port)
705    }
706}
707
708impl Default for StudioConfig {
709    fn default() -> Self {
710        Self {
711            bind_host: DEFAULT_NETWORK_HOST.to_string(),
712            bind_port: CHANGE_TO_DEFAULT_STUDIO_PORT_ONCE_SUPERVISOR_MERGED,
713        }
714    }
715}
716
717#[derive(Serialize, Deserialize, Clone, Debug)]
718#[serde(rename_all = "snake_case")]
719pub struct CreateSurfnetRequest {
720    pub domain: String,
721    pub block_production_mode: BlockProductionMode,
722    pub datasource_rpc_url: String,
723    pub settings: Option<CloudSurfnetSettings>,
724}
725
726#[derive(Serialize, Deserialize, Clone, Debug, Default)]
727#[serde(rename_all = "snake_case", default)]
728pub struct CloudSurfnetSettings {
729    pub database_url: Option<String>,
730    pub profiling_disabled: Option<bool>,
731    #[serde(skip_serializing_if = "Option::is_none")]
732    pub gating: Option<CloudSurfnetRpcGating>,
733}
734
735#[derive(Serialize, Deserialize, Clone, Debug, Default)]
736#[serde(rename_all = "snake_case", default)]
737pub struct CloudSurfnetRpcGating {
738    pub private_methods_secret_token: Option<String>,
739    pub private_methods: Vec<String>,
740    pub public_methods: Vec<String>,
741    pub disabled_methods: Vec<String>,
742}
743
744impl CloudSurfnetRpcGating {
745    pub fn restricted() -> CloudSurfnetRpcGating {
746        CloudSurfnetRpcGating {
747            private_methods: vec![],
748            private_methods_secret_token: None,
749            public_methods: vec![],
750            disabled_methods: vec![
751                "surfnet_cloneProgramAccount".into(),
752                "surfnet_profileTransaction".into(),
753                "surfnet_getProfileResultsByTag".into(),
754                "surfnet_setSupply".into(),
755                "surfnet_setProgramAuthority".into(),
756                "surfnet_getTransactionProfile".into(),
757                "surfnet_registerIdl".into(),
758                "surfnet_getActiveIdl".into(),
759                "surfnet_getLocalSignatures".into(),
760                "surfnet_timeTravel".into(),
761                "surfnet_pauseClock".into(),
762                "surfnet_resumeClock".into(),
763                "surfnet_resetAccount".into(),
764                "surfnet_resetNetwork".into(),
765                "surfnet_exportSnapshot".into(),
766                "surfnet_offlineAccount".into(),
767                "surfnet_streamAccount".into(),
768                "surfnet_streamAccounts".into(),
769                "surfnet_getStreamedAccounts".into(),
770            ],
771        }
772    }
773}
774
775#[derive(Serialize, Deserialize)]
776#[serde(rename_all = "snake_case")]
777pub struct CreateNetworkRequest {
778    pub workspace_id: Uuid,
779    pub name: String,
780    pub description: Option<String>,
781    pub datasource_rpc_url: String,
782    pub block_production_mode: BlockProductionMode,
783    pub profiling_enabled: Option<bool>,
784}
785
786impl CreateNetworkRequest {
787    pub fn new(
788        workspace_id: Uuid,
789        name: String,
790        description: Option<String>,
791        datasource_rpc_url: String,
792        block_production_mode: BlockProductionMode,
793        profiling_enabled: bool,
794    ) -> Self {
795        Self {
796            workspace_id,
797            name,
798            description,
799            datasource_rpc_url,
800            block_production_mode,
801            profiling_enabled: Some(profiling_enabled),
802        }
803    }
804}
805
806#[derive(Serialize, Deserialize)]
807pub struct CreateNetworkResponse {
808    pub rpc_url: String,
809}
810
811#[derive(Serialize, Deserialize)]
812pub struct DeleteNetworkRequest {
813    pub workspace_id: Uuid,
814    pub network_id: Uuid,
815}
816
817impl DeleteNetworkRequest {
818    pub fn new(workspace_id: Uuid, network_id: Uuid) -> Self {
819        Self {
820            workspace_id,
821            network_id,
822        }
823    }
824}
825
826#[derive(Serialize, Deserialize)]
827pub struct DeleteNetworkResponse;
828
829#[serde_as]
830#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
831#[serde(rename_all = "camelCase")]
832pub struct AccountUpdate {
833    /// providing this value sets the lamports in the account
834    pub lamports: Option<u64>,
835    /// providing this value sets the data held in this account
836    #[serde_as(as = "Option<BytesOrString>")]
837    pub data: Option<Vec<u8>>,
838    ///  providing this value sets the program that owns this account. If executable, the program that loads this account.
839    pub owner: Option<String>,
840    /// providing this value sets whether this account's data contains a loaded program (and is now read-only)
841    pub executable: Option<bool>,
842    /// providing this value sets the epoch at which this account will next owe rent
843    pub rent_epoch: Option<Epoch>,
844}
845
846#[derive(Debug, Clone)]
847pub enum SetSomeAccount {
848    Account(String),
849    NoAccount,
850}
851
852impl<'de> Deserialize<'de> for SetSomeAccount {
853    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
854    where
855        D: Deserializer<'de>,
856    {
857        struct SetSomeAccountVisitor;
858
859        impl<'de> Visitor<'de> for SetSomeAccountVisitor {
860            type Value = SetSomeAccount;
861
862            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
863                formatter.write_str("a Pubkey String or the String 'null'")
864            }
865
866            fn visit_some<D_>(self, deserializer: D_) -> std::result::Result<Self::Value, D_::Error>
867            where
868                D_: Deserializer<'de>,
869            {
870                Deserialize::deserialize(deserializer).map(|v: String| match v.as_str() {
871                    "null" => SetSomeAccount::NoAccount,
872                    _ => SetSomeAccount::Account(v.to_string()),
873                })
874            }
875        }
876
877        deserializer.deserialize_option(SetSomeAccountVisitor)
878    }
879}
880
881impl Serialize for SetSomeAccount {
882    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
883    where
884        S: Serializer,
885    {
886        match self {
887            SetSomeAccount::Account(val) => serializer.serialize_str(val),
888            SetSomeAccount::NoAccount => serializer.serialize_str("null"),
889        }
890    }
891}
892
893#[serde_as]
894#[derive(Debug, Clone, Default, Serialize, Deserialize)]
895#[serde(rename_all = "camelCase")]
896pub struct TokenAccountUpdate {
897    /// providing this value sets the amount of the token in the account data
898    pub amount: Option<u64>,
899    /// providing this value sets the delegate of the token account
900    pub delegate: Option<SetSomeAccount>,
901    /// providing this value sets the state of the token account
902    pub state: Option<String>,
903    /// providing this value sets the amount authorized to the delegate
904    pub delegated_amount: Option<u64>,
905    /// providing this value sets the close authority of the token account
906    pub close_authority: Option<SetSomeAccount>,
907}
908
909// token supply update for set supply method in SVM tricks
910#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
911pub struct SupplyUpdate {
912    pub total: Option<u64>,
913    pub circulating: Option<u64>,
914    pub non_circulating: Option<u64>,
915    pub non_circulating_accounts: Option<Vec<String>>,
916}
917
918#[derive(Clone, Debug, PartialEq, Copy)]
919pub enum UuidOrSignature {
920    Uuid(Uuid),
921    Signature(Signature),
922}
923
924impl std::fmt::Display for UuidOrSignature {
925    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
926        match self {
927            UuidOrSignature::Uuid(uuid) => write!(f, "{}", uuid),
928            UuidOrSignature::Signature(signature) => write!(f, "{}", signature),
929        }
930    }
931}
932
933impl<'de> Deserialize<'de> for UuidOrSignature {
934    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
935    where
936        D: Deserializer<'de>,
937    {
938        let s = String::deserialize(deserializer)?;
939
940        if let Ok(uuid) = Uuid::parse_str(&s) {
941            return Ok(UuidOrSignature::Uuid(uuid));
942        }
943
944        if let Ok(signature) = s.parse::<Signature>() {
945            return Ok(UuidOrSignature::Signature(signature));
946        }
947
948        Err(serde::de::Error::custom(
949            "expected a Uuid or a valid Solana Signature",
950        ))
951    }
952}
953
954impl Serialize for UuidOrSignature {
955    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
956    where
957        S: Serializer,
958    {
959        match self {
960            UuidOrSignature::Uuid(uuid) => serializer.serialize_str(&uuid.to_string()),
961            UuidOrSignature::Signature(signature) => {
962                serializer.serialize_str(&signature.to_string())
963            }
964        }
965    }
966}
967
968#[derive(Debug, Clone, Deserialize, Serialize)]
969pub enum DataIndexingCommand {
970    ProcessCollection(Uuid),
971    ProcessCollectionEntriesPack(Uuid, Vec<u8>),
972}
973
974// Define a wrapper struct
975#[derive(Debug, Clone, Serialize, Deserialize)]
976pub struct VersionedIdl(pub Slot, pub Idl);
977
978// Implement ordering based on Slot
979impl PartialEq for VersionedIdl {
980    fn eq(&self, other: &Self) -> bool {
981        self.0 == other.0
982    }
983}
984
985impl Eq for VersionedIdl {}
986
987impl PartialOrd for VersionedIdl {
988    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
989        Some(self.cmp(other))
990    }
991}
992
993impl Ord for VersionedIdl {
994    fn cmp(&self, other: &Self) -> Ordering {
995        self.0.cmp(&other.0)
996    }
997}
998
999#[derive(Debug, Clone)]
1000pub struct FifoMap<K, V> {
1001    // IndexMap is a map that preserves the insertion order of the keys. (It will be used for the FIFO eviction)
1002    map: IndexMap<K, V>,
1003}
1004
1005impl<K: std::hash::Hash + Eq, V> Default for FifoMap<K, V> {
1006    fn default() -> Self {
1007        Self::new(DEFAULT_PROFILING_MAP_CAPACITY)
1008    }
1009}
1010impl<K: std::hash::Hash + Eq, V> FifoMap<K, V> {
1011    pub fn new(capacity: usize) -> Self {
1012        Self {
1013            map: IndexMap::with_capacity(capacity),
1014        }
1015    }
1016
1017    pub fn capacity(&self) -> usize {
1018        self.map.capacity()
1019    }
1020
1021    pub fn len(&self) -> usize {
1022        self.map.len()
1023    }
1024
1025    pub fn clear(&mut self) {
1026        self.map.clear();
1027    }
1028
1029    pub fn is_empty(&self) -> bool {
1030        self.map.is_empty()
1031    }
1032
1033    /// Insert a key/value. If `K` is new and we're full, evict the oldest (FIFO)
1034    /// Returns a tuple of (old_value, evicted_key):
1035    /// - old_value: The previous value if this was an update to an existing key
1036    /// - evicted_key: The key that was evicted if the map was at capacity
1037    pub fn insert(&mut self, key: K, value: V) -> (Option<V>, Option<K>) {
1038        if self.map.contains_key(&key) {
1039            // Update doesn't change insertion order in IndexMap
1040            return (self.map.insert(key, value), None);
1041        }
1042        let evicted_key = if self.map.len() == self.map.capacity() {
1043            // Evict oldest (index 0). O(n) due shifting the rest of the map
1044            // We could use a hashmap + vecdeque to get O(1) here, but then we'd have to handle removing from both maps, storing the index, and managing the eviction.
1045            // This is a good compromise between performance and simplicity. And thinking about memory usage, this is probably the best way to go.
1046            self.map.shift_remove_index(0).map(|(k, _)| k)
1047        } else {
1048            None
1049        };
1050        self.map.insert(key, value);
1051        (None, evicted_key)
1052    }
1053
1054    pub fn get(&self, key: &K) -> Option<&V> {
1055        self.map.get(key)
1056    }
1057
1058    pub fn get_mut(&mut self, key: &K) -> Option<&mut V> {
1059        self.map.get_mut(key)
1060    }
1061
1062    pub fn contains_key(&self, key: &K) -> bool {
1063        self.map.contains_key(key)
1064    }
1065
1066    /// Removes a key from the map, returning the value if present.
1067    pub fn remove(&mut self, key: &K) -> Option<V> {
1068        self.map.shift_remove(key)
1069    }
1070
1071    // This is a wrapper around the IndexMap::iter() method, but it preserves the insertion order of the keys.
1072    // It's used to iterate over the profiling map in the order of the keys being inserted.
1073    pub fn iter(&self) -> impl ExactSizeIterator<Item = (&K, &V)> {
1074        self.map.iter()
1075    }
1076}
1077
1078#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1079#[serde(rename_all = "camelCase")]
1080pub struct AccountSnapshot {
1081    pub lamports: u64,
1082    pub owner: String,
1083    pub executable: bool,
1084    pub rent_epoch: u64,
1085    /// Base64 encoded data
1086    pub data: String,
1087    /// Parsed account data if available
1088    pub parsed_data: Option<ParsedAccount>,
1089}
1090
1091impl AccountSnapshot {
1092    pub fn new(
1093        lamports: u64,
1094        owner: String,
1095        executable: bool,
1096        rent_epoch: u64,
1097        data: String,
1098        parsed_data: Option<ParsedAccount>,
1099    ) -> Self {
1100        Self {
1101            lamports,
1102            owner,
1103            executable,
1104            rent_epoch,
1105            data,
1106            parsed_data,
1107        }
1108    }
1109}
1110
1111#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1112#[serde(rename_all = "camelCase")]
1113pub struct ExportSnapshotConfig {
1114    pub include_parsed_accounts: Option<bool>,
1115    pub filter: Option<ExportSnapshotFilter>,
1116    pub scope: ExportSnapshotScope,
1117}
1118
1119#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1120#[serde(rename_all = "camelCase")]
1121pub enum ExportSnapshotScope {
1122    #[default]
1123    Network,
1124    PreTransaction(String),
1125}
1126
1127#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1128#[serde(rename_all = "camelCase")]
1129pub struct ExportSnapshotFilter {
1130    pub include_program_accounts: Option<bool>,
1131    pub include_accounts: Option<Vec<String>>,
1132    pub exclude_accounts: Option<Vec<String>>,
1133}
1134
1135#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1136#[serde(rename_all = "camelCase")]
1137pub struct ResetAccountConfig {
1138    pub include_owned_accounts: Option<bool>,
1139}
1140
1141impl Default for ResetAccountConfig {
1142    fn default() -> Self {
1143        Self {
1144            include_owned_accounts: Some(false),
1145        }
1146    }
1147}
1148
1149#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1150#[serde(rename_all = "camelCase")]
1151pub struct StreamAccountConfig {
1152    pub include_owned_accounts: Option<bool>,
1153}
1154
1155impl Default for StreamAccountConfig {
1156    fn default() -> Self {
1157        Self {
1158            include_owned_accounts: Some(false),
1159        }
1160    }
1161}
1162
1163#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1164#[serde(rename_all = "camelCase")]
1165pub struct StreamAccountsEntry {
1166    pub pubkey: String,
1167    #[serde(default)]
1168    pub include_owned_accounts: Option<bool>,
1169}
1170
1171#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1172#[serde(rename_all = "camelCase")]
1173pub struct OfflineAccountConfig {
1174    pub include_owned_accounts: Option<bool>,
1175}
1176
1177impl Default for OfflineAccountConfig {
1178    fn default() -> Self {
1179        Self {
1180            include_owned_accounts: Some(false),
1181        }
1182    }
1183}
1184
1185#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1186#[serde(rename_all = "camelCase")]
1187pub struct StreamedAccountInfo {
1188    pub pubkey: String,
1189    pub include_owned_accounts: bool,
1190}
1191
1192#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1193#[serde(rename_all = "camelCase")]
1194pub struct GetSurfnetInfoResponse {
1195    runbook_executions: Vec<RunbookExecutionStatusReport>,
1196}
1197impl GetSurfnetInfoResponse {
1198    pub fn new(runbook_executions: Vec<RunbookExecutionStatusReport>) -> Self {
1199        Self { runbook_executions }
1200    }
1201}
1202
1203#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1204#[serde(rename_all = "camelCase")]
1205pub struct GetStreamedAccountsResponse {
1206    accounts: Vec<StreamedAccountInfo>,
1207}
1208impl GetStreamedAccountsResponse {
1209    pub fn from_iter<I>(streamed_accounts: I) -> Self
1210    where
1211        I: IntoIterator<Item = (String, bool)>,
1212    {
1213        let accounts = streamed_accounts
1214            .into_iter()
1215            .map(|(pubkey, include_owned_accounts)| StreamedAccountInfo {
1216                pubkey,
1217                include_owned_accounts,
1218            })
1219            .collect();
1220        Self { accounts }
1221    }
1222}
1223
1224#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1225#[serde(rename_all = "camelCase")]
1226pub struct RunbookExecutionStatusReport {
1227    pub started_at: u32,
1228    pub completed_at: Option<u32>,
1229    pub runbook_id: String,
1230    pub errors: Option<Vec<String>>,
1231}
1232impl RunbookExecutionStatusReport {
1233    pub fn new(runbook_id: String) -> Self {
1234        Self {
1235            started_at: Local::now().timestamp() as u32,
1236            completed_at: None,
1237            runbook_id,
1238            errors: None,
1239        }
1240    }
1241    pub fn mark_completed(&mut self, error: Option<Vec<String>>) {
1242        self.completed_at = Some(Local::now().timestamp() as u32);
1243        self.errors = error;
1244    }
1245}
1246/// WebSocket subscription counts
1247#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
1248pub struct WsSubscriptions {
1249    pub signatures: usize,
1250    pub accounts: usize,
1251    pub slots: usize,
1252    pub logs: usize,
1253}
1254
1255/// Surfpool node status information
1256#[derive(Debug, Clone, Serialize, Deserialize)]
1257pub struct SurfpoolStatus {
1258    pub slot: u64,
1259    pub epoch: u64,
1260    pub slot_index: u64,
1261    pub transactions_count: u64,
1262    pub transactions_processed: u64,
1263    pub uptime_ms: u64,
1264    pub ws_subscriptions: WsSubscriptions,
1265}
1266
1267#[cfg(feature = "prometheus")]
1268fn default_prometheus_addr() -> String {
1269    "0.0.0.0:9000".to_string()
1270}
1271
1272#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1273#[serde(rename_all = "camelCase")]
1274pub struct CheatcodeConfig {
1275    pub lockout: bool, // if true, allows disabling even the `surfnet_enableCheatcodes`/`surfnetdisableCheatcodes` methods
1276    pub filter: CheatcodeFilter,
1277}
1278
1279#[derive(Serialize, Deserialize, Default)]
1280pub struct CheatcodeControlConfig {
1281    pub lockout: Option<bool>,
1282}
1283
1284#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1285#[serde(untagged)]
1286pub enum CheatcodeFilter {
1287    All(String),
1288    List(Vec<String>), // disables cheatcodes in a named list
1289}
1290
1291impl CheatcodeConfig {
1292    pub fn new() -> Arc<Mutex<Self>> {
1293        Arc::new(Mutex::new(CheatcodeConfig {
1294            lockout: false,
1295            filter: CheatcodeFilter::List(vec![]),
1296        }))
1297    }
1298
1299    pub fn lockout(&mut self) {
1300        self.lockout = true;
1301    }
1302
1303    pub fn disable_all(&mut self, lockout: bool, available_cheatcodes: Vec<String>) {
1304        if lockout {
1305            self.lockout = true;
1306        }
1307        self.filter = Self::filter_all_list(lockout, available_cheatcodes);
1308    }
1309
1310    pub fn disable_cheatcode(&mut self, cheatcode: &String) -> Result<(), String> {
1311        if !self.lockout
1312            && (cheatcode.eq("surfnet_enableCheatcode") || cheatcode.eq("surfnet_disableCheatcode"))
1313        {
1314            return Err("Cannot disable surfnet_disableCheatcode or surfnet_enableCheatcode while lockout is false".to_string());
1315        }
1316
1317        if let CheatcodeFilter::List(list) = &mut self.filter {
1318            if !list.contains(cheatcode) {
1319                list.push(cheatcode.to_string());
1320                Ok(())
1321            } else {
1322                Err("Cheatcode already disabled".to_string())
1323            }
1324        } else {
1325            Err("All cheatcodes disabled".to_string())
1326        }
1327    }
1328    pub fn enable_cheatcode(&mut self, cheatcode: &str) -> Result<(), String> {
1329        if let CheatcodeFilter::List(list) = &mut self.filter {
1330            if let Some(pos) = list.iter().position(|c| c == cheatcode) {
1331                list.remove(pos);
1332                Ok(())
1333            } else {
1334                Err("Cheatcode isn't disabled".to_string())
1335            }
1336        } else {
1337            Err("All cheatcodes are disabled".to_string())
1338        }
1339    }
1340
1341    pub fn is_cheatcode_disabled(&self, cheatcode: &String) -> bool {
1342        match &self.filter {
1343            CheatcodeFilter::List(list) => list.contains(cheatcode),
1344            CheatcodeFilter::All(_) => true,
1345        }
1346    }
1347
1348    pub fn filter_all_list(lockout: bool, available_cheatcodes: Vec<String>) -> CheatcodeFilter {
1349        // when lockout == true, it's important to disable surfnet_disableCheatcode as well
1350        // since calling surfnet_disableCheatcode with lockout == false will override the current config, which is a bug
1351        if lockout {
1352            CheatcodeFilter::All("all".to_string())
1353        } else {
1354            // remove `surfnet_disableCheatcode` and `surfnet_enableCheatcode` from the list of available cheatcodes
1355            let filter = available_cheatcodes
1356                .into_iter()
1357                .filter(|c| (c.ne("surfnet_disableCheatcode") && c.ne("surfnet_enableCheatcode")))
1358                .collect();
1359            CheatcodeFilter::List(filter)
1360        }
1361    }
1362}
1363
1364#[cfg(test)]
1365mod tests {
1366    use serde_json::json;
1367    use solana_account_decoder_client_types::{ParsedAccount, UiAccountData};
1368
1369    use super::*;
1370
1371    #[test]
1372    fn test_disable_cheatcode_with_lockout_allows_protected_methods() {
1373        // This test catches the bug where lockout was not propagated to
1374        // CheatcodeConfig before calling disable_cheatcode(), causing
1375        // "Cannot disable surfnet_disableCheatcode or surfnet_enableCheatcode
1376        // while lockout is false" even when the request included lockout: true.
1377        let config = CheatcodeConfig::new();
1378        let mut config = config.lock().unwrap();
1379
1380        // Simulate the RPC layer propagating lockout before processing the list
1381        config.lockout();
1382
1383        // These should succeed because lockout is set
1384        assert!(
1385            config
1386                .disable_cheatcode(&"surfnet_setAccount".to_string())
1387                .is_ok()
1388        );
1389        assert!(
1390            config
1391                .disable_cheatcode(&"surfnet_enableCheatcode".to_string())
1392                .is_ok()
1393        );
1394        assert!(
1395            config
1396                .disable_cheatcode(&"surfnet_disableCheatcode".to_string())
1397                .is_ok()
1398        );
1399    }
1400
1401    #[test]
1402    fn test_disable_cheatcode_without_lockout_rejects_protected_methods() {
1403        let config = CheatcodeConfig::new();
1404        let mut config = config.lock().unwrap();
1405
1406        // Without lockout, disabling protected methods should fail
1407        assert!(
1408            config
1409                .disable_cheatcode(&"surfnet_enableCheatcode".to_string())
1410                .is_err()
1411        );
1412        assert!(
1413            config
1414                .disable_cheatcode(&"surfnet_disableCheatcode".to_string())
1415                .is_err()
1416        );
1417
1418        // But regular cheatcodes should still work
1419        assert!(
1420            config
1421                .disable_cheatcode(&"surfnet_setAccount".to_string())
1422                .is_ok()
1423        );
1424    }
1425
1426    #[test]
1427    fn test_disable_all_with_lockout_persists_lockout_flag() {
1428        // This test catches the bug where disable_all() did not set
1429        // self.lockout = true, so subsequent operations would not see lockout.
1430        let config = CheatcodeConfig::new();
1431        let mut config = config.lock().unwrap();
1432
1433        let available = vec![
1434            "surfnet_setAccount".to_string(),
1435            "surfnet_enableCheatcode".to_string(),
1436            "surfnet_disableCheatcode".to_string(),
1437        ];
1438
1439        config.disable_all(true, available);
1440        assert!(config.lockout);
1441    }
1442
1443    #[test]
1444    fn test_disable_all_without_lockout_does_not_set_lockout() {
1445        let config = CheatcodeConfig::new();
1446        let mut config = config.lock().unwrap();
1447
1448        let available = vec![
1449            "surfnet_setAccount".to_string(),
1450            "surfnet_enableCheatcode".to_string(),
1451            "surfnet_disableCheatcode".to_string(),
1452        ];
1453
1454        config.disable_all(false, available);
1455        assert!(!config.lockout);
1456    }
1457
1458    #[test]
1459    fn print_ui_keyed_profile_result() {
1460        let pubkey = Pubkey::new_unique();
1461        let owner = Pubkey::new_unique();
1462        let readonly_account_state = UiAccount {
1463            lamports: 100,
1464            data: UiAccountData::Binary(
1465                "ABCDEFG".into(),
1466                solana_account_decoder_client_types::UiAccountEncoding::Base64,
1467            ),
1468            owner: owner.to_string(),
1469            executable: false,
1470            rent_epoch: 0,
1471            space: Some(100),
1472        };
1473
1474        let account_1 = UiAccount {
1475            lamports: 100,
1476            data: UiAccountData::Json(ParsedAccount {
1477                program: "custom-program".into(),
1478                parsed: json!({
1479                    "field1": "value1",
1480                    "field2": "value2"
1481                }),
1482                space: 50,
1483            }),
1484            owner: owner.to_string(),
1485            executable: false,
1486            rent_epoch: 0,
1487            space: Some(100),
1488        };
1489
1490        let account_2 = UiAccount {
1491            lamports: 100,
1492            data: UiAccountData::Json(ParsedAccount {
1493                program: "custom-program".into(),
1494                parsed: json!({
1495                    "field1": "updated-value1",
1496                    "field2": "updated-value2"
1497                }),
1498                space: 50,
1499            }),
1500            owner: owner.to_string(),
1501            executable: false,
1502            rent_epoch: 0,
1503            space: Some(100),
1504        };
1505        let profile_result = UiKeyedProfileResult {
1506            slot: 123,
1507            key: UuidOrSignature::Uuid(Uuid::new_v4()),
1508            instruction_profiles: Some(vec![
1509                UiProfileResult {
1510                    account_states: IndexMap::from_iter([
1511                        (
1512                            pubkey,
1513                            UiAccountProfileState::Writable(UiAccountChange::Create(
1514                                account_1.clone(),
1515                            )),
1516                        ),
1517                        (owner, UiAccountProfileState::Readonly),
1518                    ]),
1519                    compute_units_consumed: 100,
1520                    log_messages: Some(vec![
1521                        "Log message: Creating Account".to_string(),
1522                        "Log message: Account created".to_string(),
1523                    ]),
1524                    error_message: None,
1525                },
1526                UiProfileResult {
1527                    account_states: IndexMap::from_iter([
1528                        (
1529                            pubkey,
1530                            UiAccountProfileState::Writable(UiAccountChange::Update(
1531                                account_1,
1532                                account_2.clone(),
1533                            )),
1534                        ),
1535                        (owner, UiAccountProfileState::Readonly),
1536                    ]),
1537                    compute_units_consumed: 100,
1538                    log_messages: Some(vec![
1539                        "Log message: Updating Account".to_string(),
1540                        "Log message: Account updated".to_string(),
1541                    ]),
1542                    error_message: None,
1543                },
1544                UiProfileResult {
1545                    account_states: IndexMap::from_iter([
1546                        (
1547                            pubkey,
1548                            UiAccountProfileState::Writable(UiAccountChange::Delete(account_2)),
1549                        ),
1550                        (owner, UiAccountProfileState::Readonly),
1551                    ]),
1552                    compute_units_consumed: 100,
1553                    log_messages: Some(vec![
1554                        "Log message: Deleting Account".to_string(),
1555                        "Log message: Account deleted".to_string(),
1556                    ]),
1557                    error_message: None,
1558                },
1559            ]),
1560            transaction_profile: UiProfileResult {
1561                account_states: IndexMap::from_iter([
1562                    (
1563                        pubkey,
1564                        UiAccountProfileState::Writable(UiAccountChange::Unchanged(None)),
1565                    ),
1566                    (owner, UiAccountProfileState::Readonly),
1567                ]),
1568                compute_units_consumed: 300,
1569                log_messages: Some(vec![
1570                    "Log message: Creating Account".to_string(),
1571                    "Log message: Account created".to_string(),
1572                    "Log message: Updating Account".to_string(),
1573                    "Log message: Account updated".to_string(),
1574                    "Log message: Deleting Account".to_string(),
1575                    "Log message: Account deleted".to_string(),
1576                ]),
1577                error_message: None,
1578            },
1579            readonly_account_states: IndexMap::from_iter([(owner, readonly_account_state)]),
1580        };
1581        println!("{}", serde_json::to_string_pretty(&profile_result).unwrap());
1582    }
1583
1584    #[test]
1585    fn test_profiling_map_capacity() {
1586        let profiling_map = FifoMap::<Signature, KeyedProfileResult>::new(10);
1587        assert_eq!(profiling_map.capacity(), 10);
1588    }
1589
1590    #[test]
1591    fn test_profiling_map_len() {
1592        let profiling_map = FifoMap::<Signature, KeyedProfileResult>::new(10);
1593        assert!(profiling_map.len() == 0);
1594    }
1595
1596    #[test]
1597    fn test_profiling_map_is_empty() {
1598        let profiling_map = FifoMap::<Signature, KeyedProfileResult>::new(10);
1599        assert_eq!(profiling_map.is_empty(), true);
1600    }
1601
1602    #[test]
1603    fn test_profiling_map_insert() {
1604        let mut profiling_map = FifoMap::<Signature, KeyedProfileResult>::new(10);
1605        let key = Signature::default();
1606        let value = KeyedProfileResult::new(
1607            1,
1608            UuidOrSignature::Signature(key),
1609            None,
1610            ProfileResult::new(BTreeMap::new(), BTreeMap::new(), 0, None, None),
1611            HashMap::new(),
1612        );
1613        profiling_map.insert(key, value.clone());
1614        assert_eq!(profiling_map.len(), 1);
1615    }
1616
1617    #[test]
1618    fn test_profiling_map_get() {
1619        let mut profiling_map = FifoMap::<Signature, KeyedProfileResult>::new(10);
1620        let key = Signature::default();
1621        let value = KeyedProfileResult::new(
1622            1,
1623            UuidOrSignature::Signature(key),
1624            None,
1625            ProfileResult::new(BTreeMap::new(), BTreeMap::new(), 0, None, None),
1626            HashMap::new(),
1627        );
1628        profiling_map.insert(key, value.clone());
1629
1630        assert_eq!(profiling_map.get(&key), Some(&value));
1631    }
1632
1633    #[test]
1634    fn test_profiling_map_get_mut() {
1635        let mut profiling_map = FifoMap::<Signature, KeyedProfileResult>::new(10);
1636        let key = Signature::default();
1637        let mut value = KeyedProfileResult::new(
1638            1,
1639            UuidOrSignature::Signature(key),
1640            None,
1641            ProfileResult::new(BTreeMap::new(), BTreeMap::new(), 0, None, None),
1642            HashMap::new(),
1643        );
1644        profiling_map.insert(key, value.clone());
1645        assert_eq!(profiling_map.get_mut(&key), Some(&mut value));
1646    }
1647
1648    #[test]
1649    fn test_profiling_map_contains_key() {
1650        let mut profiling_map = FifoMap::<Signature, KeyedProfileResult>::new(10);
1651        let key = Signature::default();
1652        let value = KeyedProfileResult::new(
1653            1,
1654            UuidOrSignature::Signature(key),
1655            None,
1656            ProfileResult::new(BTreeMap::new(), BTreeMap::new(), 0, None, None),
1657            HashMap::new(),
1658        );
1659        profiling_map.insert(key, value.clone());
1660
1661        assert_eq!(profiling_map.contains_key(&key), true);
1662    }
1663
1664    #[test]
1665    fn test_profiling_map_iter() {
1666        let mut profiling_map = FifoMap::<Signature, KeyedProfileResult>::new(10);
1667        let key = Signature::default();
1668        let value = KeyedProfileResult::new(
1669            1,
1670            UuidOrSignature::Signature(key),
1671            None,
1672            ProfileResult::new(BTreeMap::new(), BTreeMap::new(), 0, None, None),
1673            HashMap::new(),
1674        );
1675        profiling_map.insert(key, value.clone());
1676
1677        assert_eq!(profiling_map.iter().count(), 1);
1678    }
1679
1680    #[test]
1681    fn test_profiling_map_evicts_oldest_on_overflow() {
1682        let mut profiling_map = FifoMap::<String, u32>::new(10);
1683        profiling_map.insert("a".to_string(), 1);
1684        profiling_map.insert("b".to_string(), 2);
1685        profiling_map.insert("c".to_string(), 3);
1686        profiling_map.insert("d".to_string(), 4);
1687        profiling_map.insert("e".to_string(), 5);
1688        profiling_map.insert("f".to_string(), 6);
1689        profiling_map.insert("g".to_string(), 7);
1690        profiling_map.insert("h".to_string(), 8);
1691        profiling_map.insert("i".to_string(), 9);
1692        profiling_map.insert("j".to_string(), 10);
1693
1694        println!("Profiling map: {:?}", profiling_map);
1695        println!("Profile Map capacity: {:?}", profiling_map.capacity());
1696        println!("Profile Map len: {:?}", profiling_map.len());
1697
1698        assert_eq!(profiling_map.len(), 10);
1699
1700        // Now insert one more, which should evict the oldest
1701        profiling_map.insert("k".to_string(), 11);
1702        assert_eq!(profiling_map.len(), 10);
1703        assert_eq!(profiling_map.get(&"a".to_string()), None);
1704        assert_eq!(profiling_map.get(&"k".to_string()), Some(&11));
1705    }
1706
1707    #[test]
1708    fn test_profiling_map_update_do_not_reorder() {
1709        let mut profiling_map = FifoMap::<&str, u32>::new(4);
1710        profiling_map.insert("a", 1);
1711        profiling_map.insert("b", 2);
1712        profiling_map.insert("c", 3);
1713        profiling_map.insert("d", 4);
1714
1715        //update b, should not reorder (order remains a:1,b:2,c:3,d:4)
1716        println!("Profiling map: {:?}", profiling_map);
1717        println!("Profile Map key b holds: {:?}", profiling_map.get(&"b"));
1718        profiling_map.insert("b", 4);
1719        println!("Profile Map key b holds: {:?}", profiling_map.get(&"b"));
1720
1721        //overflow with a new key, should evict the oldest (a)
1722        profiling_map.insert("e", 5);
1723        assert_eq!(profiling_map.len(), 4);
1724        assert_eq!(profiling_map.get(&"a"), None);
1725        assert_eq!(profiling_map.get(&"b"), Some(&4));
1726        assert_eq!(profiling_map.get(&"e"), Some(&5));
1727
1728        let get: Vec<_> = profiling_map.iter().map(|(k, v)| (*k, *v)).collect();
1729        println!("Profiling map: {:?}", get);
1730        assert_eq!(get, vec![("b", 4), ("c", 3), ("d", 4), ("e", 5)]);
1731    }
1732
1733    #[test]
1734    fn test_export_snapshot_scope_serialization() {
1735        // Test Network variant
1736        let network_config = ExportSnapshotConfig {
1737            include_parsed_accounts: None,
1738            filter: None,
1739            scope: ExportSnapshotScope::Network,
1740        };
1741        let network_json = serde_json::to_value(&network_config).unwrap();
1742        println!(
1743            "Network config: {}",
1744            serde_json::to_string_pretty(&network_json).unwrap()
1745        );
1746        assert_eq!(network_json["scope"], json!("network"));
1747
1748        // Test PreTransaction variant
1749        let pre_tx_config = ExportSnapshotConfig {
1750            include_parsed_accounts: None,
1751            filter: None,
1752            scope: ExportSnapshotScope::PreTransaction("5signature123".to_string()),
1753        };
1754        let pre_tx_json = serde_json::to_value(&pre_tx_config).unwrap();
1755        println!(
1756            "PreTransaction config: {}",
1757            serde_json::to_string_pretty(&pre_tx_json).unwrap()
1758        );
1759        assert_eq!(
1760            pre_tx_json["scope"],
1761            json!({"preTransaction": "5signature123"})
1762        );
1763
1764        // Test deserialization
1765        let deserialized_network: ExportSnapshotConfig =
1766            serde_json::from_value(network_json).unwrap();
1767        assert_eq!(deserialized_network.scope, ExportSnapshotScope::Network);
1768
1769        let deserialized_pre_tx: ExportSnapshotConfig =
1770            serde_json::from_value(pre_tx_json).unwrap();
1771        assert_eq!(
1772            deserialized_pre_tx.scope,
1773            ExportSnapshotScope::PreTransaction("5signature123".to_string())
1774        );
1775    }
1776
1777    #[test]
1778    fn test_sanitize_datasource_url_strips_path_and_query() {
1779        // API key in path should be stripped
1780        let config = SimnetConfig {
1781            remote_rpc_url: Some(
1782                "https://example.rpc-provider.com/v2/abc123def456ghi789".to_string(),
1783            ),
1784            ..Default::default()
1785        };
1786        let sanitized = config.get_sanitized_datasource_url().unwrap();
1787        assert_eq!(sanitized, "https://example.rpc-provider.com");
1788        assert!(!sanitized.contains("abc123"));
1789    }
1790
1791    #[test]
1792    fn test_sanitize_datasource_url_strips_query_params() {
1793        let config = SimnetConfig {
1794            remote_rpc_url: Some(
1795                "https://mainnet.helius-rpc.com/?api-key=secret-key-12345".to_string(),
1796            ),
1797            ..Default::default()
1798        };
1799        let sanitized = config.get_sanitized_datasource_url().unwrap();
1800        assert_eq!(sanitized, "https://mainnet.helius-rpc.com");
1801        assert!(!sanitized.contains("secret-key"));
1802    }
1803
1804    #[test]
1805    fn test_sanitize_datasource_url_public_rpc() {
1806        let config = SimnetConfig {
1807            remote_rpc_url: Some("https://api.mainnet-beta.solana.com".to_string()),
1808            ..Default::default()
1809        };
1810        let sanitized = config.get_sanitized_datasource_url().unwrap();
1811        assert_eq!(sanitized, "https://api.mainnet-beta.solana.com");
1812    }
1813
1814    #[test]
1815    fn test_sanitize_datasource_url_none() {
1816        let config = SimnetConfig {
1817            remote_rpc_url: None,
1818            ..Default::default()
1819        };
1820        assert!(config.get_sanitized_datasource_url().is_none());
1821    }
1822
1823    #[test]
1824    fn test_sanitize_datasource_url_invalid() {
1825        let config = SimnetConfig {
1826            remote_rpc_url: Some("not-a-valid-url".to_string()),
1827            ..Default::default()
1828        };
1829        assert!(config.get_sanitized_datasource_url().is_none());
1830    }
1831}