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    #[serde(default)]
613    pub skip_blockhash_check: bool,
614    /// Unique identifier for this surfnet instance. Used to isolate database storage
615    /// when multiple surfnets share the same database. Defaults to "default".
616    pub surfnet_id: String,
617    /// Snapshot accounts to preload at startup.
618    /// Keys are pubkey strings, values can be None to fetch from remote RPC.
619    pub snapshot: BTreeMap<String, Option<AccountSnapshot>>,
620}
621
622impl Default for SimnetConfig {
623    fn default() -> Self {
624        Self {
625            offline_mode: false,
626            remote_rpc_url: Some(DEFAULT_MAINNET_RPC_URL.to_string()),
627            slot_time: DEFAULT_SLOT_TIME_MS, // Default to 400ms to match CLI default
628            block_production_mode: BlockProductionMode::Clock,
629            airdrop_addresses: vec![],
630            airdrop_token_amount: 0,
631            expiry: None,
632            instruction_profiling_enabled: true,
633            max_profiles: DEFAULT_PROFILING_MAP_CAPACITY,
634            log_bytes_limit: Some(10_000),
635            skip_signature_verification: false,
636            skip_blockhash_check: false,
637            surfnet_id: "default".to_string(),
638            snapshot: BTreeMap::new(),
639        }
640    }
641}
642
643impl SimnetConfig {
644    /// Returns a sanitized version of the datasource URL safe for display.
645    /// Only returns scheme and host (e.g., "https://example.com") to prevent
646    /// leaking API keys in paths or query parameters.
647    pub fn get_sanitized_datasource_url(&self) -> Option<String> {
648        let raw = self.remote_rpc_url.as_ref()?;
649
650        if let Ok(url) = url::Url::parse(raw) {
651            let scheme = url.scheme();
652            let host = url.host_str()?;
653            Some(format!("{}://{}", scheme, host))
654        } else {
655            None
656        }
657    }
658}
659
660#[derive(Clone, Debug, Default, Serialize, Deserialize)]
661pub struct SubgraphConfig {}
662
663pub const DEFAULT_GOSSIP_PORT: u16 = 8001;
664pub const DEFAULT_TPU_PORT: u16 = 8003;
665pub const DEFAULT_TPU_QUIC_PORT: u16 = 8004;
666
667#[derive(Clone, Debug, Serialize, Deserialize)]
668pub struct RpcConfig {
669    pub bind_host: String,
670    pub bind_port: u16,
671    pub ws_port: u16,
672    pub gossip_port: u16,
673    pub tpu_port: u16,
674    pub tpu_quic_port: u16,
675}
676
677impl RpcConfig {
678    pub fn get_rpc_base_url(&self) -> String {
679        format!("{}:{}", self.bind_host, self.bind_port)
680    }
681    pub fn get_ws_base_url(&self) -> String {
682        format!("{}:{}", self.bind_host, self.ws_port)
683    }
684}
685
686impl Default for RpcConfig {
687    fn default() -> Self {
688        Self {
689            bind_host: DEFAULT_NETWORK_HOST.to_string(),
690            bind_port: DEFAULT_RPC_PORT,
691            ws_port: DEFAULT_WS_PORT,
692            gossip_port: DEFAULT_GOSSIP_PORT,
693            tpu_port: DEFAULT_TPU_PORT,
694            tpu_quic_port: DEFAULT_TPU_QUIC_PORT,
695        }
696    }
697}
698
699#[derive(Clone, Debug, Serialize, Deserialize)]
700pub struct StudioConfig {
701    pub bind_host: String,
702    pub bind_port: u16,
703}
704
705impl StudioConfig {
706    pub fn get_studio_base_url(&self) -> String {
707        format!("{}:{}", self.bind_host, self.bind_port)
708    }
709}
710
711impl Default for StudioConfig {
712    fn default() -> Self {
713        Self {
714            bind_host: DEFAULT_NETWORK_HOST.to_string(),
715            bind_port: CHANGE_TO_DEFAULT_STUDIO_PORT_ONCE_SUPERVISOR_MERGED,
716        }
717    }
718}
719
720#[derive(Serialize, Deserialize, Clone, Debug)]
721#[serde(rename_all = "snake_case")]
722pub struct CreateSurfnetRequest {
723    pub domain: String,
724    pub block_production_mode: BlockProductionMode,
725    pub datasource_rpc_url: String,
726    pub settings: Option<CloudSurfnetSettings>,
727}
728
729#[derive(Serialize, Deserialize, Clone, Debug, Default)]
730#[serde(rename_all = "snake_case", default)]
731pub struct CloudSurfnetSettings {
732    pub database_url: Option<String>,
733    pub profiling_disabled: Option<bool>,
734    #[serde(skip_serializing_if = "Option::is_none")]
735    pub gating: Option<CloudSurfnetRpcGating>,
736}
737
738#[derive(Serialize, Deserialize, Clone, Debug, Default)]
739#[serde(rename_all = "snake_case", default)]
740pub struct CloudSurfnetRpcGating {
741    pub private_methods_secret_token: Option<String>,
742    pub private_methods: Vec<String>,
743    pub public_methods: Vec<String>,
744    pub disabled_methods: Vec<String>,
745}
746
747impl CloudSurfnetRpcGating {
748    pub fn restricted() -> CloudSurfnetRpcGating {
749        CloudSurfnetRpcGating {
750            private_methods: vec![],
751            private_methods_secret_token: None,
752            public_methods: vec![],
753            disabled_methods: vec![
754                "surfnet_cloneProgramAccount".into(),
755                "surfnet_profileTransaction".into(),
756                "surfnet_getProfileResultsByTag".into(),
757                "surfnet_setSupply".into(),
758                "surfnet_setProgramAuthority".into(),
759                "surfnet_getTransactionProfile".into(),
760                "surfnet_registerIdl".into(),
761                "surfnet_getActiveIdl".into(),
762                "surfnet_getLocalSignatures".into(),
763                "surfnet_timeTravel".into(),
764                "surfnet_pauseClock".into(),
765                "surfnet_resumeClock".into(),
766                "surfnet_resetAccount".into(),
767                "surfnet_resetNetwork".into(),
768                "surfnet_exportSnapshot".into(),
769                "surfnet_offlineAccount".into(),
770                "surfnet_streamAccount".into(),
771                "surfnet_streamAccounts".into(),
772                "surfnet_getStreamedAccounts".into(),
773            ],
774        }
775    }
776}
777
778#[derive(Serialize, Deserialize)]
779#[serde(rename_all = "snake_case")]
780pub struct CreateNetworkRequest {
781    pub workspace_id: Uuid,
782    pub name: String,
783    pub description: Option<String>,
784    pub datasource_rpc_url: String,
785    pub block_production_mode: BlockProductionMode,
786    pub profiling_enabled: Option<bool>,
787}
788
789impl CreateNetworkRequest {
790    pub fn new(
791        workspace_id: Uuid,
792        name: String,
793        description: Option<String>,
794        datasource_rpc_url: String,
795        block_production_mode: BlockProductionMode,
796        profiling_enabled: bool,
797    ) -> Self {
798        Self {
799            workspace_id,
800            name,
801            description,
802            datasource_rpc_url,
803            block_production_mode,
804            profiling_enabled: Some(profiling_enabled),
805        }
806    }
807}
808
809#[derive(Serialize, Deserialize)]
810pub struct CreateNetworkResponse {
811    pub rpc_url: String,
812}
813
814#[derive(Serialize, Deserialize)]
815pub struct DeleteNetworkRequest {
816    pub workspace_id: Uuid,
817    pub network_id: Uuid,
818}
819
820impl DeleteNetworkRequest {
821    pub fn new(workspace_id: Uuid, network_id: Uuid) -> Self {
822        Self {
823            workspace_id,
824            network_id,
825        }
826    }
827}
828
829#[derive(Serialize, Deserialize)]
830pub struct DeleteNetworkResponse;
831
832#[serde_as]
833#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
834#[serde(rename_all = "camelCase")]
835pub struct AccountUpdate {
836    /// providing this value sets the lamports in the account
837    pub lamports: Option<u64>,
838    /// providing this value sets the data held in this account
839    #[serde_as(as = "Option<BytesOrString>")]
840    pub data: Option<Vec<u8>>,
841    ///  providing this value sets the program that owns this account. If executable, the program that loads this account.
842    pub owner: Option<String>,
843    /// providing this value sets whether this account's data contains a loaded program (and is now read-only)
844    pub executable: Option<bool>,
845    /// providing this value sets the epoch at which this account will next owe rent
846    pub rent_epoch: Option<Epoch>,
847}
848
849#[derive(Debug, Clone)]
850pub enum SetSomeAccount {
851    Account(String),
852    NoAccount,
853}
854
855impl<'de> Deserialize<'de> for SetSomeAccount {
856    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
857    where
858        D: Deserializer<'de>,
859    {
860        struct SetSomeAccountVisitor;
861
862        impl<'de> Visitor<'de> for SetSomeAccountVisitor {
863            type Value = SetSomeAccount;
864
865            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
866                formatter.write_str("a Pubkey String or the String 'null'")
867            }
868
869            fn visit_some<D_>(self, deserializer: D_) -> std::result::Result<Self::Value, D_::Error>
870            where
871                D_: Deserializer<'de>,
872            {
873                Deserialize::deserialize(deserializer).map(|v: String| match v.as_str() {
874                    "null" => SetSomeAccount::NoAccount,
875                    _ => SetSomeAccount::Account(v.to_string()),
876                })
877            }
878        }
879
880        deserializer.deserialize_option(SetSomeAccountVisitor)
881    }
882}
883
884impl Serialize for SetSomeAccount {
885    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
886    where
887        S: Serializer,
888    {
889        match self {
890            SetSomeAccount::Account(val) => serializer.serialize_str(val),
891            SetSomeAccount::NoAccount => serializer.serialize_str("null"),
892        }
893    }
894}
895
896#[serde_as]
897#[derive(Debug, Clone, Default, Serialize, Deserialize)]
898#[serde(rename_all = "camelCase")]
899pub struct TokenAccountUpdate {
900    /// providing this value sets the amount of the token in the account data
901    pub amount: Option<u64>,
902    /// providing this value sets the delegate of the token account
903    pub delegate: Option<SetSomeAccount>,
904    /// providing this value sets the state of the token account
905    pub state: Option<String>,
906    /// providing this value sets the amount authorized to the delegate
907    pub delegated_amount: Option<u64>,
908    /// providing this value sets the close authority of the token account
909    pub close_authority: Option<SetSomeAccount>,
910}
911
912// token supply update for set supply method in SVM tricks
913#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
914pub struct SupplyUpdate {
915    pub total: Option<u64>,
916    pub circulating: Option<u64>,
917    pub non_circulating: Option<u64>,
918    pub non_circulating_accounts: Option<Vec<String>>,
919}
920
921#[derive(Clone, Debug, PartialEq, Copy)]
922pub enum UuidOrSignature {
923    Uuid(Uuid),
924    Signature(Signature),
925}
926
927impl std::fmt::Display for UuidOrSignature {
928    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
929        match self {
930            UuidOrSignature::Uuid(uuid) => write!(f, "{}", uuid),
931            UuidOrSignature::Signature(signature) => write!(f, "{}", signature),
932        }
933    }
934}
935
936impl<'de> Deserialize<'de> for UuidOrSignature {
937    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
938    where
939        D: Deserializer<'de>,
940    {
941        let s = String::deserialize(deserializer)?;
942
943        if let Ok(uuid) = Uuid::parse_str(&s) {
944            return Ok(UuidOrSignature::Uuid(uuid));
945        }
946
947        if let Ok(signature) = s.parse::<Signature>() {
948            return Ok(UuidOrSignature::Signature(signature));
949        }
950
951        Err(serde::de::Error::custom(
952            "expected a Uuid or a valid Solana Signature",
953        ))
954    }
955}
956
957impl Serialize for UuidOrSignature {
958    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
959    where
960        S: Serializer,
961    {
962        match self {
963            UuidOrSignature::Uuid(uuid) => serializer.serialize_str(&uuid.to_string()),
964            UuidOrSignature::Signature(signature) => {
965                serializer.serialize_str(&signature.to_string())
966            }
967        }
968    }
969}
970
971#[derive(Debug, Clone, Deserialize, Serialize)]
972pub enum DataIndexingCommand {
973    ProcessCollection(Uuid),
974    ProcessCollectionEntriesPack(Uuid, Vec<u8>),
975}
976
977// Define a wrapper struct
978#[derive(Debug, Clone, Serialize, Deserialize)]
979pub struct VersionedIdl(pub Slot, pub Idl);
980
981// Implement ordering based on Slot
982impl PartialEq for VersionedIdl {
983    fn eq(&self, other: &Self) -> bool {
984        self.0 == other.0
985    }
986}
987
988impl Eq for VersionedIdl {}
989
990impl PartialOrd for VersionedIdl {
991    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
992        Some(self.cmp(other))
993    }
994}
995
996impl Ord for VersionedIdl {
997    fn cmp(&self, other: &Self) -> Ordering {
998        self.0.cmp(&other.0)
999    }
1000}
1001
1002#[derive(Debug, Clone)]
1003pub struct FifoMap<K, V> {
1004    // IndexMap is a map that preserves the insertion order of the keys. (It will be used for the FIFO eviction)
1005    map: IndexMap<K, V>,
1006}
1007
1008impl<K: std::hash::Hash + Eq, V> Default for FifoMap<K, V> {
1009    fn default() -> Self {
1010        Self::new(DEFAULT_PROFILING_MAP_CAPACITY)
1011    }
1012}
1013impl<K: std::hash::Hash + Eq, V> FifoMap<K, V> {
1014    pub fn new(capacity: usize) -> Self {
1015        Self {
1016            map: IndexMap::with_capacity(capacity),
1017        }
1018    }
1019
1020    pub fn capacity(&self) -> usize {
1021        self.map.capacity()
1022    }
1023
1024    pub fn len(&self) -> usize {
1025        self.map.len()
1026    }
1027
1028    pub fn clear(&mut self) {
1029        self.map.clear();
1030    }
1031
1032    pub fn is_empty(&self) -> bool {
1033        self.map.is_empty()
1034    }
1035
1036    /// Insert a key/value. If `K` is new and we're full, evict the oldest (FIFO)
1037    /// Returns a tuple of (old_value, evicted_key):
1038    /// - old_value: The previous value if this was an update to an existing key
1039    /// - evicted_key: The key that was evicted if the map was at capacity
1040    pub fn insert(&mut self, key: K, value: V) -> (Option<V>, Option<K>) {
1041        if self.map.contains_key(&key) {
1042            // Update doesn't change insertion order in IndexMap
1043            return (self.map.insert(key, value), None);
1044        }
1045        let evicted_key = if self.map.len() == self.map.capacity() {
1046            // Evict oldest (index 0). O(n) due shifting the rest of the map
1047            // 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.
1048            // This is a good compromise between performance and simplicity. And thinking about memory usage, this is probably the best way to go.
1049            self.map.shift_remove_index(0).map(|(k, _)| k)
1050        } else {
1051            None
1052        };
1053        self.map.insert(key, value);
1054        (None, evicted_key)
1055    }
1056
1057    pub fn get(&self, key: &K) -> Option<&V> {
1058        self.map.get(key)
1059    }
1060
1061    pub fn get_mut(&mut self, key: &K) -> Option<&mut V> {
1062        self.map.get_mut(key)
1063    }
1064
1065    pub fn contains_key(&self, key: &K) -> bool {
1066        self.map.contains_key(key)
1067    }
1068
1069    /// Removes a key from the map, returning the value if present.
1070    pub fn remove(&mut self, key: &K) -> Option<V> {
1071        self.map.shift_remove(key)
1072    }
1073
1074    // This is a wrapper around the IndexMap::iter() method, but it preserves the insertion order of the keys.
1075    // It's used to iterate over the profiling map in the order of the keys being inserted.
1076    pub fn iter(&self) -> impl ExactSizeIterator<Item = (&K, &V)> {
1077        self.map.iter()
1078    }
1079}
1080
1081#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1082#[serde(rename_all = "camelCase")]
1083pub struct AccountSnapshot {
1084    pub lamports: u64,
1085    pub owner: String,
1086    pub executable: bool,
1087    pub rent_epoch: u64,
1088    /// Base64 encoded data
1089    pub data: String,
1090    /// Parsed account data if available
1091    pub parsed_data: Option<ParsedAccount>,
1092}
1093
1094impl AccountSnapshot {
1095    pub fn new(
1096        lamports: u64,
1097        owner: String,
1098        executable: bool,
1099        rent_epoch: u64,
1100        data: String,
1101        parsed_data: Option<ParsedAccount>,
1102    ) -> Self {
1103        Self {
1104            lamports,
1105            owner,
1106            executable,
1107            rent_epoch,
1108            data,
1109            parsed_data,
1110        }
1111    }
1112}
1113
1114#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1115#[serde(rename_all = "camelCase")]
1116pub struct ExportSnapshotConfig {
1117    pub include_parsed_accounts: Option<bool>,
1118    pub filter: Option<ExportSnapshotFilter>,
1119    pub scope: ExportSnapshotScope,
1120}
1121
1122#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1123#[serde(rename_all = "camelCase")]
1124pub enum ExportSnapshotScope {
1125    #[default]
1126    Network,
1127    PreTransaction(String),
1128}
1129
1130#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1131#[serde(rename_all = "camelCase")]
1132pub struct ExportSnapshotFilter {
1133    pub include_program_accounts: Option<bool>,
1134    pub include_accounts: Option<Vec<String>>,
1135    pub exclude_accounts: Option<Vec<String>>,
1136}
1137
1138#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1139#[serde(rename_all = "camelCase")]
1140pub struct ResetAccountConfig {
1141    pub include_owned_accounts: Option<bool>,
1142}
1143
1144impl Default for ResetAccountConfig {
1145    fn default() -> Self {
1146        Self {
1147            include_owned_accounts: Some(false),
1148        }
1149    }
1150}
1151
1152#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1153#[serde(rename_all = "camelCase")]
1154pub struct StreamAccountConfig {
1155    pub include_owned_accounts: Option<bool>,
1156}
1157
1158impl Default for StreamAccountConfig {
1159    fn default() -> Self {
1160        Self {
1161            include_owned_accounts: Some(false),
1162        }
1163    }
1164}
1165
1166#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1167#[serde(rename_all = "camelCase")]
1168pub struct StreamAccountsEntry {
1169    pub pubkey: String,
1170    #[serde(default)]
1171    pub include_owned_accounts: Option<bool>,
1172}
1173
1174#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1175#[serde(rename_all = "camelCase")]
1176pub struct OfflineAccountConfig {
1177    pub include_owned_accounts: Option<bool>,
1178}
1179
1180impl Default for OfflineAccountConfig {
1181    fn default() -> Self {
1182        Self {
1183            include_owned_accounts: Some(false),
1184        }
1185    }
1186}
1187
1188#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1189#[serde(rename_all = "camelCase")]
1190pub struct StreamedAccountInfo {
1191    pub pubkey: String,
1192    pub include_owned_accounts: bool,
1193}
1194
1195#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1196#[serde(rename_all = "camelCase")]
1197pub struct GetSurfnetInfoResponse {
1198    runbook_executions: Vec<RunbookExecutionStatusReport>,
1199}
1200impl GetSurfnetInfoResponse {
1201    pub fn new(runbook_executions: Vec<RunbookExecutionStatusReport>) -> Self {
1202        Self { runbook_executions }
1203    }
1204}
1205
1206#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1207#[serde(rename_all = "camelCase")]
1208pub struct GetStreamedAccountsResponse {
1209    accounts: Vec<StreamedAccountInfo>,
1210}
1211impl GetStreamedAccountsResponse {
1212    pub fn from_iter<I>(streamed_accounts: I) -> Self
1213    where
1214        I: IntoIterator<Item = (String, bool)>,
1215    {
1216        let accounts = streamed_accounts
1217            .into_iter()
1218            .map(|(pubkey, include_owned_accounts)| StreamedAccountInfo {
1219                pubkey,
1220                include_owned_accounts,
1221            })
1222            .collect();
1223        Self { accounts }
1224    }
1225}
1226
1227#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1228#[serde(rename_all = "camelCase")]
1229pub struct RunbookExecutionStatusReport {
1230    pub started_at: u32,
1231    pub completed_at: Option<u32>,
1232    pub runbook_id: String,
1233    pub errors: Option<Vec<String>>,
1234}
1235impl RunbookExecutionStatusReport {
1236    pub fn new(runbook_id: String) -> Self {
1237        Self {
1238            started_at: Local::now().timestamp() as u32,
1239            completed_at: None,
1240            runbook_id,
1241            errors: None,
1242        }
1243    }
1244    pub fn mark_completed(&mut self, error: Option<Vec<String>>) {
1245        self.completed_at = Some(Local::now().timestamp() as u32);
1246        self.errors = error;
1247    }
1248}
1249/// WebSocket subscription counts
1250#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
1251pub struct WsSubscriptions {
1252    pub signatures: usize,
1253    pub accounts: usize,
1254    pub slots: usize,
1255    pub logs: usize,
1256}
1257
1258/// Surfpool node status information
1259#[derive(Debug, Clone, Serialize, Deserialize)]
1260pub struct SurfpoolStatus {
1261    pub slot: u64,
1262    pub epoch: u64,
1263    pub slot_index: u64,
1264    pub transactions_count: u64,
1265    pub transactions_processed: u64,
1266    pub uptime_ms: u64,
1267    pub ws_subscriptions: WsSubscriptions,
1268}
1269
1270#[cfg(feature = "prometheus")]
1271fn default_prometheus_addr() -> String {
1272    "0.0.0.0:9000".to_string()
1273}
1274
1275#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1276#[serde(rename_all = "camelCase")]
1277pub struct CheatcodeConfig {
1278    pub lockout: bool, // if true, allows disabling even the `surfnet_enableCheatcodes`/`surfnetdisableCheatcodes` methods
1279    pub filter: CheatcodeFilter,
1280}
1281
1282#[derive(Serialize, Deserialize, Default)]
1283pub struct CheatcodeControlConfig {
1284    pub lockout: Option<bool>,
1285}
1286
1287#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1288#[serde(untagged)]
1289pub enum CheatcodeFilter {
1290    All(String),
1291    List(Vec<String>), // disables cheatcodes in a named list
1292}
1293
1294impl CheatcodeConfig {
1295    pub fn new() -> Arc<Mutex<Self>> {
1296        Arc::new(Mutex::new(CheatcodeConfig {
1297            lockout: false,
1298            filter: CheatcodeFilter::List(vec![]),
1299        }))
1300    }
1301
1302    pub fn lockout(&mut self) {
1303        self.lockout = true;
1304    }
1305
1306    pub fn disable_all(&mut self, lockout: bool, available_cheatcodes: Vec<String>) {
1307        if lockout {
1308            self.lockout = true;
1309        }
1310        self.filter = Self::filter_all_list(lockout, available_cheatcodes);
1311    }
1312
1313    pub fn disable_cheatcode(&mut self, cheatcode: &String) -> Result<(), String> {
1314        if !self.lockout
1315            && (cheatcode.eq("surfnet_enableCheatcode") || cheatcode.eq("surfnet_disableCheatcode"))
1316        {
1317            return Err("Cannot disable surfnet_disableCheatcode or surfnet_enableCheatcode while lockout is false".to_string());
1318        }
1319
1320        if let CheatcodeFilter::List(list) = &mut self.filter {
1321            if !list.contains(cheatcode) {
1322                list.push(cheatcode.to_string());
1323                Ok(())
1324            } else {
1325                Err("Cheatcode already disabled".to_string())
1326            }
1327        } else {
1328            Err("All cheatcodes disabled".to_string())
1329        }
1330    }
1331    pub fn enable_cheatcode(&mut self, cheatcode: &str) -> Result<(), String> {
1332        if let CheatcodeFilter::List(list) = &mut self.filter {
1333            if let Some(pos) = list.iter().position(|c| c == cheatcode) {
1334                list.remove(pos);
1335                Ok(())
1336            } else {
1337                Err("Cheatcode isn't disabled".to_string())
1338            }
1339        } else {
1340            Err("All cheatcodes are disabled".to_string())
1341        }
1342    }
1343
1344    pub fn is_cheatcode_disabled(&self, cheatcode: &String) -> bool {
1345        match &self.filter {
1346            CheatcodeFilter::List(list) => list.contains(cheatcode),
1347            CheatcodeFilter::All(_) => true,
1348        }
1349    }
1350
1351    pub fn filter_all_list(lockout: bool, available_cheatcodes: Vec<String>) -> CheatcodeFilter {
1352        // when lockout == true, it's important to disable surfnet_disableCheatcode as well
1353        // since calling surfnet_disableCheatcode with lockout == false will override the current config, which is a bug
1354        if lockout {
1355            CheatcodeFilter::All("all".to_string())
1356        } else {
1357            // remove `surfnet_disableCheatcode` and `surfnet_enableCheatcode` from the list of available cheatcodes
1358            let filter = available_cheatcodes
1359                .into_iter()
1360                .filter(|c| c.ne("surfnet_disableCheatcode") && c.ne("surfnet_enableCheatcode"))
1361                .collect();
1362            CheatcodeFilter::List(filter)
1363        }
1364    }
1365}
1366
1367#[cfg(test)]
1368mod tests {
1369    use serde_json::json;
1370    use solana_account_decoder_client_types::{ParsedAccount, UiAccountData};
1371
1372    use super::*;
1373
1374    #[test]
1375    fn test_disable_cheatcode_with_lockout_allows_protected_methods() {
1376        // This test catches the bug where lockout was not propagated to
1377        // CheatcodeConfig before calling disable_cheatcode(), causing
1378        // "Cannot disable surfnet_disableCheatcode or surfnet_enableCheatcode
1379        // while lockout is false" even when the request included lockout: true.
1380        let config = CheatcodeConfig::new();
1381        let mut config = config.lock().unwrap();
1382
1383        // Simulate the RPC layer propagating lockout before processing the list
1384        config.lockout();
1385
1386        // These should succeed because lockout is set
1387        assert!(
1388            config
1389                .disable_cheatcode(&"surfnet_setAccount".to_string())
1390                .is_ok()
1391        );
1392        assert!(
1393            config
1394                .disable_cheatcode(&"surfnet_enableCheatcode".to_string())
1395                .is_ok()
1396        );
1397        assert!(
1398            config
1399                .disable_cheatcode(&"surfnet_disableCheatcode".to_string())
1400                .is_ok()
1401        );
1402    }
1403
1404    #[test]
1405    fn test_disable_cheatcode_without_lockout_rejects_protected_methods() {
1406        let config = CheatcodeConfig::new();
1407        let mut config = config.lock().unwrap();
1408
1409        // Without lockout, disabling protected methods should fail
1410        assert!(
1411            config
1412                .disable_cheatcode(&"surfnet_enableCheatcode".to_string())
1413                .is_err()
1414        );
1415        assert!(
1416            config
1417                .disable_cheatcode(&"surfnet_disableCheatcode".to_string())
1418                .is_err()
1419        );
1420
1421        // But regular cheatcodes should still work
1422        assert!(
1423            config
1424                .disable_cheatcode(&"surfnet_setAccount".to_string())
1425                .is_ok()
1426        );
1427    }
1428
1429    #[test]
1430    fn test_disable_all_with_lockout_persists_lockout_flag() {
1431        // This test catches the bug where disable_all() did not set
1432        // self.lockout = true, so subsequent operations would not see lockout.
1433        let config = CheatcodeConfig::new();
1434        let mut config = config.lock().unwrap();
1435
1436        let available = vec![
1437            "surfnet_setAccount".to_string(),
1438            "surfnet_enableCheatcode".to_string(),
1439            "surfnet_disableCheatcode".to_string(),
1440        ];
1441
1442        config.disable_all(true, available);
1443        assert!(config.lockout);
1444    }
1445
1446    #[test]
1447    fn test_disable_all_without_lockout_does_not_set_lockout() {
1448        let config = CheatcodeConfig::new();
1449        let mut config = config.lock().unwrap();
1450
1451        let available = vec![
1452            "surfnet_setAccount".to_string(),
1453            "surfnet_enableCheatcode".to_string(),
1454            "surfnet_disableCheatcode".to_string(),
1455        ];
1456
1457        config.disable_all(false, available);
1458        assert!(!config.lockout);
1459    }
1460
1461    #[test]
1462    fn print_ui_keyed_profile_result() {
1463        let pubkey = Pubkey::new_unique();
1464        let owner = Pubkey::new_unique();
1465        let readonly_account_state = UiAccount {
1466            lamports: 100,
1467            data: UiAccountData::Binary(
1468                "ABCDEFG".into(),
1469                solana_account_decoder_client_types::UiAccountEncoding::Base64,
1470            ),
1471            owner: owner.to_string(),
1472            executable: false,
1473            rent_epoch: 0,
1474            space: Some(100),
1475        };
1476
1477        let account_1 = UiAccount {
1478            lamports: 100,
1479            data: UiAccountData::Json(ParsedAccount {
1480                program: "custom-program".into(),
1481                parsed: json!({
1482                    "field1": "value1",
1483                    "field2": "value2"
1484                }),
1485                space: 50,
1486            }),
1487            owner: owner.to_string(),
1488            executable: false,
1489            rent_epoch: 0,
1490            space: Some(100),
1491        };
1492
1493        let account_2 = UiAccount {
1494            lamports: 100,
1495            data: UiAccountData::Json(ParsedAccount {
1496                program: "custom-program".into(),
1497                parsed: json!({
1498                    "field1": "updated-value1",
1499                    "field2": "updated-value2"
1500                }),
1501                space: 50,
1502            }),
1503            owner: owner.to_string(),
1504            executable: false,
1505            rent_epoch: 0,
1506            space: Some(100),
1507        };
1508        let profile_result = UiKeyedProfileResult {
1509            slot: 123,
1510            key: UuidOrSignature::Uuid(Uuid::new_v4()),
1511            instruction_profiles: Some(vec![
1512                UiProfileResult {
1513                    account_states: IndexMap::from_iter([
1514                        (
1515                            pubkey,
1516                            UiAccountProfileState::Writable(UiAccountChange::Create(
1517                                account_1.clone(),
1518                            )),
1519                        ),
1520                        (owner, UiAccountProfileState::Readonly),
1521                    ]),
1522                    compute_units_consumed: 100,
1523                    log_messages: Some(vec![
1524                        "Log message: Creating Account".to_string(),
1525                        "Log message: Account created".to_string(),
1526                    ]),
1527                    error_message: None,
1528                },
1529                UiProfileResult {
1530                    account_states: IndexMap::from_iter([
1531                        (
1532                            pubkey,
1533                            UiAccountProfileState::Writable(UiAccountChange::Update(
1534                                account_1,
1535                                account_2.clone(),
1536                            )),
1537                        ),
1538                        (owner, UiAccountProfileState::Readonly),
1539                    ]),
1540                    compute_units_consumed: 100,
1541                    log_messages: Some(vec![
1542                        "Log message: Updating Account".to_string(),
1543                        "Log message: Account updated".to_string(),
1544                    ]),
1545                    error_message: None,
1546                },
1547                UiProfileResult {
1548                    account_states: IndexMap::from_iter([
1549                        (
1550                            pubkey,
1551                            UiAccountProfileState::Writable(UiAccountChange::Delete(account_2)),
1552                        ),
1553                        (owner, UiAccountProfileState::Readonly),
1554                    ]),
1555                    compute_units_consumed: 100,
1556                    log_messages: Some(vec![
1557                        "Log message: Deleting Account".to_string(),
1558                        "Log message: Account deleted".to_string(),
1559                    ]),
1560                    error_message: None,
1561                },
1562            ]),
1563            transaction_profile: UiProfileResult {
1564                account_states: IndexMap::from_iter([
1565                    (
1566                        pubkey,
1567                        UiAccountProfileState::Writable(UiAccountChange::Unchanged(None)),
1568                    ),
1569                    (owner, UiAccountProfileState::Readonly),
1570                ]),
1571                compute_units_consumed: 300,
1572                log_messages: Some(vec![
1573                    "Log message: Creating Account".to_string(),
1574                    "Log message: Account created".to_string(),
1575                    "Log message: Updating Account".to_string(),
1576                    "Log message: Account updated".to_string(),
1577                    "Log message: Deleting Account".to_string(),
1578                    "Log message: Account deleted".to_string(),
1579                ]),
1580                error_message: None,
1581            },
1582            readonly_account_states: IndexMap::from_iter([(owner, readonly_account_state)]),
1583        };
1584        println!("{}", serde_json::to_string_pretty(&profile_result).unwrap());
1585    }
1586
1587    #[test]
1588    fn test_profiling_map_capacity() {
1589        let profiling_map = FifoMap::<Signature, KeyedProfileResult>::new(10);
1590        assert_eq!(profiling_map.capacity(), 10);
1591    }
1592
1593    #[test]
1594    fn test_profiling_map_len() {
1595        let profiling_map = FifoMap::<Signature, KeyedProfileResult>::new(10);
1596        assert!(profiling_map.len() == 0);
1597    }
1598
1599    #[test]
1600    fn test_profiling_map_is_empty() {
1601        let profiling_map = FifoMap::<Signature, KeyedProfileResult>::new(10);
1602        assert_eq!(profiling_map.is_empty(), true);
1603    }
1604
1605    #[test]
1606    fn test_profiling_map_insert() {
1607        let mut profiling_map = FifoMap::<Signature, KeyedProfileResult>::new(10);
1608        let key = Signature::default();
1609        let value = KeyedProfileResult::new(
1610            1,
1611            UuidOrSignature::Signature(key),
1612            None,
1613            ProfileResult::new(BTreeMap::new(), BTreeMap::new(), 0, None, None),
1614            HashMap::new(),
1615        );
1616        profiling_map.insert(key, value.clone());
1617        assert_eq!(profiling_map.len(), 1);
1618    }
1619
1620    #[test]
1621    fn test_profiling_map_get() {
1622        let mut profiling_map = FifoMap::<Signature, KeyedProfileResult>::new(10);
1623        let key = Signature::default();
1624        let value = KeyedProfileResult::new(
1625            1,
1626            UuidOrSignature::Signature(key),
1627            None,
1628            ProfileResult::new(BTreeMap::new(), BTreeMap::new(), 0, None, None),
1629            HashMap::new(),
1630        );
1631        profiling_map.insert(key, value.clone());
1632
1633        assert_eq!(profiling_map.get(&key), Some(&value));
1634    }
1635
1636    #[test]
1637    fn test_profiling_map_get_mut() {
1638        let mut profiling_map = FifoMap::<Signature, KeyedProfileResult>::new(10);
1639        let key = Signature::default();
1640        let mut value = KeyedProfileResult::new(
1641            1,
1642            UuidOrSignature::Signature(key),
1643            None,
1644            ProfileResult::new(BTreeMap::new(), BTreeMap::new(), 0, None, None),
1645            HashMap::new(),
1646        );
1647        profiling_map.insert(key, value.clone());
1648        assert_eq!(profiling_map.get_mut(&key), Some(&mut value));
1649    }
1650
1651    #[test]
1652    fn test_profiling_map_contains_key() {
1653        let mut profiling_map = FifoMap::<Signature, KeyedProfileResult>::new(10);
1654        let key = Signature::default();
1655        let value = KeyedProfileResult::new(
1656            1,
1657            UuidOrSignature::Signature(key),
1658            None,
1659            ProfileResult::new(BTreeMap::new(), BTreeMap::new(), 0, None, None),
1660            HashMap::new(),
1661        );
1662        profiling_map.insert(key, value.clone());
1663
1664        assert_eq!(profiling_map.contains_key(&key), true);
1665    }
1666
1667    #[test]
1668    fn test_profiling_map_iter() {
1669        let mut profiling_map = FifoMap::<Signature, KeyedProfileResult>::new(10);
1670        let key = Signature::default();
1671        let value = KeyedProfileResult::new(
1672            1,
1673            UuidOrSignature::Signature(key),
1674            None,
1675            ProfileResult::new(BTreeMap::new(), BTreeMap::new(), 0, None, None),
1676            HashMap::new(),
1677        );
1678        profiling_map.insert(key, value.clone());
1679
1680        assert_eq!(profiling_map.iter().count(), 1);
1681    }
1682
1683    #[test]
1684    fn test_profiling_map_evicts_oldest_on_overflow() {
1685        let mut profiling_map = FifoMap::<String, u32>::new(10);
1686        profiling_map.insert("a".to_string(), 1);
1687        profiling_map.insert("b".to_string(), 2);
1688        profiling_map.insert("c".to_string(), 3);
1689        profiling_map.insert("d".to_string(), 4);
1690        profiling_map.insert("e".to_string(), 5);
1691        profiling_map.insert("f".to_string(), 6);
1692        profiling_map.insert("g".to_string(), 7);
1693        profiling_map.insert("h".to_string(), 8);
1694        profiling_map.insert("i".to_string(), 9);
1695        profiling_map.insert("j".to_string(), 10);
1696
1697        println!("Profiling map: {:?}", profiling_map);
1698        println!("Profile Map capacity: {:?}", profiling_map.capacity());
1699        println!("Profile Map len: {:?}", profiling_map.len());
1700
1701        assert_eq!(profiling_map.len(), 10);
1702
1703        // Now insert one more, which should evict the oldest
1704        profiling_map.insert("k".to_string(), 11);
1705        assert_eq!(profiling_map.len(), 10);
1706        assert_eq!(profiling_map.get(&"a".to_string()), None);
1707        assert_eq!(profiling_map.get(&"k".to_string()), Some(&11));
1708    }
1709
1710    #[test]
1711    fn test_profiling_map_update_do_not_reorder() {
1712        let mut profiling_map = FifoMap::<&str, u32>::new(4);
1713        profiling_map.insert("a", 1);
1714        profiling_map.insert("b", 2);
1715        profiling_map.insert("c", 3);
1716        profiling_map.insert("d", 4);
1717
1718        //update b, should not reorder (order remains a:1,b:2,c:3,d:4)
1719        println!("Profiling map: {:?}", profiling_map);
1720        println!("Profile Map key b holds: {:?}", profiling_map.get(&"b"));
1721        profiling_map.insert("b", 4);
1722        println!("Profile Map key b holds: {:?}", profiling_map.get(&"b"));
1723
1724        //overflow with a new key, should evict the oldest (a)
1725        profiling_map.insert("e", 5);
1726        assert_eq!(profiling_map.len(), 4);
1727        assert_eq!(profiling_map.get(&"a"), None);
1728        assert_eq!(profiling_map.get(&"b"), Some(&4));
1729        assert_eq!(profiling_map.get(&"e"), Some(&5));
1730
1731        let get: Vec<_> = profiling_map.iter().map(|(k, v)| (*k, *v)).collect();
1732        println!("Profiling map: {:?}", get);
1733        assert_eq!(get, vec![("b", 4), ("c", 3), ("d", 4), ("e", 5)]);
1734    }
1735
1736    #[test]
1737    fn test_export_snapshot_scope_serialization() {
1738        // Test Network variant
1739        let network_config = ExportSnapshotConfig {
1740            include_parsed_accounts: None,
1741            filter: None,
1742            scope: ExportSnapshotScope::Network,
1743        };
1744        let network_json = serde_json::to_value(&network_config).unwrap();
1745        println!(
1746            "Network config: {}",
1747            serde_json::to_string_pretty(&network_json).unwrap()
1748        );
1749        assert_eq!(network_json["scope"], json!("network"));
1750
1751        // Test PreTransaction variant
1752        let pre_tx_config = ExportSnapshotConfig {
1753            include_parsed_accounts: None,
1754            filter: None,
1755            scope: ExportSnapshotScope::PreTransaction("5signature123".to_string()),
1756        };
1757        let pre_tx_json = serde_json::to_value(&pre_tx_config).unwrap();
1758        println!(
1759            "PreTransaction config: {}",
1760            serde_json::to_string_pretty(&pre_tx_json).unwrap()
1761        );
1762        assert_eq!(
1763            pre_tx_json["scope"],
1764            json!({"preTransaction": "5signature123"})
1765        );
1766
1767        // Test deserialization
1768        let deserialized_network: ExportSnapshotConfig =
1769            serde_json::from_value(network_json).unwrap();
1770        assert_eq!(deserialized_network.scope, ExportSnapshotScope::Network);
1771
1772        let deserialized_pre_tx: ExportSnapshotConfig =
1773            serde_json::from_value(pre_tx_json).unwrap();
1774        assert_eq!(
1775            deserialized_pre_tx.scope,
1776            ExportSnapshotScope::PreTransaction("5signature123".to_string())
1777        );
1778    }
1779
1780    #[test]
1781    fn test_sanitize_datasource_url_strips_path_and_query() {
1782        // API key in path should be stripped
1783        let config = SimnetConfig {
1784            remote_rpc_url: Some(
1785                "https://example.rpc-provider.com/v2/abc123def456ghi789".to_string(),
1786            ),
1787            ..Default::default()
1788        };
1789        let sanitized = config.get_sanitized_datasource_url().unwrap();
1790        assert_eq!(sanitized, "https://example.rpc-provider.com");
1791        assert!(!sanitized.contains("abc123"));
1792    }
1793
1794    #[test]
1795    fn test_sanitize_datasource_url_strips_query_params() {
1796        let config = SimnetConfig {
1797            remote_rpc_url: Some(
1798                "https://mainnet.helius-rpc.com/?api-key=secret-key-12345".to_string(),
1799            ),
1800            ..Default::default()
1801        };
1802        let sanitized = config.get_sanitized_datasource_url().unwrap();
1803        assert_eq!(sanitized, "https://mainnet.helius-rpc.com");
1804        assert!(!sanitized.contains("secret-key"));
1805    }
1806
1807    #[test]
1808    fn test_sanitize_datasource_url_public_rpc() {
1809        let config = SimnetConfig {
1810            remote_rpc_url: Some("https://api.mainnet-beta.solana.com".to_string()),
1811            ..Default::default()
1812        };
1813        let sanitized = config.get_sanitized_datasource_url().unwrap();
1814        assert_eq!(sanitized, "https://api.mainnet-beta.solana.com");
1815    }
1816
1817    #[test]
1818    fn test_sanitize_datasource_url_none() {
1819        let config = SimnetConfig {
1820            remote_rpc_url: None,
1821            ..Default::default()
1822        };
1823        assert!(config.get_sanitized_datasource_url().is_none());
1824    }
1825
1826    #[test]
1827    fn test_sanitize_datasource_url_invalid() {
1828        let config = SimnetConfig {
1829            remote_rpc_url: Some("not-a-valid-url".to_string()),
1830            ..Default::default()
1831        };
1832        assert!(config.get_sanitized_datasource_url().is_none());
1833    }
1834
1835    #[test]
1836    fn test_simnet_config_skip_blockhash_check_defaults_on_deserialize() {
1837        let mut config_json = serde_json::to_value(SimnetConfig::default()).unwrap();
1838        config_json
1839            .as_object_mut()
1840            .unwrap()
1841            .remove("skip_blockhash_check");
1842
1843        let config: SimnetConfig = serde_json::from_value(config_json).unwrap();
1844        assert!(!config.skip_blockhash_check);
1845    }
1846}