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