use crate::core::{BatchScanRequest, Result, ScanResult, RecoveryRequest, RecoveryResult};
use crate::core::scanner::WalletScanner;
use crate::core::processor::BatchProcessor;
use crate::core::recovery::RecoveryManager;
use crate::wallet::WalletManager;
use crate::storage::{CacheManager, PersistenceManager};
use crate::config::Config;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::net::TcpListener;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[derive(Debug, Serialize, Deserialize)]
pub struct ScanRequest {
pub wallet_address: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ApiResponse<T> {
pub success: bool,
pub data: Option<T>,
pub error: Option<String>,
}
impl<T> ApiResponse<T> {
pub fn success(data: T) -> Self {
Self {
success: true,
data: Some(data),
error: None,
}
}
pub fn error(error: String) -> Self {
Self {
success: false,
data: None,
error: Some(error),
}
}
}
#[derive(Clone)]
pub struct ApiState {
pub scanner: Arc<WalletScanner>,
pub batch_processor: Arc<BatchProcessor>,
pub recovery_manager: Arc<RecoveryManager>,
pub wallet_manager: Arc<WalletManager>,
pub cache_manager: Arc<CacheManager>,
pub persistence_manager: Arc<dyn PersistenceManager>,
pub config: Config,
}
pub struct Server {
shutdown_tx: tokio::sync::oneshot::Sender<()>,
}
impl Server {
pub async fn shutdown(self) -> Result<()> {
let _ = self.shutdown_tx.send(());
Ok(())
}
}
pub async fn start_server(
state: ApiState,
config: &crate::config::ServerConfig,
) -> Result<Server> {
let addr = format!("{}:{}", config.host, config.port);
let listener = TcpListener::bind(&addr).await
.map_err(|e| crate::SolanaRecoverError::InternalError(
format!("Failed to bind to {}: {}", addr, e)
))?;
println!("Server listening on {}", addr);
let (shutdown_tx, mut shutdown_rx) = tokio::sync::oneshot::channel();
let state_clone = state.clone();
tokio::spawn(async move {
loop {
tokio::select! {
result = listener.accept() => {
match result {
Ok((stream, addr)) => {
let state = state_clone.clone();
tokio::spawn(async move {
if let Err(e) = handle_connection(stream, state).await {
eprintln!("Error handling connection from {}: {}", addr, e);
}
});
}
Err(e) => {
eprintln!("Failed to accept connection: {}", e);
}
}
}
_ = &mut shutdown_rx => {
println!("Server shutdown signal received");
break;
}
}
}
});
Ok(Server { shutdown_tx })
}
async fn handle_connection(
mut stream: tokio::net::TcpStream,
state: ApiState,
) -> Result<()> {
let mut buffer = [0; 1024];
let n = stream.read(&mut buffer).await
.map_err(|e| crate::SolanaRecoverError::IoError(e.to_string()))?;
let request = String::from_utf8_lossy(&buffer[..n]);
let response = if request.contains("GET / HTTP") {
create_json_response(&ApiResponse::success("Solana Recover API v1.0.0 - Scalable wallet scanner"))
} else if request.contains("GET /health HTTP") {
create_json_response(&ApiResponse::success("OK"))
} else if request.contains("POST /api/v1/scan HTTP") {
handle_scan_request(&request, &state).await
} else if request.contains("POST /api/v1/batch-scan HTTP") {
handle_batch_scan_request(&request, &state).await
} else if request.contains("POST /api/v1/recover HTTP") {
handle_recovery_request(&request, &state).await
} else if request.contains("POST /api/v1/estimate-fees HTTP") {
handle_estimate_fees_request(&request, &state).await
} else if request.contains("GET /api/v1/recovery/") {
handle_get_recovery_status(&request, &state).await
} else {
"HTTP/1.1 404 Not Found\r\n\r\n".to_string()
};
stream.write_all(response.as_bytes()).await
.map_err(|e| crate::SolanaRecoverError::IoError(e.to_string()))?;
Ok(())
}
async fn handle_scan_request(request: &str, state: &ApiState) -> String {
let json_str = extract_json_from_request(request);
match serde_json::from_str::<ScanRequest>(json_str) {
Ok(scan_req) => {
match state.scanner.scan_wallet(&scan_req.wallet_address).await {
Ok(result) => create_json_response(&ApiResponse::success(result)),
Err(e) => create_json_response(&ApiResponse::<ScanResult>::error(e.to_string())),
}
}
Err(e) => create_json_response(&ApiResponse::<ScanResult>::error(format!("Invalid scan request: {}", e))),
}
}
async fn handle_batch_scan_request(request: &str, state: &ApiState) -> String {
let json_str = extract_json_from_request(request);
match serde_json::from_str::<BatchScanRequest>(json_str) {
Ok(batch_req) => {
match state.batch_processor.process_batch(&batch_req).await {
Ok(result) => create_json_response(&ApiResponse::success(result)),
Err(e) => create_json_response(&ApiResponse::<crate::core::BatchScanResult>::error(e.to_string())),
}
}
Err(e) => create_json_response(&ApiResponse::<crate::core::BatchScanResult>::error(format!("Invalid batch scan request: {}", e))),
}
}
async fn handle_recovery_request(request: &str, state: &ApiState) -> String {
let json_str = extract_json_from_request(request);
match serde_json::from_str::<RecoveryRequest>(json_str) {
Ok(recovery_req) => {
match state.recovery_manager.recover_sol(&recovery_req).await {
Ok(result) => create_json_response(&ApiResponse::success(result)),
Err(e) => create_json_response(&ApiResponse::<RecoveryResult>::error(e.to_string())),
}
}
Err(e) => create_json_response(&ApiResponse::<RecoveryResult>::error(format!("Invalid recovery request: {}", e))),
}
}
async fn handle_estimate_fees_request(request: &str, state: &ApiState) -> String {
let json_str = extract_json_from_request(request);
match serde_json::from_str::<Vec<String>>(json_str) {
Ok(accounts) => {
match state.recovery_manager.estimate_recovery_fees(&accounts).await {
Ok(fees) => create_json_response(&ApiResponse::success(fees)),
Err(e) => create_json_response(&ApiResponse::<u64>::error(e.to_string())),
}
}
Err(e) => create_json_response(&ApiResponse::<u64>::error(format!("Invalid accounts list: {}", e))),
}
}
async fn handle_get_recovery_status(request: &str, state: &ApiState) -> String {
if let Some(start) = request.find("/api/v1/recovery/") {
let path_part = &request[start..];
if let Some(end) = path_part.find(" HTTP") {
let recovery_id_str = &path_part[19..end]; if let Ok(recovery_id) = uuid::Uuid::parse_str(recovery_id_str) {
match state.recovery_manager.get_recovery_status(&recovery_id).await {
Ok(result) => create_json_response(&ApiResponse::success(result)),
Err(e) => create_json_response(&ApiResponse::<Option<RecoveryResult>>::error(e.to_string())),
}
} else {
create_json_response(&ApiResponse::<Option<RecoveryResult>>::error("Invalid recovery ID".to_string()))
}
} else {
create_json_response(&ApiResponse::<Option<RecoveryResult>>::error("Invalid request format".to_string()))
}
} else {
create_json_response(&ApiResponse::<Option<RecoveryResult>>::error("Recovery ID not found".to_string()))
}
}
pub fn extract_json_from_request(request: &str) -> &str {
if let Some(start) = request.find("{") {
if let Some(end) = request.rfind("}") {
&request[start..=end]
} else {
&request[start..]
}
} else {
"{}"
}
}
pub fn create_json_response<T: Serialize>(data: &T) -> String {
let json_body = serde_json::to_string(data).unwrap_or_default();
format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
json_body.len(),
json_body
)
}