Skip to main content

surfpool_core/rpc/
admin.rs

1use jsonrpc_core::{BoxFuture, Error, ErrorCode, Result};
2use jsonrpc_derive::rpc;
3use solana_client::rpc_custom_error::RpcCustomError;
4use surfpool_types::SimnetCommand;
5
6use super::RunloopContext;
7use crate::{PluginInfo, rpc::State, surfnet::PluginCommand};
8
9#[rpc]
10pub trait AdminRpc {
11    type Metadata;
12
13    /// Immediately shuts down the RPC server.
14    ///
15    /// This administrative endpoint is typically used during controlled shutdowns of the validator
16    /// or service exposing the RPC interface. It allows remote administrators to gracefully terminate
17    /// the process, stopping all RPC activity.
18    ///
19    /// ## Returns
20    /// - [`Result<()>`] — A unit result indicating successful shutdown, or an error if the call fails.
21    ///
22    /// ## Example Request (JSON-RPC)
23    /// ```json
24    /// {
25    ///   "jsonrpc": "2.0",
26    ///   "id": 42,
27    ///   "method": "exit",
28    ///   "params": []
29    /// }
30    /// ```
31    ///
32    /// # Notes
33    /// - This method is privileged and should only be accessible to trusted clients.
34    /// - Use with extreme caution in production environments.
35    /// - If successful, the RPC server process will terminate immediately after processing this call.
36    ///
37    /// # Security
38    /// Access to this method should be tightly restricted. Implement proper authorization mechanisms
39    /// via the RPC metadata to prevent accidental or malicious use.
40    #[rpc(meta, name = "exit")]
41    fn exit(&self, meta: Self::Metadata) -> Result<()>;
42
43    #[rpc(meta, name = "reloadPlugin")]
44    fn reload_plugin(
45        &self,
46        meta: Self::Metadata,
47        name: String,
48        config_file: String,
49    ) -> BoxFuture<Result<()>>;
50
51    /// Reloads a runtime plugin with new configuration.
52    ///
53    /// This administrative endpoint is used to dynamically reload a plugin without restarting
54    /// the entire RPC server or validator. It is useful for applying updated configurations
55    /// to a plugin that supports hot-reloading.
56    ///
57    /// ## Parameters
58    /// - `name`: The identifier of the plugin to reload.
59    /// - `config_file`: Path to the new configuration file to load for the plugin.
60    ///
61    /// ## Returns
62    /// - [`BoxFuture<Result<()>>`] — A future resolving to a unit result on success, or an error if
63    ///   reloading fails.
64    ///
65    /// ## Example Request (JSON-RPC)
66    /// ```json
67    /// {
68    ///   "jsonrpc": "2.0",
69    ///   "id": 101,
70    ///   "method": "reloadPlugin",
71    ///   "params": ["myPlugin", "/etc/plugins/my_plugin_config.toml"]
72    /// }
73    /// ```
74    ///
75    /// # Notes
76    /// - The plugin must support reloading in order for this to succeed.
77    /// - A failed reload will leave the plugin in its previous state.
78    /// - This method is intended for administrators and should be properly secured.
79    ///
80    /// # Security
81    /// Ensure only trusted clients can invoke this method. Use metadata-based access control to limit exposure.
82    #[rpc(meta, name = "unloadPlugin")]
83    fn unload_plugin(&self, meta: Self::Metadata, name: String) -> BoxFuture<Result<()>>;
84
85    /// Dynamically loads a new plugin into the runtime from a configuration file.
86    ///
87    /// This administrative endpoint is used to add a new plugin to the system at runtime,
88    /// based on the configuration provided. It enables extensibility without restarting
89    /// the validator or RPC server.
90    ///
91    /// ## Parameters
92    /// - `config_file`: Path to the plugin's configuration file, which defines its behavior and settings.
93    ///
94    /// ## Returns
95    /// - [`BoxFuture<Result<String>>`] — A future resolving to the name or identifier of the loaded plugin,
96    ///   or an error if the plugin could not be loaded.
97    ///
98    /// ## Example Request (JSON-RPC)
99    /// ```json
100    /// {
101    ///   "jsonrpc": "2.0",
102    ///   "id": 102,
103    ///   "method": "loadPlugin",
104    ///   "params": ["/etc/plugins/my_plugin_config.toml"]
105    /// }
106    /// ```
107    ///
108    /// # Notes
109    /// - The plugin system must be initialized and support runtime loading.
110    /// - The config file should be well-formed and point to a valid plugin implementation.
111    /// - Duplicate plugin names may lead to conflicts or errors.
112    ///
113    /// # Security
114    /// This method should be restricted to administrators only. Validate inputs and use access control.
115    #[rpc(meta, name = "loadPlugin")]
116    fn load_plugin(&self, meta: Self::Metadata, config_file: String) -> BoxFuture<Result<String>>;
117
118    /// Returns a list of all currently loaded plugin names.
119    ///
120    /// This administrative RPC method is used to inspect which plugins have been successfully
121    /// loaded into the runtime. It can be useful for debugging or operational monitoring.
122    ///
123    /// ## Returns
124    /// - `Vec<PluginInfo>` — A list of plugin information objects, each containing:
125    ///   - `plugin_name`: The name of the plugin (e.g., "surfpool-subgraph")
126    ///   - `uuid`: The unique identifier of the plugin instance
127    ///
128    /// ## Example Request (JSON-RPC)
129    /// ```json
130    /// {
131    ///   "jsonrpc": "2.0",
132    ///   "id": 103,
133    ///   "method": "listPlugins",
134    ///   "params": []
135    /// }
136    /// ```
137    ///
138    /// ## Example Response
139    /// ```json
140    /// {
141    ///   "jsonrpc": "2.0",
142    ///   "result": [
143    ///     {
144    ///       "plugin_name": "surfpool-subgraph",
145    ///       "uuid": "550e8400-e29b-41d4-a716-446655440000"
146    ///     }
147    ///   ],
148    ///   "id": 103
149    /// }
150    /// ```
151    ///
152    /// # Notes
153    /// - Only plugins that have been successfully loaded will appear in this list.
154    /// - This method is read-only and safe to call frequently.
155    #[rpc(meta, name = "listPlugins")]
156    fn list_plugins(&self, meta: Self::Metadata) -> BoxFuture<Result<Vec<PluginInfo>>>;
157
158    /// Returns the system start time.
159    ///
160    /// This RPC method retrieves the timestamp of when the system was started, represented as
161    /// a `SystemTime`. It can be useful for measuring uptime or for tracking the system's runtime
162    /// in logs or monitoring systems.
163    ///
164    /// ## Returns
165    /// - `SystemTime` — The timestamp representing when the system was started.
166    ///
167    /// ## Example Request (JSON-RPC)
168    /// ```json
169    /// {
170    ///   "jsonrpc": "2.0",
171    ///   "id": 106,
172    ///   "method": "startTime",
173    ///   "params": []
174    /// }
175    /// ```
176    ///
177    /// ## Example Response
178    /// ```json
179    /// {
180    ///   "jsonrpc": "2.0",
181    ///   "result": "2025-04-24T12:34:56Z",
182    ///   "id": 106
183    /// }
184    /// ```
185    ///
186    /// # Notes
187    /// - The result is a `String` in UTC, reflecting the moment the system was initialized.
188    /// - This method is useful for monitoring system uptime and verifying system health.
189    #[rpc(meta, name = "startTime")]
190    fn start_time(&self, meta: Self::Metadata) -> Result<String>;
191}
192
193pub struct SurfpoolAdminRpc;
194impl AdminRpc for SurfpoolAdminRpc {
195    type Metadata = Option<RunloopContext>;
196
197    fn exit(&self, meta: Self::Metadata) -> Result<()> {
198        let Some(ctx) = meta else {
199            return Err(RpcCustomError::NodeUnhealthy {
200                num_slots_behind: None,
201            }
202            .into());
203        };
204
205        let _ = ctx
206            .simnet_commands_tx
207            .send(SimnetCommand::Terminate(ctx.id));
208
209        Ok(())
210    }
211
212    fn reload_plugin(
213        &self,
214        meta: Self::Metadata,
215        name: String,
216        config_file: String,
217    ) -> BoxFuture<Result<()>> {
218        Box::pin(async move {
219            let Some(ctx) = meta else {
220                return Err(RpcCustomError::NodeUnhealthy {
221                    num_slots_behind: None,
222                }
223                .into());
224            };
225            let (tx, rx) = crossbeam_channel::bounded(1);
226            ctx.plugin_commands_tx
227                .send(PluginCommand::Reload {
228                    name,
229                    config_file,
230                    response_tx: tx,
231                })
232                .map_err(|_| Error::internal_error())?;
233            rx.recv()
234                .map_err(|_| Error::internal_error())?
235                .map_err(|e| Error {
236                    code: ErrorCode::InternalError,
237                    message: e,
238                    data: None,
239                })
240        })
241    }
242
243    fn unload_plugin(&self, meta: Self::Metadata, name: String) -> BoxFuture<Result<()>> {
244        Box::pin(async move {
245            let Some(ctx) = meta else {
246                return Err(RpcCustomError::NodeUnhealthy {
247                    num_slots_behind: None,
248                }
249                .into());
250            };
251            let (tx, rx) = crossbeam_channel::bounded(1);
252            ctx.plugin_commands_tx
253                .send(PluginCommand::Unload {
254                    name,
255                    response_tx: tx,
256                })
257                .map_err(|_| Error::internal_error())?;
258            rx.recv()
259                .map_err(|_| Error::internal_error())?
260                .map_err(|e| Error {
261                    code: ErrorCode::InternalError,
262                    message: e,
263                    data: None,
264                })
265        })
266    }
267
268    fn load_plugin(&self, meta: Self::Metadata, config_file: String) -> BoxFuture<Result<String>> {
269        Box::pin(async move {
270            let Some(ctx) = meta else {
271                return Err(RpcCustomError::NodeUnhealthy {
272                    num_slots_behind: None,
273                }
274                .into());
275            };
276            let (tx, rx) = crossbeam_channel::bounded(1);
277            ctx.plugin_commands_tx
278                .send(PluginCommand::Load {
279                    config_file,
280                    response_tx: tx,
281                })
282                .map_err(|_| Error::internal_error())?;
283            rx.recv()
284                .map_err(|_| Error::internal_error())?
285                .map(|info| info.plugin_name)
286                .map_err(|e| Error {
287                    code: ErrorCode::InternalError,
288                    message: e,
289                    data: None,
290                })
291        })
292    }
293
294    fn list_plugins(&self, meta: Self::Metadata) -> BoxFuture<Result<Vec<PluginInfo>>> {
295        Box::pin(async move {
296            let Some(ctx) = meta else {
297                return Err(RpcCustomError::NodeUnhealthy {
298                    num_slots_behind: None,
299                }
300                .into());
301            };
302            let (tx, rx) = crossbeam_channel::bounded(1);
303            ctx.plugin_commands_tx
304                .send(PluginCommand::List { response_tx: tx })
305                .map_err(|_| Error::internal_error())?;
306            Ok(rx.recv().map_err(|_| Error::internal_error())?)
307        })
308    }
309
310    fn start_time(&self, meta: Self::Metadata) -> Result<String> {
311        let svm_locker = meta.get_svm_locker()?;
312        let system_time = svm_locker.get_start_time();
313
314        let datetime_utc: chrono::DateTime<chrono::Utc> = system_time.into();
315        Ok(datetime_utc.to_rfc3339())
316    }
317}