Skip to main content

surfpool_core/runloops/
mod.rs

1#![allow(unused_imports, dead_code, unused_mut, unused_variables)]
2
3use std::{
4    collections::{HashMap, HashSet},
5    net::SocketAddr,
6    path::PathBuf,
7    sync::{Arc, RwLock},
8    thread::{JoinHandle, sleep},
9    time::{Duration, Instant},
10};
11
12use agave_geyser_plugin_interface::geyser_plugin_interface::{
13    GeyserPlugin, ReplicaBlockInfoV4, ReplicaBlockInfoVersions, ReplicaEntryInfoV2,
14    ReplicaEntryInfoVersions, ReplicaTransactionInfoV3, ReplicaTransactionInfoVersions, SlotStatus,
15};
16use chrono::{Local, Utc};
17use crossbeam::select;
18use crossbeam_channel::{Receiver, Sender, unbounded};
19use itertools::Itertools;
20use jsonrpc_core::MetaIoHandler;
21use jsonrpc_http_server::{DomainsValidation, ServerBuilder};
22use jsonrpc_pubsub::{PubSubHandler, Session};
23use jsonrpc_ws_server::{RequestContext, ServerBuilder as WsServerBuilder};
24use libloading::{Library, Symbol};
25use serde::Serialize;
26use solana_commitment_config::CommitmentConfig;
27#[cfg(feature = "geyser_plugin")]
28use solana_geyser_plugin_manager::geyser_plugin_manager::{
29    GeyserPluginManager, LoadedGeyserPlugin,
30};
31use solana_message::SimpleAddressLoader;
32use solana_transaction::sanitized::{MessageHash, SanitizedTransaction};
33use solana_transaction_status::RewardsAndNumPartitions;
34use surfpool_types::{
35    BlockProductionMode, ClockCommand, ClockEvent, DEFAULT_MAINNET_RPC_URL, DataIndexingCommand,
36    SimnetCommand, SimnetConfig, SimnetEvent, SurfpoolConfig,
37};
38type PluginConstructor = unsafe fn() -> *mut dyn GeyserPlugin;
39use txtx_addon_kit::helpers::fs::FileLocation;
40
41use crate::{
42    PluginInfo,
43    rpc::{
44        self, RunloopContext, SurfpoolMiddleware, SurfpoolWebsocketMeta,
45        SurfpoolWebsocketMiddleware, accounts_data::AccountsData, accounts_scan::AccountsScan,
46        admin::AdminRpc, bank_data::BankData, full::Full, jito::Jito, minimal::Minimal,
47        surfnet_cheatcodes::SurfnetCheatcodes, ws::Rpc,
48    },
49    surfnet::{GeyserEvent, PluginCommand, locker::SurfnetSvmLocker, remote::SurfnetRemoteClient},
50};
51
52const BLOCKHASH_SLOT_TTL: u64 = 75;
53
54/// A loaded geyser plugin with all metadata needed for lifecycle management.
55/// Field order matters: `plugin` must be declared before `_library` so it drops first,
56/// since the plugin's vtable points into the loaded dynamic library.
57pub(crate) struct ManagedPlugin {
58    pub(crate) uuid: crate::Uuid,
59    pub(crate) name: String,
60    pub(crate) plugin: Box<dyn GeyserPlugin>,
61    pub(crate) _library: Library,
62    pub(crate) config_path: String,
63}
64
65pub(crate) fn load_plugin_from_config(
66    config_file: &str,
67    is_reload: bool,
68) -> Result<ManagedPlugin, String> {
69    let plugin_manifest_location = FileLocation::from_path(PathBuf::from(config_file));
70    let config_content = plugin_manifest_location
71        .read_content_as_utf8()
72        .map_err(|e| format!("Unable to read plugin config '{}': {}", config_file, e))?;
73    let result: serde_json::Value = serde_json::from_str(&config_content)
74        .map_err(|e| format!("Unable to parse plugin manifest '{}': {}", config_file, e))?;
75
76    let plugin_dylib_path = result
77        .get("libpath")
78        .and_then(|p| p.as_str())
79        .ok_or_else(|| {
80            format!(
81                "Plugin config file should include a 'libpath' field: {}",
82                plugin_manifest_location
83            )
84        })?;
85
86    let mut plugin_dylib_location = plugin_manifest_location
87        .get_parent_location()
88        .expect("path invalid");
89    plugin_dylib_location
90        .append_path(plugin_dylib_path)
91        .expect("path invalid");
92
93    let (plugin, library) = unsafe {
94        let lib = Library::new(&plugin_dylib_location.to_string())
95            .map_err(|e| format!("Unable to load plugin {}: {}", plugin_dylib_location, e))?;
96        let constructor: Symbol<PluginConstructor> = lib
97            .get(b"_create_plugin")
98            .map_err(|e| format!("Plugin missing _create_plugin symbol: {}", e))?;
99        let plugin_raw = constructor();
100        (Box::from_raw(plugin_raw), lib)
101    };
102
103    let name = plugin.name().to_string();
104
105    let mut plugin = plugin;
106    plugin
107        .on_load(config_file, is_reload)
108        .map_err(|e| format!("Failed to call on_load for plugin '{}': {}", name, e))?;
109
110    let uuid = crate::Uuid::new_v4();
111
112    Ok(ManagedPlugin {
113        uuid,
114        name,
115        plugin,
116        _library: library,
117        config_path: config_file.to_string(),
118    })
119}
120
121pub(crate) fn unload_plugin_by_name(
122    name: &str,
123    managed_plugins: &mut Vec<ManagedPlugin>,
124) -> Result<(), String> {
125    let idx = managed_plugins
126        .iter()
127        .position(|p| p.name == name)
128        .ok_or_else(|| format!("Plugin '{}' not found", name))?;
129
130    let mut plugin = managed_plugins.remove(idx);
131    plugin.plugin.on_unload();
132    // plugin and _library are dropped here in correct order
133    Ok(())
134}
135
136/// Checks if a port is available for binding.
137fn check_port_availability(addr: SocketAddr, server_type: &str) -> Result<(), String> {
138    match std::net::TcpListener::bind(addr) {
139        Ok(_listener) => Ok(()),
140        Err(e) if e.kind() == std::io::ErrorKind::AddrInUse => {
141            let msg = format!(
142                "{} port {} is already in use. Try --port or --ws-port to use a different port.",
143                server_type,
144                addr.port()
145            );
146            eprintln!("Error: {}", msg);
147            Err(msg)
148        }
149        Err(e) => {
150            let msg = format!("Failed to bind {} server to {}: {}", server_type, addr, e);
151            eprintln!("Error: {}", msg);
152            Err(msg)
153        }
154    }
155}
156
157pub async fn start_local_surfnet_runloop(
158    svm_locker: SurfnetSvmLocker,
159    config: SurfpoolConfig,
160    simnet_commands_tx: Sender<SimnetCommand>,
161    simnet_commands_rx: Receiver<SimnetCommand>,
162    geyser_events_rx: Receiver<GeyserEvent>,
163) -> Result<(), Box<dyn std::error::Error>> {
164    let Some(simnet) = config.simnets.first() else {
165        return Ok(());
166    };
167    let block_production_mode = simnet.block_production_mode.clone();
168
169    let remote_rpc_client = match simnet.offline_mode {
170        true => None,
171        false => SurfnetRemoteClient::new_unsafe(
172            simnet
173                .remote_rpc_url
174                .as_ref()
175                .unwrap_or(&DEFAULT_MAINNET_RPC_URL.to_string()),
176        ),
177    };
178
179    svm_locker.initialize(&remote_rpc_client).await?;
180
181    if let Some(remote_client) = &remote_rpc_client {
182        let _ = svm_locker
183            .simnet_events_tx()
184            .send(SimnetEvent::Connected(remote_client.client.url()));
185    }
186    let _ = svm_locker
187        .simnet_events_tx()
188        .send(SimnetEvent::EpochInfoUpdate(svm_locker.get_epoch_info()));
189
190    svm_locker.airdrop_pubkeys(simnet.airdrop_token_amount, &simnet.airdrop_addresses);
191
192    // Load snapshot accounts if provided
193    if !simnet.snapshot.is_empty() {
194        match svm_locker
195            .load_snapshot(
196                &simnet.snapshot,
197                remote_rpc_client.as_ref(),
198                CommitmentConfig::confirmed(),
199            )
200            .await
201        {
202            Ok(loaded_count) => {
203                let _ = svm_locker.with_svm_reader(|svm| {
204                    svm.simnet_events_tx.send(SimnetEvent::info(format!(
205                        "Preloaded {} accounts from snapshot(s) into SVM",
206                        loaded_count
207                    )))
208                });
209            }
210            Err(e) => {
211                let _ = svm_locker.with_svm_reader(|svm| {
212                    svm.simnet_events_tx.send(SimnetEvent::warn(format!(
213                        "Error loading snapshot accounts: {}",
214                        e
215                    )))
216                });
217            }
218        }
219    }
220
221    let simnet_events_tx_cc = svm_locker.simnet_events_tx();
222
223    let (plugin_commands_tx, plugin_commands_rx) = unbounded::<PluginCommand>();
224
225    let (_rpc_handle, _ws_handle, shutdown_rpc_servers) = start_rpc_servers_runloop(
226        &config,
227        &simnet_commands_tx,
228        svm_locker.clone(),
229        &remote_rpc_client,
230        plugin_commands_tx,
231    )
232    .await?;
233
234    let simnet_config = simnet.clone();
235
236    match start_geyser_runloop(
237        config.plugin_config_path.clone(),
238        simnet_events_tx_cc.clone(),
239        geyser_events_rx,
240        plugin_commands_rx,
241    ) {
242        Ok(_) => {}
243        Err(e) => {
244            let _ =
245                simnet_events_tx_cc.send(SimnetEvent::error(format!("Geyser plugin failed: {e}")));
246        }
247    };
248
249    let (clock_event_rx, clock_command_tx) =
250        start_clock_runloop(simnet_config.slot_time, Some(simnet_events_tx_cc.clone()));
251
252    // Emit TransactionProcessed events for each stored transaction before Ready
253    let initial_transaction_count = svm_locker.with_svm_reader(|svm| {
254        let iter_result = svm.transactions.into_iter();
255        let mut count: u64 = 0;
256
257        if let Ok(iter) = iter_result {
258            let mut events = vec![];
259            for (_, status) in iter {
260                if let Some((tx_meta, _updated_accounts)) = status.as_processed() {
261                    let signature = tx_meta.transaction.signatures[0];
262                    let err = tx_meta.meta.status.clone().err();
263
264                    // Build TransactionMetadata from stored data
265                    let meta = surfpool_types::TransactionMetadata {
266                        signature,
267                        logs: tx_meta.meta.log_messages.clone().unwrap_or_default(),
268                        inner_instructions: tx_meta
269                            .meta
270                            .inner_instructions
271                            .clone()
272                            .unwrap_or_default()
273                            .into_iter()
274                            .map(|inner_ixs| {
275                                inner_ixs
276                                    .instructions
277                                    .into_iter()
278                                    .map(|ix| solana_message::inner_instruction::InnerInstruction {
279                                        instruction: ix.instruction,
280                                        stack_height: ix.stack_height.unwrap_or(1) as u8,
281                                    })
282                                    .collect()
283                            })
284                            .collect(),
285                        compute_units_consumed: tx_meta.meta.compute_units_consumed.unwrap_or(0),
286                        return_data: tx_meta.meta.return_data.clone().unwrap_or_default(),
287                        fee: tx_meta.meta.fee,
288                    };
289
290                    events.push((
291                        tx_meta.slot,
292                        SimnetEvent::TransactionProcessed(Local::now(), meta, err.clone()),
293                    ));
294
295                    count += 1;
296                }
297            }
298            for (_, event) in events
299                .into_iter()
300                .sorted_by(|(a_slot, _), (b_slot, _)| a_slot.cmp(b_slot))
301            {
302                let _ = svm.simnet_events_tx.send(event);
303            }
304        }
305
306        count
307    });
308    let _ = simnet_events_tx_cc.send(SimnetEvent::Ready(initial_transaction_count));
309
310    // Notify geyser plugins that startup is complete
311    let _ = svm_locker.with_svm_reader(|svm| svm.geyser_events_tx.send(GeyserEvent::EndOfStartup));
312
313    start_block_production_runloop(
314        clock_event_rx,
315        clock_command_tx,
316        simnet_commands_rx,
317        simnet_commands_tx.clone(),
318        svm_locker,
319        block_production_mode,
320        &remote_rpc_client,
321        simnet_config.expiry.map(|e| e * 1000),
322        &simnet_config,
323        Some(shutdown_rpc_servers),
324    )
325    .await
326}
327
328#[allow(clippy::too_many_arguments)]
329pub async fn start_block_production_runloop(
330    clock_event_rx: Receiver<ClockEvent>,
331    clock_command_tx: Sender<ClockCommand>,
332    simnet_commands_rx: Receiver<SimnetCommand>,
333    simnet_commands_tx: Sender<SimnetCommand>,
334    svm_locker: SurfnetSvmLocker,
335    mut block_production_mode: BlockProductionMode,
336    remote_rpc_client: &Option<SurfnetRemoteClient>,
337    expiry_duration_ms: Option<u64>,
338    simnet_config: &SimnetConfig,
339    mut shutdown_rpc_servers: Option<Box<dyn FnOnce() + Send>>,
340) -> Result<(), Box<dyn std::error::Error>> {
341    let remote_client_with_commitment = remote_rpc_client.as_ref().map(|c| {
342        (
343            c.clone(),
344            solana_commitment_config::CommitmentConfig::confirmed(),
345        )
346    });
347    let mut next_scheduled_expiry_check: Option<u64> =
348        expiry_duration_ms.map(|expiry_val| Utc::now().timestamp_millis() as u64 + expiry_val);
349    let global_skip_sig_verify = simnet_config.skip_signature_verification;
350    let ix_profiling_initially_enabled = simnet_config.instruction_profiling_enabled;
351    loop {
352        let mut do_produce_block = false;
353
354        select! {
355            recv(clock_event_rx) -> msg => if let Ok(event) = msg {
356                match event {
357                    ClockEvent::Tick => {
358                        if block_production_mode.eq(&BlockProductionMode::Clock) {
359                            do_produce_block = true;
360                        }
361
362                        if let Some(expiry_ms) = expiry_duration_ms {
363                            if let Some(scheduled_time_ref) = &mut next_scheduled_expiry_check {
364                                let now_ms = Utc::now().timestamp_millis() as u64;
365                                if now_ms >= *scheduled_time_ref {
366                                    let svm = svm_locker.0.read().await;
367                                    if svm.updated_at + expiry_ms < now_ms {
368                                        let _ = simnet_commands_tx.send(SimnetCommand::Terminate(None));
369                                    } else {
370                                        *scheduled_time_ref = svm.updated_at + expiry_ms;
371                                    }
372                                }
373                            }
374                        }
375                    }
376                    ClockEvent::ExpireBlockHash => {
377                        do_produce_block = true;
378                    }
379                }
380            },
381            recv(simnet_commands_rx) -> msg => if let Ok(event) = msg {
382                match event {
383                    SimnetCommand::SlotForward(_key) => {
384                        block_production_mode = BlockProductionMode::Manual;
385                        do_produce_block = true;
386                    }
387                    SimnetCommand::SlotBackward(_key) => {
388
389                    }
390                    SimnetCommand::CommandClock(_, update) => {
391                        if let ClockCommand::UpdateSlotInterval(updated_slot_time) = update {
392                            svm_locker.with_svm_writer(|svm_writer| {
393                                svm_writer.slot_time = updated_slot_time;
394                            });
395                        }
396
397                        // Handle PauseWithConfirmation specially
398                        if let ClockCommand::PauseWithConfirmation(response_tx) = update {
399                            // Get current slot and slot_time before pausing
400                            let (current_slot, slot_time) = svm_locker.with_svm_reader(|svm_reader| {
401                                (svm_reader.latest_epoch_info.absolute_slot, svm_reader.slot_time)
402                            });
403
404                            // Send Pause to clock runloop
405                            let _ = clock_command_tx.send(ClockCommand::Pause);
406
407                            // Give the clock time to process the pause command
408                            tokio::time::sleep(tokio::time::Duration::from_millis(slot_time / 2)).await;
409
410                            // Loop and check if the slot has stopped advancing
411                            let max_attempts = 10;
412                            let mut attempts = 0;
413                            loop {
414                                tokio::time::sleep(tokio::time::Duration::from_millis(slot_time)).await;
415
416                                let new_slot = svm_locker.with_svm_reader(|svm_reader| {
417                                    svm_reader.latest_epoch_info.absolute_slot
418                                });
419
420                                // If slot hasn't changed, clock has stopped
421                                if new_slot == current_slot || attempts >= max_attempts {
422                                    break;
423                                }
424
425                                attempts += 1;
426                            }
427
428                            // Read epoch info after clock has stopped
429                            let epoch_info = svm_locker.with_svm_reader(|svm_reader| {
430                                svm_reader.latest_epoch_info.clone()
431                            });
432                            // Send response
433                            let _ = response_tx.send(epoch_info);
434                        } else {
435                            let _ = clock_command_tx.send(update);
436                        }
437                        continue
438                    }
439                    SimnetCommand::UpdateInternalClock(_, clock) => {
440                        // Confirm the current block to materialize any scheduled overrides for this slot
441                        if let Err(e) = svm_locker.confirm_current_block(&remote_client_with_commitment).await {
442                            let _ = svm_locker.simnet_events_tx().send(SimnetEvent::error(format!(
443                                "Failed to confirm block after time travel: {}", e
444                            )));
445                        }
446
447                        svm_locker.with_svm_writer(|svm_writer| {
448                            svm_writer.inner.set_sysvar(&clock);
449                            svm_writer.updated_at = clock.unix_timestamp as u64 * 1_000;
450                            svm_writer.latest_epoch_info.absolute_slot = clock.slot;
451                            svm_writer.latest_epoch_info.epoch = clock.epoch;
452                            svm_writer.latest_epoch_info.slot_index = clock.slot;
453                            svm_writer.latest_epoch_info.epoch = clock.epoch;
454                            svm_writer.latest_epoch_info.absolute_slot = clock.slot + clock.epoch * svm_writer.latest_epoch_info.slots_in_epoch;
455                            let _ = svm_writer.simnet_events_tx.send(SimnetEvent::SystemClockUpdated(clock));
456                        });
457                    }
458                    SimnetCommand::UpdateInternalClockWithConfirmation(_, clock, response_tx) => {
459                        // Confirm the current block to materialize any scheduled overrides for this slot
460                        if let Err(e) = svm_locker.confirm_current_block(&remote_client_with_commitment).await {
461                            let _ = svm_locker.simnet_events_tx().send(SimnetEvent::error(format!(
462                                "Failed to confirm block after time travel: {}", e
463                            )));
464                        }
465
466                        let epoch_info = svm_locker.with_svm_writer(|svm_writer| {
467                            svm_writer.inner.set_sysvar(&clock);
468                            svm_writer.updated_at = clock.unix_timestamp as u64 * 1_000;
469                            svm_writer.latest_epoch_info.absolute_slot = clock.slot;
470                            svm_writer.latest_epoch_info.epoch = clock.epoch;
471                            svm_writer.latest_epoch_info.slot_index = clock.slot;
472                            svm_writer.latest_epoch_info.epoch = clock.epoch;
473                            svm_writer.latest_epoch_info.absolute_slot = clock.slot + clock.epoch * svm_writer.latest_epoch_info.slots_in_epoch;
474                            let _ = svm_writer.simnet_events_tx.send(SimnetEvent::SystemClockUpdated(clock));
475                            svm_writer.latest_epoch_info.clone()
476                        });
477
478                        // Send confirmation back
479                        let _ = response_tx.send(epoch_info);
480                    }
481                    SimnetCommand::UpdateBlockProductionMode(update) => {
482                        block_production_mode = update;
483                        continue
484                    }
485                    SimnetCommand::ProcessTransaction(_key, transaction, status_tx, skip_preflight, skip_sig_verify_override) => {
486                       let skip_sig_verify = skip_sig_verify_override.unwrap_or(global_skip_sig_verify);
487                       let sigverify = !skip_sig_verify;
488                       if let Err(e) = svm_locker.process_transaction(&remote_client_with_commitment, transaction, status_tx, skip_preflight, sigverify).await {
489                            let _ = svm_locker.simnet_events_tx().send(SimnetEvent::error(format!("Failed to process transaction: {}", e)));
490                       }
491                       if block_production_mode.eq(&BlockProductionMode::Transaction) {
492                           do_produce_block = true;
493                       }
494                    }
495                    SimnetCommand::Terminate(_) => {
496                        // Explicitly shutdown storage to trigger WAL checkpoint before exiting
497                        svm_locker.shutdown();
498                        // Close RPC servers on a separate thread to avoid dropping
499                        // Tokio runtimes from within an async context
500                        if let Some(shutdown_fn) = shutdown_rpc_servers.take() {
501                            std::thread::spawn(shutdown_fn);
502                        }
503                        break;
504                    }
505                    SimnetCommand::StartRunbookExecution(runbook_id) => {
506                        svm_locker.start_runbook_execution(runbook_id);
507                    }
508
509                    SimnetCommand::CompleteRunbookExecution(runbook_id, error) => {
510                        svm_locker.complete_runbook_execution(runbook_id, error, ix_profiling_initially_enabled);
511                    }
512                    SimnetCommand::FetchRemoteAccounts(pubkeys, remote_url) => {
513                        let remote_client = SurfnetRemoteClient::new_unsafe(&remote_url);
514                        if let Some(remote_client) = remote_client {
515                              match svm_locker.get_multiple_accounts_with_remote_fallback(&remote_client, &pubkeys, CommitmentConfig::confirmed()).await {
516                                 Ok(account_updates) => {
517                                     svm_locker.write_multiple_account_updates(&account_updates.inner);
518                                 }
519                                 Err(e) => {
520                                     svm_locker.simnet_events_tx().try_send(SimnetEvent::error(format!("Failed to fetch remote accounts {:?}: {}", pubkeys, e))).ok();
521                                 }
522                             };
523                        }
524                    }
525                    SimnetCommand::AirdropProcessed => {
526                       if block_production_mode.eq(&BlockProductionMode::Transaction) {
527                           do_produce_block = true;
528                       }
529                    }
530                }
531            },
532        }
533
534        {
535            if do_produce_block {
536                svm_locker
537                    .confirm_current_block(&remote_client_with_commitment)
538                    .await?;
539            }
540        }
541    }
542    Ok(())
543}
544
545pub fn start_clock_runloop(
546    mut slot_time: u64,
547    simnet_events_tx: Option<Sender<SimnetEvent>>,
548) -> (Receiver<ClockEvent>, Sender<ClockCommand>) {
549    let (clock_event_tx, clock_event_rx) = unbounded::<ClockEvent>();
550    let (clock_command_tx, clock_command_rx) = unbounded::<ClockCommand>();
551
552    let _handle = hiro_system_kit::thread_named("clock").spawn(move || {
553        let mut enabled = true;
554        let mut block_hash_timeout = Instant::now();
555
556        loop {
557            match clock_command_rx.try_recv() {
558                Ok(ClockCommand::Pause) => {
559                    enabled = false;
560                    if let Some(ref simnet_events_tx) = simnet_events_tx {
561                        let _ =
562                            simnet_events_tx.send(SimnetEvent::ClockUpdate(ClockCommand::Pause));
563                    }
564                }
565                Ok(ClockCommand::Resume) => {
566                    enabled = true;
567                    if let Some(ref simnet_events_tx) = simnet_events_tx {
568                        let _ =
569                            simnet_events_tx.send(SimnetEvent::ClockUpdate(ClockCommand::Resume));
570                    }
571                }
572                Ok(ClockCommand::Toggle) => {
573                    enabled = !enabled;
574                    if let Some(ref simnet_events_tx) = simnet_events_tx {
575                        let _ =
576                            simnet_events_tx.send(SimnetEvent::ClockUpdate(ClockCommand::Toggle));
577                    }
578                }
579                Ok(ClockCommand::UpdateSlotInterval(updated_slot_time)) => {
580                    slot_time = updated_slot_time;
581                }
582                Ok(ClockCommand::PauseWithConfirmation(_)) => {
583                    // This should be handled in the block production runloop, not here
584                    // If it reaches here, just treat it as a regular Pause
585                    enabled = false;
586                    if let Some(ref simnet_events_tx) = simnet_events_tx {
587                        let _ =
588                            simnet_events_tx.send(SimnetEvent::ClockUpdate(ClockCommand::Pause));
589                    }
590                }
591                Err(_e) => {}
592            }
593            sleep(Duration::from_millis(slot_time));
594            if enabled {
595                let _ = clock_event_tx.send(ClockEvent::Tick);
596                // Todo: the block expiration is not completely accurate.
597                if block_hash_timeout.elapsed()
598                    > Duration::from_millis(BLOCKHASH_SLOT_TTL * slot_time)
599                {
600                    let _ = clock_event_tx.send(ClockEvent::ExpireBlockHash);
601                    block_hash_timeout = Instant::now();
602                }
603            }
604        }
605    });
606
607    (clock_event_rx, clock_command_tx)
608}
609
610fn start_geyser_runloop(
611    plugin_config_paths: Vec<PathBuf>,
612    simnet_events_tx: Sender<SimnetEvent>,
613    geyser_events_rx: Receiver<GeyserEvent>,
614    plugin_commands_rx: Receiver<PluginCommand>,
615) -> Result<JoinHandle<Result<(), String>>, String> {
616    let handle: JoinHandle<Result<(), String>> = hiro_system_kit::thread_named("Geyser Plugins Handler").spawn(move || {
617        let mut indexing_enabled = false;
618
619        #[cfg(feature = "geyser_plugin")]
620        let mut plugin_manager = GeyserPluginManager::new();
621        #[cfg(not(feature = "geyser_plugin"))]
622        let mut plugin_manager = ();
623
624        let mut managed_plugins: Vec<ManagedPlugin> = vec![];
625
626        // helper to log errors that can't be propagated
627        let log_error = |msg:String|{
628            let _ = simnet_events_tx.send(SimnetEvent::error(msg));
629        };
630
631        let log_warn = |msg:String|{
632            let _ = simnet_events_tx.send(SimnetEvent::warn(msg));
633        };
634
635        let log_info = |msg:String|{
636            let _ = simnet_events_tx.send(SimnetEvent::info(msg));
637        };
638
639        // Load plugins from config paths at startup
640        for plugin_config_path in plugin_config_paths.iter() {
641            let config_file = plugin_config_path.to_string_lossy().to_string();
642            match load_plugin_from_config(&config_file, false) {
643                Ok(managed) => {
644                    indexing_enabled = true;
645                    log_info(format!("Loaded geyser plugin '{}'", managed.name));
646                    managed_plugins.push(managed);
647                }
648                Err(e) => {
649                    log_error(format!("Failed to load geyser plugin from '{}': {}", config_file, e));
650                }
651            }
652        }
653
654        #[cfg(feature = "geyser_plugin")]
655        for plugin_config_path in plugin_config_paths.into_iter() {
656            let plugin_manifest_location = FileLocation::from_path(plugin_config_path);
657            let config_file = plugin_manifest_location.read_content_as_utf8()?;
658            let result: serde_json::Value = match json5::from_str(&config_file) {
659                Ok(res) => res,
660                Err(e) => {
661                    let error = format!("Unable to read manifest: {}", e);
662                    let _ = simnet_events_tx.send(SimnetEvent::error(error.clone()));
663                    return Err(error)
664                }
665            };
666
667            let plugin_dylib_path = match result.get("libpath").map(|p| p.as_str()) {
668                Some(Some(name)) => name,
669                _ => {
670                    let error = format!("Plugin config file should include a 'libpath' field: {}", plugin_manifest_location);
671                    let _ = simnet_events_tx.send(SimnetEvent::error(error.clone()));
672                    return Err(error)
673                }
674            };
675
676            let mut plugin_dylib_location = plugin_manifest_location.get_parent_location().expect("path invalid");
677            plugin_dylib_location.append_path(&plugin_dylib_path).expect("path invalid");
678
679            let (plugin, lib) = unsafe {
680                let lib = match Library::new(&plugin_dylib_location.to_string()) {
681                    Ok(lib) => lib,
682                    Err(e) => {
683                        log_error(format!("Unable to load plugin {}: {}", plugin_dylib_location.to_string(), e.to_string()));
684                        continue;
685                    }
686                };
687                let constructor: Symbol<PluginConstructor> = lib
688                    .get(b"_create_plugin")
689                    .map_err(|e| format!("{}", e.to_string()))?;
690                let plugin_raw = constructor();
691                (Box::from_raw(plugin_raw), lib)
692            };
693            indexing_enabled = true;
694
695            let mut plugin = LoadedGeyserPlugin::new(lib, plugin, None);
696            if let Err(e) = plugin.on_load(&plugin_manifest_location.to_string(), false) {
697                let error = format!("Unable to load plugin:: {}", e.to_string());
698                let _ = simnet_events_tx.send(SimnetEvent::error(error.clone()));
699                return Err(error)
700            }
701
702            plugin_manager.plugins.push(plugin);
703        }
704
705        let err = loop {
706            use agave_geyser_plugin_interface::geyser_plugin_interface::{ReplicaAccountInfoV3, ReplicaAccountInfoVersions};
707
708            use crate::types::GeyserAccountUpdate;
709
710            select! {
711                recv(geyser_events_rx) -> msg => match msg {
712                    Err(e) => {
713                        break format!("Failed to read new transaction to send to Geyser plugin: {e}");
714                    },
715                    Ok(GeyserEvent::NotifyTransaction(transaction_with_status_meta, versioned_transaction)) => {
716
717                        if !indexing_enabled {
718                            continue;
719                        }
720
721                        let transaction = match versioned_transaction {
722                            Some(tx) => tx,
723                            None => {
724                                log_warn("Unable to index sanitized transaction".to_string());
725                                continue;
726                            }
727                        };
728
729                        let transaction_replica = ReplicaTransactionInfoV3 {
730                            signature: &transaction.signatures[0],
731                            is_vote: false,
732                            transaction: &transaction,
733                            transaction_status_meta: &transaction_with_status_meta.meta,
734                            index: 0,
735                            message_hash: &transaction.message.hash(),
736                        };
737
738                        for plugin in managed_plugins.iter().map(|p| &*p.plugin) {
739                            if let Err(e) = plugin.notify_transaction(ReplicaTransactionInfoVersions::V0_0_3(&transaction_replica), transaction_with_status_meta.slot) {
740                                log_error(format!("Failed to notify Geyser plugin of new transaction: {:?}", e))
741                            };
742                        }
743
744                        #[cfg(feature = "geyser_plugin")]
745                        for plugin in plugin_manager.plugins.iter() {
746                            if let Err(e) = plugin.notify_transaction(ReplicaTransactionInfoVersions::V0_0_3(&transaction_replica), transaction_with_status_meta.slot) {
747                                log_error(format!("Failed to notify Geyser plugin of new transaction: {:?}", e))
748                            };
749                        }
750                    }
751                    Ok(GeyserEvent::UpdateAccount(account_update)) => {
752                        let GeyserAccountUpdate {
753                            pubkey,
754                            account,
755                            slot,
756                            sanitized_transaction,
757                            write_version,
758                        } = account_update;
759
760                        let account_replica = ReplicaAccountInfoV3 {
761                            pubkey: pubkey.as_ref(),
762                            lamports: account.lamports,
763                            owner: account.owner.as_ref(),
764                            executable: account.executable,
765                            rent_epoch: account.rent_epoch,
766                            data: account.data.as_ref(),
767                            write_version,
768                            txn: sanitized_transaction.as_ref(),
769                        };
770
771                        for plugin in managed_plugins.iter().map(|p| &*p.plugin) {
772                            if let Err(e) = plugin.update_account(ReplicaAccountInfoVersions::V0_0_3(&account_replica), slot, false) {
773                                log_error(format!("Failed to update account in Geyser plugin: {:?}", e));
774                            }
775                        }
776
777                        #[cfg(feature = "geyser_plugin")]
778                        for plugin in plugin_manager.plugins.iter() {
779                            if let Err(e) = plugin.update_account(ReplicaAccountInfoVersions::V0_0_3(&account_replica), slot, false) {
780                                log_error(format!("Failed to update account in Geyser plugin: {:?}", e))
781                            }
782                        }
783                    }
784                    Ok(GeyserEvent::StartupAccountUpdate(account_update)) => {
785                        let GeyserAccountUpdate {
786                            pubkey,
787                            account,
788                            slot,
789                            sanitized_transaction,
790                            write_version,
791                        } = account_update;
792
793                        let account_replica = ReplicaAccountInfoV3 {
794                            pubkey: pubkey.as_ref(),
795                            lamports: account.lamports,
796                            owner: account.owner.as_ref(),
797                            executable: account.executable,
798                            rent_epoch: account.rent_epoch,
799                            data: account.data.as_ref(),
800                            write_version,
801                            txn: sanitized_transaction.as_ref(),
802                        };
803
804                        // Send startup account updates with is_startup=true
805                        for plugin in managed_plugins.iter().map(|p| &*p.plugin) {
806                            if let Err(e) = plugin.update_account(ReplicaAccountInfoVersions::V0_0_3(&account_replica), slot, true) {
807                                log_error(format!("Failed to send startup account update to Geyser plugin: {:?}", e));
808                            }
809                        }
810
811                        #[cfg(feature = "geyser_plugin")]
812                        for plugin in plugin_manager.plugins.iter() {
813                            if let Err(e) = plugin.update_account(ReplicaAccountInfoVersions::V0_0_3(&account_replica), slot, true) {
814                                log_error(format!("Failed to send startup account update to Geyser plugin: {:?}", e))
815                            }
816                        }
817                    }
818                    Ok(GeyserEvent::EndOfStartup) => {
819                        for plugin in managed_plugins.iter().map(|p| &*p.plugin) {
820                            if let Err(e) = plugin.notify_end_of_startup() {
821                                let _ = simnet_events_tx.send(SimnetEvent::error(format!("Failed to notify end of startup to Geyser plugin: {:?}", e)));
822                            }
823                        }
824
825                        #[cfg(feature = "geyser_plugin")]
826                        for plugin in plugin_manager.plugins.iter() {
827                            if let Err(e) = plugin.notify_end_of_startup() {
828                                let _ = simnet_events_tx.send(SimnetEvent::error(format!("Failed to notify end of startup to Geyser plugin: {:?}", e)));
829                            }
830                        }
831                    }
832                    Ok(GeyserEvent::UpdateSlotStatus { slot, parent, status }) => {
833                        let slot_status = match status {
834                            crate::surfnet::GeyserSlotStatus::Processed => SlotStatus::Processed,
835                            crate::surfnet::GeyserSlotStatus::Confirmed => SlotStatus::Confirmed,
836                            crate::surfnet::GeyserSlotStatus::Rooted => SlotStatus::Rooted,
837                        };
838
839                        for plugin in managed_plugins.iter().map(|p| &*p.plugin) {
840                            if let Err(e) = plugin.update_slot_status(slot, parent, &slot_status) {
841                                let _ = simnet_events_tx.send(SimnetEvent::error(format!("Failed to update slot status in Geyser plugin: {:?}", e)));
842                            }
843                        }
844
845                        #[cfg(feature = "geyser_plugin")]
846                        for plugin in plugin_manager.plugins.iter() {
847                            if let Err(e) = plugin.update_slot_status(slot, parent, &slot_status) {
848                                let _ = simnet_events_tx.send(SimnetEvent::error(format!("Failed to update slot status in Geyser plugin: {:?}", e)));
849                            }
850                        }
851                    }
852                    Ok(GeyserEvent::NotifyBlockMetadata(block_metadata)) => {
853                        let rewards = RewardsAndNumPartitions {
854                            rewards: vec![],
855                            num_partitions: None,
856                        };
857
858                        let block_info = ReplicaBlockInfoV4 {
859                            slot: block_metadata.slot,
860                            blockhash: &block_metadata.blockhash,
861                            rewards: &rewards,
862                            block_time: block_metadata.block_time,
863                            block_height: block_metadata.block_height,
864                            parent_slot: block_metadata.parent_slot,
865                            parent_blockhash: &block_metadata.parent_blockhash,
866                            executed_transaction_count: block_metadata.executed_transaction_count,
867                            entry_count: block_metadata.entry_count,
868                        };
869
870                        for plugin in managed_plugins.iter().map(|p| &*p.plugin) {
871                            if let Err(e) = plugin.notify_block_metadata(ReplicaBlockInfoVersions::V0_0_4(&block_info)) {
872                                let _ = simnet_events_tx.send(SimnetEvent::error(format!("Failed to notify block metadata to Geyser plugin: {:?}", e)));
873                            }
874                        }
875
876                        #[cfg(feature = "geyser_plugin")]
877                        for plugin in plugin_manager.plugins.iter() {
878                            if let Err(e) = plugin.notify_block_metadata(ReplicaBlockInfoVersions::V0_0_4(&block_info)) {
879                                let _ = simnet_events_tx.send(SimnetEvent::error(format!("Failed to notify block metadata to Geyser plugin: {:?}", e)));
880                            }
881                        }
882                    }
883                    Ok(GeyserEvent::NotifyEntry(entry_info)) => {
884                        let entry_replica = ReplicaEntryInfoV2 {
885                            slot: entry_info.slot,
886                            index: entry_info.index,
887                            num_hashes: entry_info.num_hashes,
888                            hash: &entry_info.hash,
889                            executed_transaction_count: entry_info.executed_transaction_count,
890                            starting_transaction_index: entry_info.starting_transaction_index,
891                        };
892
893                        for plugin in managed_plugins.iter().map(|p| &*p.plugin) {
894                            if let Err(e) = plugin.notify_entry(ReplicaEntryInfoVersions::V0_0_2(&entry_replica)) {
895                                let _ = simnet_events_tx.send(SimnetEvent::error(format!("Failed to notify entry to Geyser plugin: {:?}", e)));
896                            }
897                        }
898
899                        #[cfg(feature = "geyser_plugin")]
900                        for plugin in plugin_manager.plugins.iter() {
901                            if let Err(e) = plugin.notify_entry(ReplicaEntryInfoVersions::V0_0_2(&entry_replica)) {
902                                let _ = simnet_events_tx.send(SimnetEvent::error(format!("Failed to notify entry to Geyser plugin: {:?}", e)));
903                            }
904                        }
905                    }
906                },
907                recv(plugin_commands_rx) -> cmd => match cmd {
908                    Err(_) => {
909                        break "Plugin command channel closed".to_string();
910                    }
911                    Ok(PluginCommand::Load { config_file, response_tx }) => {
912                        let result = match load_plugin_from_config(&config_file, false) {
913                            Ok(managed) => {
914                                indexing_enabled = true;
915                                let info = PluginInfo {
916                                    plugin_name: managed.name.clone(),
917                                    uuid: managed.uuid.to_string(),
918                                };
919                                log_info(format!("Loaded geyser plugin '{}'", managed.name));
920                                managed_plugins.push(managed);
921                                Ok(info)
922                            }
923                            Err(e) => {
924                                log_error(format!("Failed to load plugin: {}", e));
925                                Err(e)
926                            }
927                        };
928                        let _ = response_tx.send(result);
929                    }
930                    Ok(PluginCommand::Unload { name, response_tx }) => {
931                        let result = unload_plugin_by_name(&name, &mut managed_plugins);
932                        if result.is_ok() {
933                            log_info(format!("Unloaded geyser plugin '{}'", name));
934                        }
935                        let _ = response_tx.send(result);
936                    }
937                    Ok(PluginCommand::Reload { name, config_file, response_tx }) => {
938                        let result = unload_plugin_by_name(&name, &mut managed_plugins)
939                            .and_then(|_| {
940                                let managed = load_plugin_from_config(&config_file, true)?;
941                                log_info(format!("Reloaded geyser plugin '{}'", managed.name));
942                                managed_plugins.push(managed);
943                                Ok(())
944                            });
945                        if let Err(ref e) = result {
946                            log_error(format!("Failed to reload plugin '{}': {}", name, e));
947                        }
948                        let _ = response_tx.send(result);
949                    }
950                    Ok(PluginCommand::List { response_tx }) => {
951                        let infos = managed_plugins.iter().map(|p| PluginInfo {
952                            plugin_name: p.name.clone(),
953                            uuid: p.uuid.to_string(),
954                        }).collect();
955                        let _ = response_tx.send(infos);
956                    }
957                }
958            }
959        };
960        Err(err)
961    }).map_err(|e| format!("Failed to spawn Geyser Plugins Handler thread: {:?}", e))?;
962    Ok(handle)
963}
964
965async fn start_rpc_servers_runloop(
966    config: &SurfpoolConfig,
967    simnet_commands_tx: &Sender<SimnetCommand>,
968    svm_locker: SurfnetSvmLocker,
969    remote_rpc_client: &Option<SurfnetRemoteClient>,
970    plugin_commands_tx: Sender<PluginCommand>,
971) -> Result<(JoinHandle<()>, JoinHandle<()>, Box<dyn FnOnce() + Send>), String> {
972    let rpc_addr: SocketAddr = config
973        .rpc
974        .get_rpc_base_url()
975        .parse()
976        .map_err(|e: std::net::AddrParseError| e.to_string())?;
977    let ws_addr: SocketAddr = config
978        .rpc
979        .get_ws_base_url()
980        .parse()
981        .map_err(|e: std::net::AddrParseError| e.to_string())?;
982
983    check_port_availability(rpc_addr, "RPC")?;
984    check_port_availability(ws_addr, "WebSocket")?;
985
986    let simnet_events_tx = svm_locker.simnet_events_tx();
987
988    let middleware = SurfpoolMiddleware::new(
989        svm_locker,
990        simnet_commands_tx,
991        &config.rpc,
992        remote_rpc_client,
993        plugin_commands_tx,
994    );
995
996    let (rpc_handle, rpc_close_handle) =
997        start_http_rpc_server_runloop(config, middleware.clone(), simnet_events_tx.clone()).await?;
998    let (ws_handle, ws_close_handle) =
999        start_ws_rpc_server_runloop(config, middleware, simnet_events_tx).await?;
1000
1001    let shutdown_rpc_servers: Box<dyn FnOnce() + Send> = Box::new(move || {
1002        rpc_close_handle.close();
1003        ws_close_handle.close();
1004    });
1005
1006    Ok((rpc_handle, ws_handle, shutdown_rpc_servers))
1007}
1008
1009async fn start_http_rpc_server_runloop(
1010    config: &SurfpoolConfig,
1011    middleware: SurfpoolMiddleware,
1012    simnet_events_tx: Sender<SimnetEvent>,
1013) -> Result<(JoinHandle<()>, jsonrpc_http_server::CloseHandle), String> {
1014    let server_bind: SocketAddr = config
1015        .rpc
1016        .get_rpc_base_url()
1017        .parse::<SocketAddr>()
1018        .map_err(|e| e.to_string())?;
1019
1020    let mut io = MetaIoHandler::with_middleware(middleware);
1021
1022    // Cheatcodes should be added first and are a special case. One of the cheatcode methods needs access
1023    // to the list of all cheatcode methods. The IoHandler allows us to iterate over them, so we're
1024    // initializing and storing that list here.
1025    {
1026        let cheatcode_methods: Arc<RwLock<Vec<String>>> = Arc::new(RwLock::new(vec![]));
1027        let mut cheatcodes_impl = rpc::surfnet_cheatcodes::SurfnetCheatcodesRpc {
1028            registered_methods: Arc::clone(&cheatcode_methods),
1029        };
1030        io.extend_with(cheatcodes_impl.to_delegate());
1031
1032        cheatcode_methods
1033            .write()
1034            .unwrap()
1035            .extend(io.iter().map(|(n, _)| n.clone()).collect::<Vec<String>>());
1036    }
1037
1038    io.extend_with(rpc::minimal::SurfpoolMinimalRpc.to_delegate());
1039    io.extend_with(rpc::full::SurfpoolFullRpc.to_delegate());
1040    io.extend_with(rpc::jito::SurfpoolJitoRpc.to_delegate());
1041    io.extend_with(rpc::accounts_data::SurfpoolAccountsDataRpc.to_delegate());
1042    io.extend_with(rpc::accounts_scan::SurfpoolAccountsScanRpc.to_delegate());
1043    io.extend_with(rpc::bank_data::SurfpoolBankDataRpc.to_delegate());
1044    io.extend_with(rpc::admin::SurfpoolAdminRpc.to_delegate());
1045
1046    let (close_handle_tx, close_handle_rx) =
1047        crossbeam_channel::bounded::<Result<jsonrpc_http_server::CloseHandle, String>>(1);
1048
1049    let _handle = hiro_system_kit::thread_named("RPC Handler")
1050        .spawn(move || {
1051            let server = match ServerBuilder::new(io)
1052                .cors(DomainsValidation::Disabled)
1053                .threads(6)
1054                .max_request_body_size(15 * 1024 * 1024)
1055                .start_http(&server_bind)
1056            {
1057                Ok(server) => server,
1058                Err(e) => {
1059                    let error = format!("Failed to start RPC server: {:?}", e);
1060                    let _ = close_handle_tx.send(Err(error.clone()));
1061                    let _ = simnet_events_tx.send(SimnetEvent::Aborted(error));
1062                    return;
1063                }
1064            };
1065
1066            let _ = close_handle_tx.send(Ok(server.close_handle()));
1067            server.wait();
1068            let _ = simnet_events_tx.send(SimnetEvent::Shutdown);
1069        })
1070        .map_err(|e| format!("Failed to spawn RPC Handler thread: {:?}", e))?;
1071
1072    let close_handle = close_handle_rx
1073        .recv()
1074        .map_err(|_| "Failed to receive HTTP RPC server startup result".to_string())??;
1075
1076    Ok((_handle, close_handle))
1077}
1078async fn start_ws_rpc_server_runloop(
1079    config: &SurfpoolConfig,
1080    middleware: SurfpoolMiddleware,
1081    simnet_events_tx: Sender<SimnetEvent>,
1082) -> Result<(JoinHandle<()>, jsonrpc_ws_server::CloseHandle), String> {
1083    let ws_server_bind: SocketAddr = config
1084        .rpc
1085        .get_ws_base_url()
1086        .parse::<SocketAddr>()
1087        .map_err(|e| e.to_string())?;
1088
1089    let uid = std::sync::atomic::AtomicUsize::new(0);
1090    let ws_middleware = SurfpoolWebsocketMiddleware::new(middleware.clone(), None);
1091
1092    let mut rpc_io = PubSubHandler::new(MetaIoHandler::with_middleware(ws_middleware));
1093
1094    let (close_handle_tx, close_handle_rx) =
1095        crossbeam_channel::bounded::<Result<jsonrpc_ws_server::CloseHandle, String>>(1);
1096
1097    let _ws_handle = hiro_system_kit::thread_named("WebSocket RPC Handler")
1098        .spawn(move || {
1099            // The pubsub handler needs to be able to run async tasks, so we create a Tokio runtime here
1100            let runtime = tokio::runtime::Builder::new_multi_thread()
1101                .enable_all()
1102                .build()
1103                .expect("Failed to build Tokio runtime");
1104
1105            let tokio_handle = runtime.handle();
1106            rpc_io.extend_with(
1107                rpc::ws::SurfpoolWsRpc {
1108                    uid,
1109                    signature_subscription_map: Arc::new(RwLock::new(HashMap::new())),
1110                    account_subscription_map: Arc::new(RwLock::new(HashMap::new())),
1111                    program_subscription_map: Arc::new(RwLock::new(HashMap::new())),
1112                    slot_subscription_map: Arc::new(RwLock::new(HashMap::new())),
1113                    logs_subscription_map: Arc::new(RwLock::new(HashMap::new())),
1114                    snapshot_subscription_map: Arc::new(RwLock::new(HashMap::new())),
1115                    tokio_handle: tokio_handle.clone(),
1116                }
1117                .to_delegate(),
1118            );
1119            runtime.block_on(async move {
1120                let server = match WsServerBuilder::new(rpc_io)
1121                    .session_meta_extractor(move |ctx: &RequestContext| {
1122                        // Create meta from context + session
1123                        let runloop_context = RunloopContext {
1124                            id: None,
1125                            svm_locker: middleware.surfnet_svm.clone(),
1126                            simnet_commands_tx: middleware.simnet_commands_tx.clone(),
1127                            remote_rpc_client: middleware.remote_rpc_client.clone(),
1128                            rpc_config: middleware.config.clone(),
1129                            cheatcode_config: middleware.cheatcode_config.clone(),
1130                            plugin_commands_tx: middleware.plugin_commands_tx.clone(),
1131                        };
1132                        Some(SurfpoolWebsocketMeta::new(
1133                            runloop_context,
1134                            Some(Arc::new(Session::new(ctx.sender()))),
1135                        ))
1136                    })
1137                    .start(&ws_server_bind)
1138                {
1139                    Ok(server) => server,
1140                    Err(e) => {
1141                        let error = format!("Failed to start WebSocket RPC server: {:?}", e);
1142                        let _ = close_handle_tx.send(Err(error.clone()));
1143                        let _ = simnet_events_tx.send(SimnetEvent::Aborted(error));
1144                        return;
1145                    }
1146                };
1147                let _ = close_handle_tx.send(Ok(server.close_handle()));
1148                // The server itself is blocking, so spawn it in a separate thread if needed
1149                tokio::task::spawn_blocking(move || {
1150                    server.wait().unwrap();
1151                })
1152                .await
1153                .ok();
1154
1155                let _ = simnet_events_tx.send(SimnetEvent::Shutdown);
1156            });
1157        })
1158        .map_err(|e| format!("Failed to spawn WebSocket RPC Handler thread: {:?}", e))?;
1159
1160    let close_handle = close_handle_rx
1161        .recv()
1162        .map_err(|_| "Failed to receive WebSocket RPC server startup result".to_string())??;
1163
1164    Ok((_ws_handle, close_handle))
1165}