use super::methods::MethodHandler;
use super::types::{RpcError, RpcRequest, RpcResponse};
use super::utils::get_default_rpc_port;
use crate::EngineContext;
use axum::{
extract::{Json as JsonExtract, State},
response::Json as JsonResponse,
routing::{get, post},
Router,
};
use eyre::Result;
use revm::database::CacheDB;
use revm::{Database, DatabaseCommit, DatabaseRef};
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::sync::oneshot;
use tracing::{error, info, warn};
#[derive(Debug)]
pub struct RpcServerHandle {
pub addr: SocketAddr,
shutdown_tx: oneshot::Sender<()>,
}
impl RpcServerHandle {
pub fn addr(&self) -> SocketAddr {
self.addr
}
pub fn port(&self) -> u16 {
self.addr.port()
}
pub fn shutdown(self) -> Result<()> {
if self.shutdown_tx.send(()).is_err() {
warn!("RPC server already shut down");
}
Ok(())
}
}
#[derive(Clone)]
struct RpcState<DB>
where
DB: Database + DatabaseCommit + DatabaseRef + Clone + Send + Sync + 'static,
<CacheDB<DB> as Database>::Error: Clone + Send + Sync,
<DB as Database>::Error: Clone + Send + Sync,
{
server: Arc<DebugRpcServer<DB>>,
}
pub struct DebugRpcServer<DB>
where
DB: Database + DatabaseCommit + DatabaseRef + Clone + Send + Sync + 'static,
<CacheDB<DB> as Database>::Error: Clone + Send + Sync,
<DB as Database>::Error: Clone + Send + Sync,
{
context: Arc<EngineContext<DB>>,
method_handler: Arc<MethodHandler<DB>>,
}
impl<DB> DebugRpcServer<DB>
where
DB: Database + DatabaseCommit + DatabaseRef + Clone + Send + Sync + 'static,
<CacheDB<DB> as Database>::Error: Clone + Send + Sync,
<DB as Database>::Error: Clone + Send + Sync,
{
pub fn new(context: EngineContext<DB>) -> Self {
let context = Arc::new(context);
let method_handler = Arc::new(MethodHandler::new(context.clone()));
Self { context, method_handler }
}
pub async fn start(self) -> Result<RpcServerHandle> {
let port = get_default_rpc_port()?;
self.start_on_port(port).await
}
pub async fn start_on_port(self, port: u16) -> Result<RpcServerHandle> {
let app = Router::new()
.route("/", post(handle_rpc_request))
.route("/health", get(health_check))
.with_state(RpcState { server: Arc::new(self) });
let addr = SocketAddr::from(([127, 0, 0, 1], port));
let listener = tokio::net::TcpListener::bind(addr).await?;
let actual_addr = listener.local_addr()?;
let (shutdown_tx, shutdown_rx) = oneshot::channel();
tokio::spawn(async move {
axum::serve(listener, app)
.with_graceful_shutdown(async {
shutdown_rx.await.ok();
})
.await
.expect("RPC server failed");
});
info!("Debug RPC server started on {}", actual_addr);
Ok(RpcServerHandle { addr: actual_addr, shutdown_tx })
}
async fn handle_request(&self, request: RpcRequest) -> RpcResponse {
let id = request.id.clone();
match self.method_handler.handle_method(&request.method, request.params).await {
Ok(result) => {
RpcResponse { jsonrpc: "2.0".to_string(), result: Some(result), error: None, id }
}
Err(err) => {
error!(target: "rpc", "Error handling RPC request: {:?}", err);
RpcResponse { jsonrpc: "2.0".to_string(), result: None, error: Some(err), id }
}
}
}
pub fn snapshot_count(&self) -> usize {
self.context.snapshots.len()
}
pub fn validate_snapshot_index(&self, index: usize) -> Result<()> {
if index >= self.snapshot_count() {
return Err(eyre::eyre!(
"Snapshot index {} out of bounds (max: {})",
index,
self.snapshot_count() - 1
));
}
Ok(())
}
pub fn context(&self) -> &Arc<EngineContext<DB>> {
&self.context
}
}
async fn handle_rpc_request<DB>(
State(state): State<RpcState<DB>>,
JsonExtract(request): JsonExtract<RpcRequest>,
) -> JsonResponse<RpcResponse>
where
DB: Database + DatabaseCommit + DatabaseRef + Clone + Send + Sync + 'static,
<CacheDB<DB> as Database>::Error: Clone + Send + Sync,
<DB as Database>::Error: Clone + Send + Sync,
{
if request.jsonrpc != "2.0" {
return JsonResponse(RpcResponse {
jsonrpc: "2.0".to_string(),
result: None,
error: Some(RpcError {
code: -32600,
message: "Invalid Request - JSON-RPC version must be 2.0".to_string(),
data: None,
}),
id: request.id.clone(),
});
}
let response = state.server.handle_request(request).await;
JsonResponse(response)
}
async fn health_check() -> JsonResponse<serde_json::Value> {
JsonResponse(serde_json::json!({
"status": "healthy",
"service": "edb-debug-rpc-server",
"version": env!("CARGO_PKG_VERSION"),
"architecture": "multi-threaded"
}))
}
pub async fn start_debug_server<DB>(context: EngineContext<DB>) -> Result<RpcServerHandle>
where
DB: Database + DatabaseCommit + DatabaseRef + Clone + Send + Sync + 'static,
<CacheDB<DB> as Database>::Error: Clone + Send + Sync,
<DB as Database>::Error: Clone + Send + Sync,
{
let server = DebugRpcServer::new(context);
server.start().await
}