webylib 0.1.0

Webcash wallet library implemented in Rust
Documentation
//! Server communication for Webcash operations
//!
//! This module handles HTTP communication with the Webcash server for operations
//! like health checks, replacements, target queries, and mining report submissions.

use serde::{Deserialize, Serialize};
use reqwest::Client;

use crate::error::{Error, Result};
use crate::webcash::PublicWebcash;
use crate::endpoints;

/// Platform-specific server client implementations
#[cfg(target_os = "ios")]
pub mod ios;

/// Cross-platform server client trait
#[async_trait::async_trait]
pub trait ServerClientTrait {
    async fn health_check(&self, webcash: &[PublicWebcash]) -> Result<HealthResponse>;
    async fn replace(&self, request: &ReplaceRequest) -> Result<ReplaceResponse>;
    async fn get_target(&self) -> Result<TargetResponse>;
    async fn submit_mining_report(&self, report: &MiningReportRequest) -> Result<MiningReportResponse>;
}

/// Server configuration
#[derive(Debug, Clone)]
pub struct ServerConfig {
    /// Base URL of the Webcash server
    pub base_url: String,
    /// Request timeout in seconds
    pub timeout_seconds: u64,
}

impl Default for ServerConfig {
    fn default() -> Self {
        ServerConfig {
            base_url: "https://webcash.org".to_string(),
            timeout_seconds: 30,
        }
    }
}

/// Webcash server client
pub struct ServerClient {
    client: Client,
    config: ServerConfig,
}

impl ServerClient {
    /// Create a new server client with default configuration
    pub fn new() -> Result<Self> {
        Self::with_config(ServerConfig::default())
    }

    /// Create a new server client with custom configuration
    pub fn with_config(config: ServerConfig) -> Result<Self> {
        let client = Client::builder()
            .timeout(std::time::Duration::from_secs(config.timeout_seconds))
            .no_proxy()
            .build()?;

        Ok(ServerClient { client, config })
    }

    /// Check the health status of webcash entries
    pub async fn health_check(&self, webcash: &[PublicWebcash]) -> Result<HealthResponse> {
        let mut request_data = Vec::new();
        for wc in webcash {
            request_data.push(wc.to_string());
        }

        let url = format!("{}{}", self.config.base_url, endpoints::HEALTH_CHECK);
        let response = self.client
            .post(&url)
            .json(&request_data)
            .send()
            .await?;

        if !response.status().is_success() {
            return Err(Error::server("Health check request failed"));
        }

        let health_response: HealthResponse = response.json().await?;
        Ok(health_response)
    }

    /// Submit a replacement request to the server
    pub async fn replace(&self, request: &ReplaceRequest) -> Result<ReplaceResponse> {
        let url = format!("{}{}", self.config.base_url, endpoints::REPLACE);

        // Debug: print the request being sent
        println!("🔍 Sending replace request to: {}", url);
        println!("📤 Request payload: {}", serde_json::to_string_pretty(request).unwrap_or_else(|_| "Failed to serialize".to_string()));

        let response = self.client
            .post(&url)
            .json(request)
            .send()
            .await?;

        // Debug: print response status
        let status = response.status();
        println!("📥 Response status: {}", status);

        let response_text = response.text().await?;
        println!("📥 Response body: {}", response_text);

        if !status.is_success() {
            // Try to parse error response for detailed error message
            if let Ok(error_response) = serde_json::from_str::<serde_json::Value>(&response_text) {
                if let Some(error_msg) = error_response.get("error").and_then(|v| v.as_str()) {
                    return Err(Error::server(&format!("Replace request failed: {}", error_msg)));
                }
            }
            return Err(Error::server(&format!("Replace request failed with status {}: {}", status, response_text)));
        }

        let replace_response: ReplaceResponse = serde_json::from_str(&response_text)?;
        Ok(replace_response)
    }

    /// Get current mining target information
    pub async fn get_target(&self) -> Result<TargetResponse> {
        let url = format!("{}{}", self.config.base_url, endpoints::TARGET);
        let response = self.client
            .get(&url)
            .send()
            .await?;

        if !response.status().is_success() {
            return Err(Error::server("Target request failed"));
        }

        let target_response: TargetResponse = response.json().await?;
        Ok(target_response)
    }

    /// Submit a mining report
    pub async fn submit_mining_report(&self, report: &MiningReportRequest) -> Result<MiningReportResponse> {
        let url = format!("{}{}", self.config.base_url, endpoints::MINING_REPORT);
        let response = self.client
            .post(&url)
            .json(report)
            .send()
            .await?;

        if !response.status().is_success() {
            return Err(Error::server("Mining report submission failed"));
        }

        let mining_response: MiningReportResponse = response.json().await?;
        Ok(mining_response)
    }
}

/// Health check response
#[derive(Debug, Deserialize)]
pub struct HealthResponse {
    pub status: String,
    pub results: std::collections::HashMap<String, HealthResult>,
}

/// Individual health check result
#[derive(Debug, Deserialize)]
pub struct HealthResult {
    pub spent: Option<bool>,
    pub amount: Option<String>,
}

/// Replacement request
#[derive(Debug, Serialize)]
pub struct ReplaceRequest {
    pub webcashes: Vec<String>,
    pub new_webcashes: Vec<String>,
    pub legalese: Legalese,
}

/// Terms acceptance
#[derive(Debug, Serialize)]
pub struct Legalese {
    pub terms: bool,
}

/// Replacement response
#[derive(Debug, Deserialize)]
pub struct ReplaceResponse {
    pub status: String,
}

/// Target information response
#[derive(Debug, Deserialize)]
pub struct TargetResponse {
    pub difficulty_target_bits: u32,
    pub epoch: u32,
    pub mining_amount: String,
    pub mining_subsidy_amount: String,
    pub ratio: f64,
}

/// Mining report request
#[derive(Debug, Serialize)]
pub struct MiningReportRequest {
    pub preimage: String,
    pub legalese: Legalese,
}

/// Mining report response
#[derive(Debug, Deserialize)]
pub struct MiningReportResponse {
    pub status: String,
    pub difficulty_target: Option<u32>,
}