use actix_web::{middleware::Logger, web, App, HttpResponse, HttpServer, Result};
use alloy::primitives::{Address, TxKind, U256};
use revm_trace::{
traits::TransactionTrace,
types::{SimulationBatch, SimulationTx},
TxInspector,
};
use serde::{Deserialize, Serialize};
use std::str::FromStr;
#[cfg(not(feature = "foundry-fork"))]
use revm_trace::{create_evm, create_evm_with_tracer};
#[cfg(feature = "foundry-fork")]
use revm_trace::{create_shared_evm, create_shared_evm_with_tracer};
#[derive(Deserialize)]
struct SimulateRequest {
rpc_url: String,
from: String,
to: String,
value: Option<String>,
data: Option<String>,
with_trace: Option<bool>,
}
#[derive(Serialize)]
struct SimulateResponse {
success: bool,
gas_used: Option<u64>,
error: Option<String>,
traces: Option<serde_json::Value>,
}
async fn simulate_transaction(req: web::Json<SimulateRequest>) -> Result<HttpResponse> {
let request = req.into_inner();
let result = tokio::task::spawn_blocking(move || {
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async { simulate_tx_internal(request).await })
})
.await;
match result {
Ok(response) => Ok(HttpResponse::Ok().json(response)),
Err(e) => Ok(HttpResponse::InternalServerError().json(SimulateResponse {
success: false,
gas_used: None,
error: Some(format!("Runtime error: {}", e)),
traces: None,
})),
}
}
async fn simulate_transaction_web_block(req: web::Json<SimulateRequest>) -> Result<HttpResponse> {
let request = req.into_inner();
let result = web::block(move || {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async { simulate_tx_internal(request).await })
})
.await;
match result {
Ok(response) => Ok(HttpResponse::Ok().json(response)),
Err(e) => Ok(HttpResponse::InternalServerError().json(SimulateResponse {
success: false,
gas_used: None,
error: Some(format!("Web block error: {}", e)),
traces: None,
})),
}
}
async fn simulate_tx_internal(request: SimulateRequest) -> SimulateResponse {
let from_addr = match Address::from_str(&request.from) {
Ok(addr) => addr,
Err(e) => {
return SimulateResponse {
success: false,
gas_used: None,
error: Some(format!("Invalid from address: {}", e)),
traces: None,
}
}
};
let to_addr = match Address::from_str(&request.to) {
Ok(addr) => addr,
Err(e) => {
return SimulateResponse {
success: false,
gas_used: None,
error: Some(format!("Invalid to address: {}", e)),
traces: None,
}
}
};
let value = if let Some(val_str) = request.value {
match U256::from_str(&val_str) {
Ok(val) => val,
Err(e) => {
return SimulateResponse {
success: false,
gas_used: None,
error: Some(format!("Invalid value: {}", e)),
traces: None,
}
}
}
} else {
U256::ZERO
};
let data = if let Some(data_str) = request.data {
match hex::decode(data_str.strip_prefix("0x").unwrap_or(&data_str)) {
Ok(bytes) => bytes.into(),
Err(e) => {
return SimulateResponse {
success: false,
gas_used: None,
error: Some(format!("Invalid data: {}", e)),
traces: None,
}
}
}
} else {
vec![].into()
};
let tx = SimulationTx {
caller: from_addr,
transact_to: TxKind::Call(to_addr),
value,
data,
};
let batch = SimulationBatch {
transactions: vec![tx],
is_stateful: false,
overrides: None,
};
if request.with_trace.unwrap_or(false) {
#[cfg(not(feature = "foundry-fork"))]
let create_evm_result = create_evm_with_tracer(&request.rpc_url, TxInspector::new()).await;
#[cfg(feature = "foundry-fork")]
let create_evm_result =
create_shared_evm_with_tracer(&request.rpc_url, TxInspector::new()).await;
match create_evm_result {
Ok(mut evm) => {
let results = evm.trace_transactions(batch);
match results.into_iter().next() {
Some(Ok((execution_result, _, trace_output))) => SimulateResponse {
success: true,
gas_used: Some(execution_result.gas_used()),
error: None,
traces: Some(serde_json::json!({
"asset_transfers": trace_output.asset_transfers.len(),
"call_traces": trace_output.call_trace,
})),
},
Some(Err(e)) => SimulateResponse {
success: false,
gas_used: None,
error: Some(e.to_string()),
traces: None,
},
None => SimulateResponse {
success: false,
gas_used: None,
error: Some("No results returned".to_string()),
traces: None,
},
}
}
Err(e) => SimulateResponse {
success: false,
gas_used: None,
error: Some(format!("Failed to create tracing EVM: {}", e)),
traces: None,
},
}
} else {
#[cfg(not(feature = "foundry-fork"))]
let create_evm_result = create_evm(&request.rpc_url).await;
#[cfg(feature = "foundry-fork")]
let create_evm_result = create_shared_evm(&request.rpc_url).await;
match create_evm_result {
Ok(mut evm) => {
let results = evm.execute_batch(batch);
match results.into_iter().next() {
Some(Ok(execution_result)) => SimulateResponse {
success: true,
gas_used: Some(execution_result.gas_used()),
error: None,
traces: None,
},
Some(Err(e)) => SimulateResponse {
success: false,
gas_used: None,
error: Some(e.to_string()),
traces: None,
},
None => SimulateResponse {
success: false,
gas_used: None,
error: Some("No results returned".to_string()),
traces: None,
},
}
}
Err(e) => SimulateResponse {
success: false,
gas_used: None,
error: Some(format!("Failed to create EVM: {}", e)),
traces: None,
},
}
}
}
async fn health_check() -> Result<HttpResponse> {
Ok(HttpResponse::Ok().json(serde_json::json!({
"status": "healthy",
"version": "3.0.0"
})))
}
#[tokio::main]
async fn main() -> std::io::Result<()> {
env_logger::init();
println!("🚀 Starting REVM-Trace API Server...");
println!("📡 Endpoints:");
println!(" POST /simulate - Simulate transactions (spawn_blocking)");
println!(" POST /simulate_web_block - Simulate transactions (web::block)");
println!(" GET /health - Health check");
#[cfg(not(feature = "foundry-fork"))]
println!("Using AlloyDB backend for EVM simulation");
#[cfg(feature = "foundry-fork")]
println!("Using Foundry fork backend for EVM simulation");
HttpServer::new(|| {
App::new()
.wrap(Logger::default())
.route("/health", web::get().to(health_check))
.route("/simulate", web::post().to(simulate_transaction))
.route(
"/simulate_web_block",
web::post().to(simulate_transaction_web_block),
)
})
.bind("127.0.0.1:8080")?
.run()
.await
}