Skip to main content

surfpool_types/
types.rs

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