Skip to main content

surfpool_core/rpc/
mod.rs

1use std::{
2    future::Future,
3    sync::{Arc, Mutex},
4};
5
6use blake3::Hash;
7use crossbeam_channel::Sender;
8use jsonrpc_core::{
9    BoxFuture, Error, ErrorCode, FutureResponse, Metadata, Middleware, Request, Response,
10    futures::{FutureExt, future::Either},
11    middleware,
12};
13use jsonrpc_pubsub::{PubSubMetadata, Session};
14use solana_clock::Slot;
15use surfpool_types::{CheatcodeConfig, SimnetCommand, SimnetEvent, types::RpcConfig};
16
17use crate::{
18    error::{SurfpoolError, SurfpoolResult},
19    surfnet::{
20        PluginCommand,
21        locker::SurfnetSvmLocker,
22        remote::{SomeRemoteCtx, SurfnetRemoteClient},
23        svm::SurfnetSvm,
24    },
25};
26
27pub mod accounts_data;
28pub mod accounts_scan;
29pub mod admin;
30pub mod bank_data;
31pub mod full;
32pub mod jito;
33pub mod minimal;
34pub mod surfnet_cheatcodes;
35pub mod utils;
36pub mod ws;
37
38#[derive(PartialEq, Eq, Clone, Copy, Debug)]
39pub enum RpcHealthStatus {
40    Ok,
41    Behind { num_slots: Slot },
42    Unknown,
43}
44
45pub struct SurfpoolRpc;
46
47#[derive(Clone)]
48pub struct RunloopContext {
49    pub id: Option<(Hash, String)>,
50    pub svm_locker: SurfnetSvmLocker,
51    pub simnet_commands_tx: Sender<SimnetCommand>,
52    pub remote_rpc_client: Option<SurfnetRemoteClient>,
53    pub rpc_config: RpcConfig,
54    pub cheatcode_config: Arc<Mutex<CheatcodeConfig>>,
55    pub plugin_commands_tx: Sender<PluginCommand>,
56}
57
58pub struct SurfnetRpcContext<T> {
59    pub svm_locker: SurfnetSvmLocker,
60    pub remote_ctx: Option<(SurfnetRemoteClient, T)>,
61}
62
63trait State {
64    fn get_svm_locker(&self) -> SurfpoolResult<SurfnetSvmLocker>;
65    fn with_svm_reader<T, F>(&self, reader: F) -> Result<T, SurfpoolError>
66    where
67        F: Fn(&SurfnetSvm) -> T + Send + Sync,
68        T: Send + 'static;
69    fn get_rpc_context<T>(&self, input: T) -> SurfpoolResult<SurfnetRpcContext<T>>;
70    fn get_surfnet_command_tx(&self) -> SurfpoolResult<Sender<SimnetCommand>>;
71}
72
73impl State for Option<RunloopContext> {
74    fn get_svm_locker(&self) -> SurfpoolResult<SurfnetSvmLocker> {
75        // Retrieve svm state
76        let Some(ctx) = self else {
77            return Err(SurfpoolError::missing_context());
78        };
79        Ok(ctx.svm_locker.clone())
80    }
81
82    fn with_svm_reader<T, F>(&self, reader: F) -> Result<T, SurfpoolError>
83    where
84        F: Fn(&SurfnetSvm) -> T + Send + Sync,
85        T: Send + 'static,
86    {
87        let Some(ctx) = self else {
88            return Err(SurfpoolError::missing_context());
89        };
90        Ok(ctx.svm_locker.with_svm_reader(reader))
91    }
92
93    fn get_rpc_context<T>(&self, input: T) -> SurfpoolResult<SurfnetRpcContext<T>> {
94        let Some(ctx) = self else {
95            return Err(SurfpoolError::missing_context());
96        };
97
98        Ok(SurfnetRpcContext {
99            svm_locker: ctx.svm_locker.clone(),
100            remote_ctx: ctx.remote_rpc_client.get_remote_ctx(input),
101        })
102    }
103
104    fn get_surfnet_command_tx(&self) -> SurfpoolResult<Sender<SimnetCommand>> {
105        let Some(ctx) = self else {
106            return Err(SurfpoolError::missing_context());
107        };
108        Ok(ctx.simnet_commands_tx.clone())
109    }
110}
111
112impl Metadata for RunloopContext {}
113
114#[derive(Clone)]
115pub struct SurfpoolMiddleware {
116    pub surfnet_svm: SurfnetSvmLocker,
117    pub simnet_commands_tx: Sender<SimnetCommand>,
118    pub config: RpcConfig,
119    pub remote_rpc_client: Option<SurfnetRemoteClient>,
120    pub cheatcode_config: Arc<Mutex<CheatcodeConfig>>,
121    pub plugin_commands_tx: Sender<PluginCommand>,
122}
123
124impl SurfpoolMiddleware {
125    pub fn new(
126        surfnet_svm: SurfnetSvmLocker,
127        simnet_commands_tx: &Sender<SimnetCommand>,
128        config: &RpcConfig,
129        remote_rpc_client: &Option<SurfnetRemoteClient>,
130        plugin_commands_tx: Sender<PluginCommand>,
131    ) -> Self {
132        Self {
133            surfnet_svm,
134            simnet_commands_tx: simnet_commands_tx.clone(),
135            config: config.clone(),
136            remote_rpc_client: remote_rpc_client.clone(),
137            cheatcode_config: CheatcodeConfig::new(),
138            plugin_commands_tx,
139        }
140    }
141}
142
143impl Middleware<Option<RunloopContext>> for SurfpoolMiddleware {
144    type Future = FutureResponse;
145    type CallFuture = middleware::NoopCallFuture;
146
147    fn on_request<F, X>(
148        &self,
149        request: Request,
150        _meta: Option<RunloopContext>,
151        next: F,
152    ) -> Either<Self::Future, X>
153    where
154        F: FnOnce(Request, Option<RunloopContext>) -> X + Send,
155        X: Future<Output = Option<Response>> + Send + 'static,
156    {
157        let Request::Single(jsonrpc_core::Call::MethodCall(ref method_call)) = request else {
158            let error = Response::from(
159                Error {
160                    code: ErrorCode::InvalidRequest,
161                    message: "Only method calls are supported".into(),
162                    data: None,
163                },
164                None,
165            );
166            warn!("Request rejected due to not being a single method call");
167
168            return Either::Left(Box::pin(async move { Some(error) }));
169        };
170
171        let method_name = method_call.method.clone();
172        debug!("Processing request '{}'", method_name);
173
174        let meta = Some(RunloopContext {
175            id: None,
176            svm_locker: self.surfnet_svm.clone(),
177            simnet_commands_tx: self.simnet_commands_tx.clone(),
178            remote_rpc_client: self.remote_rpc_client.clone(),
179            rpc_config: self.config.clone(),
180            cheatcode_config: self.cheatcode_config.clone(),
181            plugin_commands_tx: self.plugin_commands_tx.clone(),
182        });
183
184        // All surfnet cheatcodes will start with surfnet. If the request is a cheatcode, make sure it isn't disabled.
185        if method_name.starts_with("surfnet_")
186            && let Some(meta_val) = meta.clone()
187        {
188            let Ok(meta_val) = meta_val.cheatcode_config.lock() else {
189                let error = Response::from(
190                    Error {
191                        code: ErrorCode::InternalError,
192                        message: "An internal server error occured".to_string(),
193                        data: None,
194                    },
195                    None,
196                );
197                warn!("Request rejected due to cheatcode being disabled");
198
199                return Either::Left(Box::pin(async move { Some(error) }));
200            };
201            if meta_val.is_cheatcode_disabled(&method_name) {
202                let error = Response::from(
203                    Error {
204                        code: ErrorCode::InvalidRequest,
205                        message: format!(
206                            "Cheatcode rpc method: {method_name} is currently disabled"
207                        ),
208                        data: None,
209                    },
210                    None,
211                );
212                warn!("Request rejected due to cheatcode rpc method being disabled");
213
214                return Either::Left(Box::pin(async move { Some(error) }));
215            }
216        }
217
218        Either::Left(Box::pin(next(request, meta).map(move |res| {
219            if let Some(Response::Single(output)) = &res {
220                if let jsonrpc_core::Output::Failure(failure) = output {
221                    debug!(
222                        "RPC error for method '{}': code={:?}, message={}",
223                        method_name, failure.error.code, failure.error.message
224                    );
225                }
226            }
227            res
228        })))
229    }
230}
231
232#[derive(Clone)]
233pub struct SurfpoolWebsocketMiddleware {
234    pub surfpool_middleware: SurfpoolMiddleware,
235    pub session: Option<Arc<Session>>,
236}
237
238impl SurfpoolWebsocketMiddleware {
239    pub fn new(surfpool_middleware: SurfpoolMiddleware, session: Option<Arc<Session>>) -> Self {
240        Self {
241            surfpool_middleware,
242            session,
243        }
244    }
245}
246
247impl Middleware<Option<SurfpoolWebsocketMeta>> for SurfpoolWebsocketMiddleware {
248    type Future = FutureResponse;
249    type CallFuture = middleware::NoopCallFuture;
250
251    fn on_request<F, X>(
252        &self,
253        request: Request,
254        meta: Option<SurfpoolWebsocketMeta>,
255        next: F,
256    ) -> Either<Self::Future, X>
257    where
258        F: FnOnce(Request, Option<SurfpoolWebsocketMeta>) -> X + Send,
259        X: Future<Output = Option<Response>> + Send + 'static,
260    {
261        let runloop_context = RunloopContext {
262            id: None,
263            svm_locker: self.surfpool_middleware.surfnet_svm.clone(),
264            simnet_commands_tx: self.surfpool_middleware.simnet_commands_tx.clone(),
265            remote_rpc_client: self.surfpool_middleware.remote_rpc_client.clone(),
266            rpc_config: self.surfpool_middleware.config.clone(),
267            cheatcode_config: self.surfpool_middleware.cheatcode_config.clone(),
268            plugin_commands_tx: self.surfpool_middleware.plugin_commands_tx.clone(),
269        };
270        let session = meta
271            .as_ref()
272            .and_then(|m| m.session.clone())
273            .or(self.session.clone());
274        let meta = Some(SurfpoolWebsocketMeta::new(runloop_context, session));
275        Either::Left(Box::pin(next(request, meta).map(move |res| res)))
276    }
277}
278
279#[derive(Clone)]
280pub struct SurfpoolWebsocketMeta {
281    pub runloop_context: RunloopContext,
282    pub session: Option<Arc<Session>>,
283}
284
285impl SurfpoolWebsocketMeta {
286    pub fn new(runloop_context: RunloopContext, session: Option<Arc<Session>>) -> Self {
287        Self {
288            runloop_context,
289            session,
290        }
291    }
292
293    pub fn log_debug(&self, msg: &str) {
294        let _ = self
295            .runloop_context
296            .svm_locker
297            .simnet_events_tx()
298            .send(SimnetEvent::debug(msg));
299    }
300
301    pub fn log_warn(&self, msg: &str) {
302        let _ = self
303            .runloop_context
304            .svm_locker
305            .simnet_events_tx()
306            .send(SimnetEvent::warn(msg));
307    }
308}
309
310impl State for Option<SurfpoolWebsocketMeta> {
311    fn get_svm_locker(&self) -> SurfpoolResult<SurfnetSvmLocker> {
312        let Some(ctx) = self else {
313            return Err(SurfpoolError::missing_context());
314        };
315        Ok(ctx.runloop_context.svm_locker.clone())
316    }
317
318    fn with_svm_reader<T, F>(&self, reader: F) -> Result<T, SurfpoolError>
319    where
320        F: Fn(&SurfnetSvm) -> T + Send + Sync,
321        T: Send + 'static,
322    {
323        let Some(ctx) = self else {
324            return Err(SurfpoolError::missing_context());
325        };
326        Ok(ctx.runloop_context.svm_locker.with_svm_reader(reader))
327    }
328
329    fn get_rpc_context<T>(&self, input: T) -> SurfpoolResult<SurfnetRpcContext<T>> {
330        let Some(ctx) = self else {
331            return Err(SurfpoolError::missing_context());
332        };
333
334        Ok(SurfnetRpcContext {
335            svm_locker: ctx.runloop_context.svm_locker.clone(),
336            remote_ctx: ctx.runloop_context.remote_rpc_client.get_remote_ctx(input),
337        })
338    }
339
340    fn get_surfnet_command_tx(&self) -> SurfpoolResult<Sender<SimnetCommand>> {
341        let Some(ctx) = self else {
342            return Err(SurfpoolError::missing_context());
343        };
344        Ok(ctx.runloop_context.simnet_commands_tx.clone())
345    }
346}
347
348impl Metadata for SurfpoolWebsocketMeta {}
349impl PubSubMetadata for SurfpoolWebsocketMeta {
350    fn session(&self) -> Option<Arc<jsonrpc_pubsub::Session>> {
351        self.session.clone()
352    }
353}
354
355pub const NOT_IMPLEMENTED_CODE: i64 = -32051; // -32000 to -32099 are reserved by the json-rpc spec for custom errors
356pub const NOT_IMPLEMENTED_MSG: &str = "Method not yet implemented. If this endpoint is a priority for you, please open an issue here so we can prioritize: https://github.com/solana-foundation/surfpool/issues";
357fn not_implemented_msg(method: &str) -> String {
358    format!(
359        "Method `{}` is not yet implemented. If this endpoint is a priority for you, please open an issue here so we can prioritize: https://github.com/solana-foundation/surfpool/issues",
360        method
361    )
362}
363/// Helper function to return a `NotImplemented` JSON RPC error
364pub fn not_implemented_err<T>(method: &str) -> Result<T, Error> {
365    Err(Error {
366        code: jsonrpc_core::types::ErrorCode::ServerError(NOT_IMPLEMENTED_CODE),
367        message: not_implemented_msg(method),
368        data: None,
369    })
370}
371
372pub fn not_implemented_err_async<T>(method: &str) -> BoxFuture<Result<T, Error>> {
373    let method = method.to_string();
374    Box::pin(async move {
375        Err(Error {
376            code: jsonrpc_core::types::ErrorCode::ServerError(NOT_IMPLEMENTED_CODE),
377            message: not_implemented_msg(&method),
378            data: None,
379        })
380    })
381}