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