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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
978#[serde(rename_all = "camelCase")]
979pub struct JitoBundleStatus {
980    #[serde(rename = "bundle_id")]
981    pub bundle_id: String,
982    pub transactions: Vec<String>,
983    pub slot: u64,
984    pub confirmation_status: solana_transaction_status::TransactionConfirmationStatus,
985    pub err: std::result::Result<(), TransactionError>,
986}
987
988// Define a wrapper struct
989#[derive(Debug, Clone, Serialize, Deserialize)]
990pub struct VersionedIdl(pub Slot, pub Idl);
991
992// Implement ordering based on Slot
993impl PartialEq for VersionedIdl {
994    fn eq(&self, other: &Self) -> bool {
995        self.0 == other.0
996    }
997}
998
999impl Eq for VersionedIdl {}
1000
1001impl PartialOrd for VersionedIdl {
1002    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1003        Some(self.cmp(other))
1004    }
1005}
1006
1007impl Ord for VersionedIdl {
1008    fn cmp(&self, other: &Self) -> Ordering {
1009        self.0.cmp(&other.0)
1010    }
1011}
1012
1013#[derive(Debug, Clone)]
1014pub struct FifoMap<K, V> {
1015    // IndexMap is a map that preserves the insertion order of the keys. (It will be used for the FIFO eviction)
1016    map: IndexMap<K, V>,
1017}
1018
1019impl<K: std::hash::Hash + Eq, V> Default for FifoMap<K, V> {
1020    fn default() -> Self {
1021        Self::new(DEFAULT_PROFILING_MAP_CAPACITY)
1022    }
1023}
1024impl<K: std::hash::Hash + Eq, V> FifoMap<K, V> {
1025    pub fn new(capacity: usize) -> Self {
1026        Self {
1027            map: IndexMap::with_capacity(capacity),
1028        }
1029    }
1030
1031    pub fn capacity(&self) -> usize {
1032        self.map.capacity()
1033    }
1034
1035    pub fn len(&self) -> usize {
1036        self.map.len()
1037    }
1038
1039    pub fn clear(&mut self) {
1040        self.map.clear();
1041    }
1042
1043    pub fn is_empty(&self) -> bool {
1044        self.map.is_empty()
1045    }
1046
1047    /// Insert a key/value. If `K` is new and we're full, evict the oldest (FIFO)
1048    /// Returns a tuple of (old_value, evicted_key):
1049    /// - old_value: The previous value if this was an update to an existing key
1050    /// - evicted_key: The key that was evicted if the map was at capacity
1051    pub fn insert(&mut self, key: K, value: V) -> (Option<V>, Option<K>) {
1052        if self.map.contains_key(&key) {
1053            // Update doesn't change insertion order in IndexMap
1054            return (self.map.insert(key, value), None);
1055        }
1056        let evicted_key = if self.map.len() == self.map.capacity() {
1057            // Evict oldest (index 0). O(n) due shifting the rest of the map
1058            // 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.
1059            // This is a good compromise between performance and simplicity. And thinking about memory usage, this is probably the best way to go.
1060            self.map.shift_remove_index(0).map(|(k, _)| k)
1061        } else {
1062            None
1063        };
1064        self.map.insert(key, value);
1065        (None, evicted_key)
1066    }
1067
1068    pub fn get(&self, key: &K) -> Option<&V> {
1069        self.map.get(key)
1070    }
1071
1072    pub fn get_mut(&mut self, key: &K) -> Option<&mut V> {
1073        self.map.get_mut(key)
1074    }
1075
1076    pub fn contains_key(&self, key: &K) -> bool {
1077        self.map.contains_key(key)
1078    }
1079
1080    /// Removes a key from the map, returning the value if present.
1081    pub fn remove(&mut self, key: &K) -> Option<V> {
1082        self.map.shift_remove(key)
1083    }
1084
1085    // This is a wrapper around the IndexMap::iter() method, but it preserves the insertion order of the keys.
1086    // It's used to iterate over the profiling map in the order of the keys being inserted.
1087    pub fn iter(&self) -> impl ExactSizeIterator<Item = (&K, &V)> {
1088        self.map.iter()
1089    }
1090}
1091
1092#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1093#[serde(rename_all = "camelCase")]
1094pub struct AccountSnapshot {
1095    pub lamports: u64,
1096    pub owner: String,
1097    pub executable: bool,
1098    pub rent_epoch: u64,
1099    /// Base64 encoded data
1100    pub data: String,
1101    /// Parsed account data if available
1102    pub parsed_data: Option<ParsedAccount>,
1103}
1104
1105impl AccountSnapshot {
1106    pub fn new(
1107        lamports: u64,
1108        owner: String,
1109        executable: bool,
1110        rent_epoch: u64,
1111        data: String,
1112        parsed_data: Option<ParsedAccount>,
1113    ) -> Self {
1114        Self {
1115            lamports,
1116            owner,
1117            executable,
1118            rent_epoch,
1119            data,
1120            parsed_data,
1121        }
1122    }
1123}
1124
1125#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1126#[serde(rename_all = "camelCase")]
1127pub struct ExportSnapshotConfig {
1128    pub include_parsed_accounts: Option<bool>,
1129    pub filter: Option<ExportSnapshotFilter>,
1130    pub scope: ExportSnapshotScope,
1131}
1132
1133#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1134#[serde(rename_all = "camelCase")]
1135pub enum ExportSnapshotScope {
1136    #[default]
1137    Network,
1138    PreTransaction(String),
1139}
1140
1141#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1142#[serde(rename_all = "camelCase")]
1143pub struct ExportSnapshotFilter {
1144    pub include_program_accounts: Option<bool>,
1145    pub include_accounts: Option<Vec<String>>,
1146    pub exclude_accounts: Option<Vec<String>>,
1147}
1148
1149#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1150#[serde(rename_all = "camelCase")]
1151pub struct ResetAccountConfig {
1152    pub include_owned_accounts: Option<bool>,
1153}
1154
1155impl Default for ResetAccountConfig {
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 StreamAccountConfig {
1166    pub include_owned_accounts: Option<bool>,
1167}
1168
1169impl Default for StreamAccountConfig {
1170    fn default() -> Self {
1171        Self {
1172            include_owned_accounts: Some(false),
1173        }
1174    }
1175}
1176
1177#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1178#[serde(rename_all = "camelCase")]
1179pub struct StreamAccountsEntry {
1180    pub pubkey: String,
1181    #[serde(default)]
1182    pub include_owned_accounts: Option<bool>,
1183}
1184
1185#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1186#[serde(rename_all = "camelCase")]
1187pub struct OfflineAccountConfig {
1188    pub include_owned_accounts: Option<bool>,
1189}
1190
1191impl Default for OfflineAccountConfig {
1192    fn default() -> Self {
1193        Self {
1194            include_owned_accounts: Some(false),
1195        }
1196    }
1197}
1198
1199#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1200#[serde(rename_all = "camelCase")]
1201pub struct StreamedAccountInfo {
1202    pub pubkey: String,
1203    pub include_owned_accounts: bool,
1204}
1205
1206#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1207#[serde(rename_all = "camelCase")]
1208pub struct GetSurfnetInfoResponse {
1209    runbook_executions: Vec<RunbookExecutionStatusReport>,
1210}
1211impl GetSurfnetInfoResponse {
1212    pub fn new(runbook_executions: Vec<RunbookExecutionStatusReport>) -> Self {
1213        Self { runbook_executions }
1214    }
1215}
1216
1217#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1218#[serde(rename_all = "camelCase")]
1219pub struct GetStreamedAccountsResponse {
1220    accounts: Vec<StreamedAccountInfo>,
1221}
1222impl GetStreamedAccountsResponse {
1223    pub fn from_iter<I>(streamed_accounts: I) -> Self
1224    where
1225        I: IntoIterator<Item = (String, bool)>,
1226    {
1227        let accounts = streamed_accounts
1228            .into_iter()
1229            .map(|(pubkey, include_owned_accounts)| StreamedAccountInfo {
1230                pubkey,
1231                include_owned_accounts,
1232            })
1233            .collect();
1234        Self { accounts }
1235    }
1236}
1237
1238#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1239#[serde(rename_all = "camelCase")]
1240pub struct RunbookExecutionStatusReport {
1241    pub started_at: u32,
1242    pub completed_at: Option<u32>,
1243    pub runbook_id: String,
1244    pub errors: Option<Vec<String>>,
1245}
1246impl RunbookExecutionStatusReport {
1247    pub fn new(runbook_id: String) -> Self {
1248        Self {
1249            started_at: Local::now().timestamp() as u32,
1250            completed_at: None,
1251            runbook_id,
1252            errors: None,
1253        }
1254    }
1255    pub fn mark_completed(&mut self, error: Option<Vec<String>>) {
1256        self.completed_at = Some(Local::now().timestamp() as u32);
1257        self.errors = error;
1258    }
1259}
1260/// WebSocket subscription counts
1261#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
1262pub struct WsSubscriptions {
1263    pub signatures: usize,
1264    pub accounts: usize,
1265    pub slots: usize,
1266    pub logs: usize,
1267}
1268
1269/// Surfpool node status information
1270#[derive(Debug, Clone, Serialize, Deserialize)]
1271pub struct SurfpoolStatus {
1272    pub slot: u64,
1273    pub epoch: u64,
1274    pub slot_index: u64,
1275    pub transactions_count: u64,
1276    pub transactions_processed: u64,
1277    pub uptime_ms: u64,
1278    pub ws_subscriptions: WsSubscriptions,
1279}
1280
1281#[cfg(feature = "prometheus")]
1282fn default_prometheus_addr() -> String {
1283    "0.0.0.0:9000".to_string()
1284}
1285
1286#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1287#[serde(rename_all = "camelCase")]
1288pub struct CheatcodeConfig {
1289    pub lockout: bool, // if true, allows disabling even the `surfnet_enableCheatcodes`/`surfnetdisableCheatcodes` methods
1290    pub filter: CheatcodeFilter,
1291}
1292
1293#[derive(Serialize, Deserialize, Default)]
1294pub struct CheatcodeControlConfig {
1295    pub lockout: Option<bool>,
1296}
1297
1298#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1299#[serde(untagged)]
1300pub enum CheatcodeFilter {
1301    All(String),
1302    List(Vec<String>), // disables cheatcodes in a named list
1303}
1304
1305impl CheatcodeConfig {
1306    pub fn new() -> Arc<Mutex<Self>> {
1307        Arc::new(Mutex::new(CheatcodeConfig {
1308            lockout: false,
1309            filter: CheatcodeFilter::List(vec![]),
1310        }))
1311    }
1312
1313    pub fn lockout(&mut self) {
1314        self.lockout = true;
1315    }
1316
1317    pub fn disable_all(&mut self, lockout: bool, available_cheatcodes: Vec<String>) {
1318        if lockout {
1319            self.lockout = true;
1320        }
1321        self.filter = Self::filter_all_list(lockout, available_cheatcodes);
1322    }
1323
1324    pub fn disable_cheatcode(&mut self, cheatcode: &String) -> Result<(), String> {
1325        if !self.lockout
1326            && (cheatcode.eq("surfnet_enableCheatcode") || cheatcode.eq("surfnet_disableCheatcode"))
1327        {
1328            return Err("Cannot disable surfnet_disableCheatcode or surfnet_enableCheatcode while lockout is false".to_string());
1329        }
1330
1331        if let CheatcodeFilter::List(list) = &mut self.filter {
1332            if !list.contains(cheatcode) {
1333                list.push(cheatcode.to_string());
1334                Ok(())
1335            } else {
1336                Err("Cheatcode already disabled".to_string())
1337            }
1338        } else {
1339            Err("All cheatcodes disabled".to_string())
1340        }
1341    }
1342    pub fn enable_cheatcode(&mut self, cheatcode: &str) -> Result<(), String> {
1343        if let CheatcodeFilter::List(list) = &mut self.filter {
1344            if let Some(pos) = list.iter().position(|c| c == cheatcode) {
1345                list.remove(pos);
1346                Ok(())
1347            } else {
1348                Err("Cheatcode isn't disabled".to_string())
1349            }
1350        } else {
1351            Err("All cheatcodes are disabled".to_string())
1352        }
1353    }
1354
1355    pub fn is_cheatcode_disabled(&self, cheatcode: &String) -> bool {
1356        match &self.filter {
1357            CheatcodeFilter::List(list) => list.contains(cheatcode),
1358            CheatcodeFilter::All(_) => true,
1359        }
1360    }
1361
1362    pub fn filter_all_list(lockout: bool, available_cheatcodes: Vec<String>) -> CheatcodeFilter {
1363        // when lockout == true, it's important to disable surfnet_disableCheatcode as well
1364        // since calling surfnet_disableCheatcode with lockout == false will override the current config, which is a bug
1365        if lockout {
1366            CheatcodeFilter::All("all".to_string())
1367        } else {
1368            // remove `surfnet_disableCheatcode` and `surfnet_enableCheatcode` from the list of available cheatcodes
1369            let filter = available_cheatcodes
1370                .into_iter()
1371                .filter(|c| c.ne("surfnet_disableCheatcode") && c.ne("surfnet_enableCheatcode"))
1372                .collect();
1373            CheatcodeFilter::List(filter)
1374        }
1375    }
1376}
1377
1378#[cfg(test)]
1379mod tests {
1380    use serde_json::json;
1381    use solana_account_decoder_client_types::{ParsedAccount, UiAccountData};
1382
1383    use super::*;
1384
1385    #[test]
1386    fn test_disable_cheatcode_with_lockout_allows_protected_methods() {
1387        // This test catches the bug where lockout was not propagated to
1388        // CheatcodeConfig before calling disable_cheatcode(), causing
1389        // "Cannot disable surfnet_disableCheatcode or surfnet_enableCheatcode
1390        // while lockout is false" even when the request included lockout: true.
1391        let config = CheatcodeConfig::new();
1392        let mut config = config.lock().unwrap();
1393
1394        // Simulate the RPC layer propagating lockout before processing the list
1395        config.lockout();
1396
1397        // These should succeed because lockout is set
1398        assert!(
1399            config
1400                .disable_cheatcode(&"surfnet_setAccount".to_string())
1401                .is_ok()
1402        );
1403        assert!(
1404            config
1405                .disable_cheatcode(&"surfnet_enableCheatcode".to_string())
1406                .is_ok()
1407        );
1408        assert!(
1409            config
1410                .disable_cheatcode(&"surfnet_disableCheatcode".to_string())
1411                .is_ok()
1412        );
1413    }
1414
1415    #[test]
1416    fn test_disable_cheatcode_without_lockout_rejects_protected_methods() {
1417        let config = CheatcodeConfig::new();
1418        let mut config = config.lock().unwrap();
1419
1420        // Without lockout, disabling protected methods should fail
1421        assert!(
1422            config
1423                .disable_cheatcode(&"surfnet_enableCheatcode".to_string())
1424                .is_err()
1425        );
1426        assert!(
1427            config
1428                .disable_cheatcode(&"surfnet_disableCheatcode".to_string())
1429                .is_err()
1430        );
1431
1432        // But regular cheatcodes should still work
1433        assert!(
1434            config
1435                .disable_cheatcode(&"surfnet_setAccount".to_string())
1436                .is_ok()
1437        );
1438    }
1439
1440    #[test]
1441    fn test_disable_all_with_lockout_persists_lockout_flag() {
1442        // This test catches the bug where disable_all() did not set
1443        // self.lockout = true, so subsequent operations would not see lockout.
1444        let config = CheatcodeConfig::new();
1445        let mut config = config.lock().unwrap();
1446
1447        let available = vec![
1448            "surfnet_setAccount".to_string(),
1449            "surfnet_enableCheatcode".to_string(),
1450            "surfnet_disableCheatcode".to_string(),
1451        ];
1452
1453        config.disable_all(true, available);
1454        assert!(config.lockout);
1455    }
1456
1457    #[test]
1458    fn test_disable_all_without_lockout_does_not_set_lockout() {
1459        let config = CheatcodeConfig::new();
1460        let mut config = config.lock().unwrap();
1461
1462        let available = vec![
1463            "surfnet_setAccount".to_string(),
1464            "surfnet_enableCheatcode".to_string(),
1465            "surfnet_disableCheatcode".to_string(),
1466        ];
1467
1468        config.disable_all(false, available);
1469        assert!(!config.lockout);
1470    }
1471
1472    #[test]
1473    fn print_ui_keyed_profile_result() {
1474        let pubkey = Pubkey::new_unique();
1475        let owner = Pubkey::new_unique();
1476        let readonly_account_state = UiAccount {
1477            lamports: 100,
1478            data: UiAccountData::Binary(
1479                "ABCDEFG".into(),
1480                solana_account_decoder_client_types::UiAccountEncoding::Base64,
1481            ),
1482            owner: owner.to_string(),
1483            executable: false,
1484            rent_epoch: 0,
1485            space: Some(100),
1486        };
1487
1488        let account_1 = UiAccount {
1489            lamports: 100,
1490            data: UiAccountData::Json(ParsedAccount {
1491                program: "custom-program".into(),
1492                parsed: json!({
1493                    "field1": "value1",
1494                    "field2": "value2"
1495                }),
1496                space: 50,
1497            }),
1498            owner: owner.to_string(),
1499            executable: false,
1500            rent_epoch: 0,
1501            space: Some(100),
1502        };
1503
1504        let account_2 = UiAccount {
1505            lamports: 100,
1506            data: UiAccountData::Json(ParsedAccount {
1507                program: "custom-program".into(),
1508                parsed: json!({
1509                    "field1": "updated-value1",
1510                    "field2": "updated-value2"
1511                }),
1512                space: 50,
1513            }),
1514            owner: owner.to_string(),
1515            executable: false,
1516            rent_epoch: 0,
1517            space: Some(100),
1518        };
1519        let profile_result = UiKeyedProfileResult {
1520            slot: 123,
1521            key: UuidOrSignature::Uuid(Uuid::new_v4()),
1522            instruction_profiles: Some(vec![
1523                UiProfileResult {
1524                    account_states: IndexMap::from_iter([
1525                        (
1526                            pubkey,
1527                            UiAccountProfileState::Writable(UiAccountChange::Create(
1528                                account_1.clone(),
1529                            )),
1530                        ),
1531                        (owner, UiAccountProfileState::Readonly),
1532                    ]),
1533                    compute_units_consumed: 100,
1534                    log_messages: Some(vec![
1535                        "Log message: Creating Account".to_string(),
1536                        "Log message: Account created".to_string(),
1537                    ]),
1538                    error_message: None,
1539                },
1540                UiProfileResult {
1541                    account_states: IndexMap::from_iter([
1542                        (
1543                            pubkey,
1544                            UiAccountProfileState::Writable(UiAccountChange::Update(
1545                                account_1,
1546                                account_2.clone(),
1547                            )),
1548                        ),
1549                        (owner, UiAccountProfileState::Readonly),
1550                    ]),
1551                    compute_units_consumed: 100,
1552                    log_messages: Some(vec![
1553                        "Log message: Updating Account".to_string(),
1554                        "Log message: Account updated".to_string(),
1555                    ]),
1556                    error_message: None,
1557                },
1558                UiProfileResult {
1559                    account_states: IndexMap::from_iter([
1560                        (
1561                            pubkey,
1562                            UiAccountProfileState::Writable(UiAccountChange::Delete(account_2)),
1563                        ),
1564                        (owner, UiAccountProfileState::Readonly),
1565                    ]),
1566                    compute_units_consumed: 100,
1567                    log_messages: Some(vec![
1568                        "Log message: Deleting Account".to_string(),
1569                        "Log message: Account deleted".to_string(),
1570                    ]),
1571                    error_message: None,
1572                },
1573            ]),
1574            transaction_profile: UiProfileResult {
1575                account_states: IndexMap::from_iter([
1576                    (
1577                        pubkey,
1578                        UiAccountProfileState::Writable(UiAccountChange::Unchanged(None)),
1579                    ),
1580                    (owner, UiAccountProfileState::Readonly),
1581                ]),
1582                compute_units_consumed: 300,
1583                log_messages: Some(vec![
1584                    "Log message: Creating Account".to_string(),
1585                    "Log message: Account created".to_string(),
1586                    "Log message: Updating Account".to_string(),
1587                    "Log message: Account updated".to_string(),
1588                    "Log message: Deleting Account".to_string(),
1589                    "Log message: Account deleted".to_string(),
1590                ]),
1591                error_message: None,
1592            },
1593            readonly_account_states: IndexMap::from_iter([(owner, readonly_account_state)]),
1594        };
1595        println!("{}", serde_json::to_string_pretty(&profile_result).unwrap());
1596    }
1597
1598    #[test]
1599    fn test_profiling_map_capacity() {
1600        let profiling_map = FifoMap::<Signature, KeyedProfileResult>::new(10);
1601        assert_eq!(profiling_map.capacity(), 10);
1602    }
1603
1604    #[test]
1605    fn test_profiling_map_len() {
1606        let profiling_map = FifoMap::<Signature, KeyedProfileResult>::new(10);
1607        assert!(profiling_map.len() == 0);
1608    }
1609
1610    #[test]
1611    fn test_profiling_map_is_empty() {
1612        let profiling_map = FifoMap::<Signature, KeyedProfileResult>::new(10);
1613        assert_eq!(profiling_map.is_empty(), true);
1614    }
1615
1616    #[test]
1617    fn test_profiling_map_insert() {
1618        let mut profiling_map = FifoMap::<Signature, KeyedProfileResult>::new(10);
1619        let key = Signature::default();
1620        let value = KeyedProfileResult::new(
1621            1,
1622            UuidOrSignature::Signature(key),
1623            None,
1624            ProfileResult::new(BTreeMap::new(), BTreeMap::new(), 0, None, None),
1625            HashMap::new(),
1626        );
1627        profiling_map.insert(key, value.clone());
1628        assert_eq!(profiling_map.len(), 1);
1629    }
1630
1631    #[test]
1632    fn test_profiling_map_get() {
1633        let mut profiling_map = FifoMap::<Signature, KeyedProfileResult>::new(10);
1634        let key = Signature::default();
1635        let value = KeyedProfileResult::new(
1636            1,
1637            UuidOrSignature::Signature(key),
1638            None,
1639            ProfileResult::new(BTreeMap::new(), BTreeMap::new(), 0, None, None),
1640            HashMap::new(),
1641        );
1642        profiling_map.insert(key, value.clone());
1643
1644        assert_eq!(profiling_map.get(&key), Some(&value));
1645    }
1646
1647    #[test]
1648    fn test_profiling_map_get_mut() {
1649        let mut profiling_map = FifoMap::<Signature, KeyedProfileResult>::new(10);
1650        let key = Signature::default();
1651        let mut value = KeyedProfileResult::new(
1652            1,
1653            UuidOrSignature::Signature(key),
1654            None,
1655            ProfileResult::new(BTreeMap::new(), BTreeMap::new(), 0, None, None),
1656            HashMap::new(),
1657        );
1658        profiling_map.insert(key, value.clone());
1659        assert_eq!(profiling_map.get_mut(&key), Some(&mut value));
1660    }
1661
1662    #[test]
1663    fn test_profiling_map_contains_key() {
1664        let mut profiling_map = FifoMap::<Signature, KeyedProfileResult>::new(10);
1665        let key = Signature::default();
1666        let value = KeyedProfileResult::new(
1667            1,
1668            UuidOrSignature::Signature(key),
1669            None,
1670            ProfileResult::new(BTreeMap::new(), BTreeMap::new(), 0, None, None),
1671            HashMap::new(),
1672        );
1673        profiling_map.insert(key, value.clone());
1674
1675        assert_eq!(profiling_map.contains_key(&key), true);
1676    }
1677
1678    #[test]
1679    fn test_profiling_map_iter() {
1680        let mut profiling_map = FifoMap::<Signature, KeyedProfileResult>::new(10);
1681        let key = Signature::default();
1682        let value = KeyedProfileResult::new(
1683            1,
1684            UuidOrSignature::Signature(key),
1685            None,
1686            ProfileResult::new(BTreeMap::new(), BTreeMap::new(), 0, None, None),
1687            HashMap::new(),
1688        );
1689        profiling_map.insert(key, value.clone());
1690
1691        assert_eq!(profiling_map.iter().count(), 1);
1692    }
1693
1694    #[test]
1695    fn test_profiling_map_evicts_oldest_on_overflow() {
1696        let mut profiling_map = FifoMap::<String, u32>::new(10);
1697        profiling_map.insert("a".to_string(), 1);
1698        profiling_map.insert("b".to_string(), 2);
1699        profiling_map.insert("c".to_string(), 3);
1700        profiling_map.insert("d".to_string(), 4);
1701        profiling_map.insert("e".to_string(), 5);
1702        profiling_map.insert("f".to_string(), 6);
1703        profiling_map.insert("g".to_string(), 7);
1704        profiling_map.insert("h".to_string(), 8);
1705        profiling_map.insert("i".to_string(), 9);
1706        profiling_map.insert("j".to_string(), 10);
1707
1708        println!("Profiling map: {:?}", profiling_map);
1709        println!("Profile Map capacity: {:?}", profiling_map.capacity());
1710        println!("Profile Map len: {:?}", profiling_map.len());
1711
1712        assert_eq!(profiling_map.len(), 10);
1713
1714        // Now insert one more, which should evict the oldest
1715        profiling_map.insert("k".to_string(), 11);
1716        assert_eq!(profiling_map.len(), 10);
1717        assert_eq!(profiling_map.get(&"a".to_string()), None);
1718        assert_eq!(profiling_map.get(&"k".to_string()), Some(&11));
1719    }
1720
1721    #[test]
1722    fn test_profiling_map_update_do_not_reorder() {
1723        let mut profiling_map = FifoMap::<&str, u32>::new(4);
1724        profiling_map.insert("a", 1);
1725        profiling_map.insert("b", 2);
1726        profiling_map.insert("c", 3);
1727        profiling_map.insert("d", 4);
1728
1729        //update b, should not reorder (order remains a:1,b:2,c:3,d:4)
1730        println!("Profiling map: {:?}", profiling_map);
1731        println!("Profile Map key b holds: {:?}", profiling_map.get(&"b"));
1732        profiling_map.insert("b", 4);
1733        println!("Profile Map key b holds: {:?}", profiling_map.get(&"b"));
1734
1735        //overflow with a new key, should evict the oldest (a)
1736        profiling_map.insert("e", 5);
1737        assert_eq!(profiling_map.len(), 4);
1738        assert_eq!(profiling_map.get(&"a"), None);
1739        assert_eq!(profiling_map.get(&"b"), Some(&4));
1740        assert_eq!(profiling_map.get(&"e"), Some(&5));
1741
1742        let get: Vec<_> = profiling_map.iter().map(|(k, v)| (*k, *v)).collect();
1743        println!("Profiling map: {:?}", get);
1744        assert_eq!(get, vec![("b", 4), ("c", 3), ("d", 4), ("e", 5)]);
1745    }
1746
1747    #[test]
1748    fn test_export_snapshot_scope_serialization() {
1749        // Test Network variant
1750        let network_config = ExportSnapshotConfig {
1751            include_parsed_accounts: None,
1752            filter: None,
1753            scope: ExportSnapshotScope::Network,
1754        };
1755        let network_json = serde_json::to_value(&network_config).unwrap();
1756        println!(
1757            "Network config: {}",
1758            serde_json::to_string_pretty(&network_json).unwrap()
1759        );
1760        assert_eq!(network_json["scope"], json!("network"));
1761
1762        // Test PreTransaction variant
1763        let pre_tx_config = ExportSnapshotConfig {
1764            include_parsed_accounts: None,
1765            filter: None,
1766            scope: ExportSnapshotScope::PreTransaction("5signature123".to_string()),
1767        };
1768        let pre_tx_json = serde_json::to_value(&pre_tx_config).unwrap();
1769        println!(
1770            "PreTransaction config: {}",
1771            serde_json::to_string_pretty(&pre_tx_json).unwrap()
1772        );
1773        assert_eq!(
1774            pre_tx_json["scope"],
1775            json!({"preTransaction": "5signature123"})
1776        );
1777
1778        // Test deserialization
1779        let deserialized_network: ExportSnapshotConfig =
1780            serde_json::from_value(network_json).unwrap();
1781        assert_eq!(deserialized_network.scope, ExportSnapshotScope::Network);
1782
1783        let deserialized_pre_tx: ExportSnapshotConfig =
1784            serde_json::from_value(pre_tx_json).unwrap();
1785        assert_eq!(
1786            deserialized_pre_tx.scope,
1787            ExportSnapshotScope::PreTransaction("5signature123".to_string())
1788        );
1789    }
1790
1791    #[test]
1792    fn test_sanitize_datasource_url_strips_path_and_query() {
1793        // API key in path should be stripped
1794        let config = SimnetConfig {
1795            remote_rpc_url: Some(
1796                "https://example.rpc-provider.com/v2/abc123def456ghi789".to_string(),
1797            ),
1798            ..Default::default()
1799        };
1800        let sanitized = config.get_sanitized_datasource_url().unwrap();
1801        assert_eq!(sanitized, "https://example.rpc-provider.com");
1802        assert!(!sanitized.contains("abc123"));
1803    }
1804
1805    #[test]
1806    fn test_sanitize_datasource_url_strips_query_params() {
1807        let config = SimnetConfig {
1808            remote_rpc_url: Some(
1809                "https://mainnet.helius-rpc.com/?api-key=secret-key-12345".to_string(),
1810            ),
1811            ..Default::default()
1812        };
1813        let sanitized = config.get_sanitized_datasource_url().unwrap();
1814        assert_eq!(sanitized, "https://mainnet.helius-rpc.com");
1815        assert!(!sanitized.contains("secret-key"));
1816    }
1817
1818    #[test]
1819    fn test_sanitize_datasource_url_public_rpc() {
1820        let config = SimnetConfig {
1821            remote_rpc_url: Some("https://api.mainnet-beta.solana.com".to_string()),
1822            ..Default::default()
1823        };
1824        let sanitized = config.get_sanitized_datasource_url().unwrap();
1825        assert_eq!(sanitized, "https://api.mainnet-beta.solana.com");
1826    }
1827
1828    #[test]
1829    fn test_sanitize_datasource_url_none() {
1830        let config = SimnetConfig {
1831            remote_rpc_url: None,
1832            ..Default::default()
1833        };
1834        assert!(config.get_sanitized_datasource_url().is_none());
1835    }
1836
1837    #[test]
1838    fn test_sanitize_datasource_url_invalid() {
1839        let config = SimnetConfig {
1840            remote_rpc_url: Some("not-a-valid-url".to_string()),
1841            ..Default::default()
1842        };
1843        assert!(config.get_sanitized_datasource_url().is_none());
1844    }
1845
1846    #[test]
1847    fn test_simnet_config_skip_blockhash_check_defaults_on_deserialize() {
1848        let mut config_json = serde_json::to_value(SimnetConfig::default()).unwrap();
1849        config_json
1850            .as_object_mut()
1851            .unwrap()
1852            .remove("skip_blockhash_check");
1853
1854        let config: SimnetConfig = serde_json::from_value(config_json).unwrap();
1855        assert!(!config.skip_blockhash_check);
1856    }
1857}