1use std::{collections::BTreeMap, fmt, path::PathBuf, str::FromStr};
2
3use blake3::Hash;
4use chrono::{DateTime, Local};
5use crossbeam_channel::{Receiver, Sender};
6use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Visitor};
8use serde_with::{BytesOrString, serde_as};
9use solana_account_decoder_client_types::UiAccount;
10use solana_clock::{Clock, Epoch};
11use solana_epoch_info::EpochInfo;
12use solana_message::inner_instruction::InnerInstructionsList;
13use solana_pubkey::Pubkey;
14use solana_signature::Signature;
15use solana_transaction::versioned::VersionedTransaction;
16use solana_transaction_context::TransactionReturnData;
17use solana_transaction_error::TransactionError;
18use txtx_addon_network_svm_types::subgraph::SubgraphRequest;
19use uuid::Uuid;
20
21pub const DEFAULT_RPC_URL: &str = "https://api.mainnet-beta.solana.com";
22pub const DEFAULT_RPC_PORT: u16 = 8899;
23pub const DEFAULT_WS_PORT: u16 = 8900;
24pub const DEFAULT_STUDIO_PORT: u16 = 8488;
25pub const CHANGE_TO_DEFAULT_STUDIO_PORT_ONCE_SUPERVISOR_MERGED: u16 = 18488;
26pub const DEFAULT_NETWORK_HOST: &str = "127.0.0.1";
27
28#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
29pub struct TransactionMetadata {
30 pub signature: Signature,
31 pub logs: Vec<String>,
32 pub inner_instructions: InnerInstructionsList,
33 pub compute_units_consumed: u64,
34 pub return_data: TransactionReturnData,
35}
36
37#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(rename_all = "camelCase")]
39pub enum TransactionConfirmationStatus {
40 Processed,
41 Confirmed,
42 Finalized,
43}
44
45#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
46pub enum BlockProductionMode {
47 #[default]
48 Clock,
49 Transaction,
50 Manual,
51}
52
53#[derive(Debug)]
54pub enum SubgraphEvent {
55 EndpointReady,
56 InfoLog(DateTime<Local>, String),
57 ErrorLog(DateTime<Local>, String),
58 WarnLog(DateTime<Local>, String),
59 DebugLog(DateTime<Local>, String),
60 Shutdown,
61}
62
63impl SubgraphEvent {
64 pub fn info<S>(msg: S) -> Self
65 where
66 S: Into<String>,
67 {
68 Self::InfoLog(Local::now(), msg.into())
69 }
70
71 pub fn warn<S>(msg: S) -> Self
72 where
73 S: Into<String>,
74 {
75 Self::WarnLog(Local::now(), msg.into())
76 }
77
78 pub fn error<S>(msg: S) -> Self
79 where
80 S: Into<String>,
81 {
82 Self::ErrorLog(Local::now(), msg.into())
83 }
84
85 pub fn debug<S>(msg: S) -> Self
86 where
87 S: Into<String>,
88 {
89 Self::DebugLog(Local::now(), msg.into())
90 }
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
95#[serde(rename_all = "camelCase")]
96pub struct ComputeUnitsEstimationResult {
97 pub success: bool,
98 pub compute_units_consumed: u64,
99 pub log_messages: Option<Vec<String>>,
100 pub error_message: Option<String>,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
105#[serde(rename_all = "camelCase")]
106pub struct ProfileResult {
107 pub compute_units: ComputeUnitsEstimationResult,
108 pub state: ProfileState,
109 pub slot: u64,
110 pub uuid: Option<Uuid>,
111}
112
113impl ProfileResult {
114 pub fn success(
115 compute_units_consumed: u64,
116 logs: Vec<String>,
117 pre_execution: BTreeMap<Pubkey, Option<UiAccount>>,
118 post_execution: BTreeMap<Pubkey, Option<UiAccount>>,
119 slot: u64,
120 uuid: Option<Uuid>,
121 ) -> Self {
122 Self {
123 compute_units: ComputeUnitsEstimationResult {
124 success: true,
125 compute_units_consumed,
126 log_messages: Some(logs),
127 error_message: None,
128 },
129 state: ProfileState::new(pre_execution, post_execution),
130 slot,
131 uuid,
132 }
133 }
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
137#[serde(rename_all = "camelCase")]
138
139pub struct ProfileState {
140 #[serde(with = "profile_state_map")]
141 pub pre_execution: BTreeMap<Pubkey, Option<UiAccount>>,
142 #[serde(with = "profile_state_map")]
143 pub post_execution: BTreeMap<Pubkey, Option<UiAccount>>,
144}
145
146impl ProfileState {
147 pub fn new(
148 pre_execution: BTreeMap<Pubkey, Option<UiAccount>>,
149 post_execution: BTreeMap<Pubkey, Option<UiAccount>>,
150 ) -> Self {
151 Self {
152 pre_execution,
153 post_execution,
154 }
155 }
156}
157
158pub mod profile_state_map {
159 use super::*;
160
161 pub fn serialize<S>(
162 map: &BTreeMap<Pubkey, Option<UiAccount>>,
163 serializer: S,
164 ) -> Result<S::Ok, S::Error>
165 where
166 S: Serializer,
167 {
168 let str_map: BTreeMap<String, &Option<UiAccount>> =
169 map.iter().map(|(k, v)| (k.to_string(), v)).collect();
170 str_map.serialize(serializer)
171 }
172
173 pub fn deserialize<'de, D>(
174 deserializer: D,
175 ) -> Result<BTreeMap<Pubkey, Option<UiAccount>>, D::Error>
176 where
177 D: Deserializer<'de>,
178 {
179 let str_map: BTreeMap<String, Option<UiAccount>> = BTreeMap::deserialize(deserializer)?;
180 str_map
181 .into_iter()
182 .map(|(k, v)| {
183 Pubkey::from_str(&k)
184 .map(|pk| (pk, v))
185 .map_err(serde::de::Error::custom)
186 })
187 .collect()
188 }
189}
190
191#[derive(Debug, Clone)]
192pub enum SubgraphCommand {
193 CreateCollection(Uuid, SubgraphRequest, Sender<String>),
194 ObserveCollection(Receiver<DataIndexingCommand>),
195 Shutdown,
196}
197
198#[derive(Debug)]
199pub enum SimnetEvent {
200 Ready,
201 Connected(String),
202 Aborted(String),
203 Shutdown,
204 ClockUpdate(Clock),
205 EpochInfoUpdate(EpochInfo),
206 BlockHashExpired,
207 InfoLog(DateTime<Local>, String),
208 ErrorLog(DateTime<Local>, String),
209 WarnLog(DateTime<Local>, String),
210 DebugLog(DateTime<Local>, String),
211 PluginLoaded(String),
212 TransactionReceived(DateTime<Local>, VersionedTransaction),
213 TransactionProcessed(
214 DateTime<Local>,
215 TransactionMetadata,
216 Option<TransactionError>,
217 ),
218 AccountUpdate(DateTime<Local>, Pubkey),
219 TaggedProfile {
220 result: ProfileResult,
221 tag: String,
222 timestamp: DateTime<Local>,
223 },
224 RunbookStarted(String),
225 RunbookCompleted(String),
226}
227
228impl SimnetEvent {
229 pub fn info<S>(msg: S) -> Self
230 where
231 S: Into<String>,
232 {
233 Self::InfoLog(Local::now(), msg.into())
234 }
235
236 pub fn warn<S>(msg: S) -> Self
237 where
238 S: Into<String>,
239 {
240 Self::WarnLog(Local::now(), msg.into())
241 }
242
243 pub fn error<S>(msg: S) -> Self
244 where
245 S: Into<String>,
246 {
247 Self::ErrorLog(Local::now(), msg.into())
248 }
249
250 pub fn debug<S>(msg: S) -> Self
251 where
252 S: Into<String>,
253 {
254 Self::DebugLog(Local::now(), msg.into())
255 }
256
257 pub fn transaction_processed(meta: TransactionMetadata, err: Option<TransactionError>) -> Self {
258 Self::TransactionProcessed(Local::now(), meta, err)
259 }
260
261 pub fn transaction_received(tx: VersionedTransaction) -> Self {
262 Self::TransactionReceived(Local::now(), tx)
263 }
264
265 pub fn account_update(pubkey: Pubkey) -> Self {
266 Self::AccountUpdate(Local::now(), pubkey)
267 }
268
269 pub fn tagged_profile(result: ProfileResult, tag: String) -> Self {
270 Self::TaggedProfile {
271 result,
272 tag,
273 timestamp: Local::now(),
274 }
275 }
276
277 pub fn account_update_msg(&self) -> String {
278 match self {
279 SimnetEvent::AccountUpdate(_, pubkey) => {
280 format!("Account {} updated.", pubkey)
281 }
282 _ => unreachable!("This function should only be called for AccountUpdate events"),
283 }
284 }
285
286 pub fn epoch_info_update_msg(&self) -> String {
287 match self {
288 SimnetEvent::EpochInfoUpdate(epoch_info) => {
289 format!(
290 "Datasource connection successful. Epoch {} / Slot index {} / Slot {}.",
291 epoch_info.epoch, epoch_info.slot_index, epoch_info.absolute_slot
292 )
293 }
294 _ => unreachable!("This function should only be called for EpochInfoUpdate events"),
295 }
296 }
297
298 pub fn plugin_loaded_msg(&self) -> String {
299 match self {
300 SimnetEvent::PluginLoaded(plugin_name) => {
301 format!("Plugin {} successfully loaded.", plugin_name)
302 }
303 _ => unreachable!("This function should only be called for PluginLoaded events"),
304 }
305 }
306
307 pub fn clock_update_msg(&self) -> String {
308 match self {
309 SimnetEvent::ClockUpdate(clock) => {
310 format!("Clock ticking (epoch {}, slot {})", clock.epoch, clock.slot)
311 }
312 _ => unreachable!("This function should only be called for ClockUpdate events"),
313 }
314 }
315}
316
317#[derive(Debug)]
318pub enum TransactionStatusEvent {
319 Success(TransactionConfirmationStatus),
320 SimulationFailure((TransactionError, TransactionMetadata)),
321 ExecutionFailure((TransactionError, TransactionMetadata)),
322 VerificationFailure(String),
323}
324
325#[derive(Debug)]
326pub enum SimnetCommand {
327 SlotForward(Option<Hash>),
328 SlotBackward(Option<Hash>),
329 UpdateClock(ClockCommand),
330 UpdateBlockProductionMode(BlockProductionMode),
331 TransactionReceived(
332 Option<Hash>,
333 VersionedTransaction,
334 Sender<TransactionStatusEvent>,
335 bool,
336 ),
337 Terminate(Option<Hash>),
338}
339
340#[derive(Debug)]
341pub enum ClockCommand {
342 Pause,
343 Resume,
344 Toggle,
345 UpdateSlotInterval(u64),
346}
347
348pub enum ClockEvent {
349 Tick,
350 ExpireBlockHash,
351}
352
353#[derive(Clone, Debug, Default, Serialize)]
354pub struct SanitizedConfig {
355 pub rpc_url: String,
356 pub ws_url: String,
357 pub rpc_datasource_url: String,
358 pub studio_url: String,
359 pub graphql_query_route_url: String,
360}
361
362#[derive(Clone, Debug, Default)]
363pub struct SurfpoolConfig {
364 pub simnets: Vec<SimnetConfig>,
365 pub rpc: RpcConfig,
366 pub subgraph: SubgraphConfig,
367 pub studio: StudioConfig,
368 pub plugin_config_path: Vec<PathBuf>,
369}
370
371#[derive(Clone, Debug)]
372pub struct SimnetConfig {
373 pub remote_rpc_url: String,
374 pub slot_time: u64,
375 pub block_production_mode: BlockProductionMode,
376 pub airdrop_addresses: Vec<Pubkey>,
377 pub airdrop_token_amount: u64,
378 pub expiry: Option<u64>,
379}
380
381impl Default for SimnetConfig {
382 fn default() -> Self {
383 Self {
384 remote_rpc_url: DEFAULT_RPC_URL.to_string(),
385 slot_time: 0,
386 block_production_mode: BlockProductionMode::Clock,
387 airdrop_addresses: vec![],
388 airdrop_token_amount: 0,
389 expiry: None,
390 }
391 }
392}
393
394impl SimnetConfig {
395 pub fn get_sanitized_datasource_url(&self) -> String {
396 self.remote_rpc_url
397 .split("?")
398 .map(|e| e.to_string())
399 .collect::<Vec<String>>()
400 .first()
401 .expect("datasource url invalid")
402 .to_string()
403 }
404}
405
406#[derive(Clone, Debug, Default)]
407pub struct SubgraphConfig {}
408
409#[derive(Clone, Debug)]
410pub struct RpcConfig {
411 pub bind_host: String,
412 pub bind_port: u16,
413 pub ws_port: u16,
414}
415
416impl RpcConfig {
417 pub fn get_rpc_base_url(&self) -> String {
418 format!("{}:{}", self.bind_host, self.bind_port)
419 }
420 pub fn get_ws_base_url(&self) -> String {
421 format!("{}:{}", self.bind_host, self.ws_port)
422 }
423}
424
425impl Default for RpcConfig {
426 fn default() -> Self {
427 Self {
428 bind_host: DEFAULT_NETWORK_HOST.to_string(),
429 bind_port: DEFAULT_RPC_PORT,
430 ws_port: DEFAULT_WS_PORT,
431 }
432 }
433}
434
435#[derive(Clone, Debug)]
436pub struct StudioConfig {
437 pub bind_host: String,
438 pub bind_port: u16,
439}
440
441impl StudioConfig {
442 pub fn get_studio_base_url(&self) -> String {
443 format!("{}:{}", self.bind_host, self.bind_port)
444 }
445}
446
447impl Default for StudioConfig {
448 fn default() -> Self {
449 Self {
450 bind_host: DEFAULT_NETWORK_HOST.to_string(),
451 bind_port: CHANGE_TO_DEFAULT_STUDIO_PORT_ONCE_SUPERVISOR_MERGED,
452 }
453 }
454}
455
456#[derive(Debug, Clone, Deserialize, Serialize)]
457pub struct SubgraphPluginConfig {
458 pub uuid: Uuid,
459 pub ipc_token: String,
460 pub subgraph_request: SubgraphRequest,
461}
462
463#[derive(Serialize, Deserialize, Clone, Debug)]
464pub struct SvmSimnetInitializationRequest {
465 pub domain: String,
466 pub block_production_mode: BlockProductionMode,
467 pub datasource_rpc_url: String,
468}
469
470#[derive(Serialize, Deserialize, Clone, Debug)]
471pub enum SvmSimnetCommand {
472 Init(SvmSimnetInitializationRequest),
473}
474
475#[derive(Serialize, Deserialize)]
476pub struct CreateNetworkRequest {
477 pub workspace_id: Uuid,
478 pub name: String,
479 pub description: Option<String>,
480 pub datasource_rpc_url: String,
481 pub block_production_mode: BlockProductionMode,
482}
483
484impl CreateNetworkRequest {
485 pub fn new(
486 workspace_id: Uuid,
487 name: String,
488 description: Option<String>,
489 datasource_rpc_url: String,
490 block_production_mode: BlockProductionMode,
491 ) -> Self {
492 Self {
493 workspace_id,
494 name,
495 description,
496 datasource_rpc_url,
497 block_production_mode,
498 }
499 }
500}
501
502#[derive(Serialize, Deserialize)]
503pub struct CreateNetworkResponse {
504 pub rpc_url: String,
505}
506
507#[derive(Serialize, Deserialize)]
508pub struct DeleteNetworkRequest {
509 pub workspace_id: Uuid,
510 pub network_id: Uuid,
511}
512
513impl DeleteNetworkRequest {
514 pub fn new(workspace_id: Uuid, network_id: Uuid) -> Self {
515 Self {
516 workspace_id,
517 network_id,
518 }
519 }
520}
521
522#[derive(Serialize, Deserialize)]
523pub struct DeleteNetworkResponse;
524
525#[serde_as]
526#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
527#[serde(rename_all = "camelCase")]
528pub struct AccountUpdate {
529 pub lamports: Option<u64>,
531 #[serde_as(as = "Option<BytesOrString>")]
533 pub data: Option<Vec<u8>>,
534 pub owner: Option<String>,
536 pub executable: Option<bool>,
538 pub rent_epoch: Option<Epoch>,
540}
541
542#[derive(Debug, Clone)]
543pub enum SetSomeAccount {
544 Account(String),
545 NoAccount,
546}
547
548impl<'de> Deserialize<'de> for SetSomeAccount {
549 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
550 where
551 D: Deserializer<'de>,
552 {
553 struct SetSomeAccountVisitor;
554
555 impl<'de> Visitor<'de> for SetSomeAccountVisitor {
556 type Value = SetSomeAccount;
557
558 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
559 formatter.write_str("a Pubkey String or the String 'null'")
560 }
561
562 fn visit_some<D_>(self, deserializer: D_) -> std::result::Result<Self::Value, D_::Error>
563 where
564 D_: Deserializer<'de>,
565 {
566 Deserialize::deserialize(deserializer).map(|v: String| match v.as_str() {
567 "null" => SetSomeAccount::NoAccount,
568 _ => SetSomeAccount::Account(v.to_string()),
569 })
570 }
571 }
572
573 deserializer.deserialize_option(SetSomeAccountVisitor)
574 }
575}
576
577impl Serialize for SetSomeAccount {
578 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
579 where
580 S: Serializer,
581 {
582 match self {
583 SetSomeAccount::Account(val) => serializer.serialize_str(val),
584 SetSomeAccount::NoAccount => serializer.serialize_str("null"),
585 }
586 }
587}
588
589#[serde_as]
590#[derive(Debug, Clone, Default, Serialize, Deserialize)]
591#[serde(rename_all = "camelCase")]
592pub struct TokenAccountUpdate {
593 pub amount: Option<u64>,
595 pub delegate: Option<SetSomeAccount>,
597 pub state: Option<String>,
599 pub delegated_amount: Option<u64>,
601 pub close_authority: Option<SetSomeAccount>,
603}
604
605#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
607pub struct SupplyUpdate {
608 pub total: Option<u64>,
609 pub circulating: Option<u64>,
610 pub non_circulating: Option<u64>,
611 pub non_circulating_accounts: Option<Vec<String>>,
612}
613
614#[derive(Clone, Debug, Serialize)]
615pub enum UuidOrSignature {
616 Uuid(Uuid),
617 Signature(Signature),
618}
619
620impl<'de> Deserialize<'de> for UuidOrSignature {
621 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
622 where
623 D: Deserializer<'de>,
624 {
625 let s = String::deserialize(deserializer)?;
626
627 if let Ok(uuid) = Uuid::parse_str(&s) {
628 return Ok(UuidOrSignature::Uuid(uuid));
629 }
630
631 if let Ok(signature) = s.parse::<Signature>() {
632 return Ok(UuidOrSignature::Signature(signature));
633 }
634
635 Err(serde::de::Error::custom(
636 "expected a Uuid or a valid Solana Signature",
637 ))
638 }
639}
640
641#[derive(Debug, Clone, Deserialize, Serialize)]
642pub enum DataIndexingCommand {
643 ProcessCollection(Uuid),
644 ProcessCollectionEntriesPack(Uuid, Vec<u8>),
645}