use serde::{Deserialize, Serialize};
#[cfg(any(feature = "native", feature = "wasm"))]
use crate::error::{Error, Result};
#[cfg(any(feature = "native", feature = "wasm"))]
use crate::webcash::PublicWebcash;
#[cfg(any(feature = "native", feature = "wasm"))]
use reqwest::Client;
pub mod endpoints {
pub const HEALTH_CHECK: &str = "/api/v1/health_check";
pub const REPLACE: &str = "/api/v1/replace";
pub const TARGET: &str = "/api/v1/target";
pub const MINING_REPORT: &str = "/api/v1/mining_report";
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum NetworkMode {
#[default]
Production,
Testnet,
Custom(String),
}
impl NetworkMode {
pub fn base_url(&self) -> &str {
match self {
NetworkMode::Production => "https://webcash.org",
NetworkMode::Testnet => "https://weby.cash/api/webcash/testnet",
NetworkMode::Custom(url) => url.as_str(),
}
}
}
impl NetworkMode {
pub fn endpoint_url(&self, endpoint: &str) -> String {
format!("{}{}", self.base_url(), endpoint)
}
}
#[cfg(any(feature = "native", feature = "wasm"))]
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), 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>;
}
#[cfg(any(feature = "native", feature = "wasm"))]
#[derive(Debug, Clone)]
pub struct ServerConfig {
pub network: NetworkMode,
pub timeout_seconds: u64,
}
#[cfg(any(feature = "native", feature = "wasm"))]
impl ServerConfig {
pub fn base_url(&self) -> &str {
self.network.base_url()
}
}
#[cfg(any(feature = "native", feature = "wasm"))]
impl Default for ServerConfig {
fn default() -> Self {
ServerConfig {
network: NetworkMode::default(),
timeout_seconds: 30,
}
}
}
#[cfg(any(feature = "native", feature = "wasm"))]
#[derive(Clone)]
pub struct ServerClient {
client: Client,
config: ServerConfig,
}
#[cfg(any(feature = "native", feature = "wasm"))]
impl ServerClient {
pub fn new() -> Result<Self> {
Self::with_config(ServerConfig::default())
}
pub fn with_config(config: ServerConfig) -> Result<Self> {
let builder = Client::builder();
#[cfg(not(target_arch = "wasm32"))]
let builder = builder
.timeout(std::time::Duration::from_secs(config.timeout_seconds))
.pool_max_idle_per_host(10000)
.pool_idle_timeout(std::time::Duration::from_secs(90))
.tcp_nodelay(true);
let client = builder.build()?;
Ok(ServerClient { client, config })
}
fn post_json_body<B: serde::Serialize>(
&self,
url: &str,
body: &B,
) -> Result<reqwest::RequestBuilder> {
let json_str = serde_json::to_string(body)?;
#[cfg(target_arch = "wasm32")]
{
Ok(self.client.post(url).body(json_str))
}
#[cfg(not(target_arch = "wasm32"))]
{
Ok(self
.client
.post(url)
.header("Content-Type", "application/json")
.body(json_str))
}
}
pub async fn health_check(&self, webcash: &[PublicWebcash]) -> Result<HealthResponse> {
let request_data: Vec<String> = webcash.iter().map(|wc| wc.to_string()).collect();
let url = format!("{}{}", self.config.base_url(), endpoints::HEALTH_CHECK);
let response = self.post_json_body(&url, &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)
}
pub async fn replace(&self, request: &ReplaceRequest) -> Result<ReplaceResponse> {
let url = format!("{}{}", self.config.base_url(), endpoints::REPLACE);
let response = self.post_json_body(&url, request)?.send().await?;
let status = response.status();
let response_text = response.text().await?;
if !status.is_success() {
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)
}
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)
}
pub async fn submit_mining_report(
&self,
report: &MiningReportRequest,
) -> Result<MiningReportResponse> {
let url = format!("{}{}", self.config.base_url(), endpoints::MINING_REPORT);
let response = self.post_json_body(&url, 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)
}
}
#[cfg(any(feature = "native", feature = "wasm"))]
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl ServerClientTrait for ServerClient {
async fn health_check(&self, webcash: &[PublicWebcash]) -> Result<HealthResponse> {
self.health_check(webcash).await
}
async fn replace(&self, request: &ReplaceRequest) -> Result<ReplaceResponse> {
self.replace(request).await
}
async fn get_target(&self) -> Result<TargetResponse> {
self.get_target().await
}
async fn submit_mining_report(
&self,
report: &MiningReportRequest,
) -> Result<MiningReportResponse> {
self.submit_mining_report(report).await
}
}
#[derive(Debug, Deserialize)]
pub struct HealthResponse {
pub status: String,
pub results: std::collections::HashMap<String, HealthResult>,
}
#[derive(Debug, Deserialize)]
pub struct HealthResult {
pub spent: Option<bool>,
pub amount: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct ReplaceRequest {
pub webcashes: Vec<String>,
pub new_webcashes: Vec<String>,
pub legalese: Legalese,
}
#[derive(Debug, Serialize)]
pub struct Legalese {
pub terms: bool,
}
#[derive(Debug, Deserialize)]
pub struct ReplaceResponse {
pub status: String,
}
#[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,
}
#[derive(Debug, Serialize)]
pub struct MiningReportRequest {
pub preimage: String,
pub legalese: Legalese,
}
#[derive(Debug, Deserialize)]
pub struct MiningReportResponse {
pub status: String,
pub difficulty_target: Option<u32>,
}