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