use reqwest::Client;
use serde::Deserialize;
use webylib::Amount;
#[derive(Debug, Clone)]
pub struct TargetInfo {
pub difficulty: u32,
pub epoch: u32,
pub mining_amount: Amount,
pub subsidy_amount: Amount,
pub ratio: f64,
}
#[derive(Debug, Deserialize)]
struct TargetResponse {
difficulty_target_bits: u32,
epoch: u32,
mining_amount: String,
mining_subsidy_amount: String,
ratio: f64,
}
#[derive(Debug, Deserialize)]
pub struct MiningReportResponse {
#[serde(default)]
pub status: Option<String>,
#[serde(default)]
pub difficulty_target: Option<u32>,
#[serde(default)]
pub error: Option<String>,
}
pub struct MiningProtocol {
server_url: String,
http: Client,
#[cfg(not(target_arch = "wasm32"))]
http_blocking: reqwest::blocking::Client,
}
impl MiningProtocol {
pub fn server_url(&self) -> &str {
&self.server_url
}
}
impl MiningProtocol {
pub fn new(server_url: &str) -> anyhow::Result<Self> {
let timeout = std::time::Duration::from_secs(60);
#[cfg(not(target_arch = "wasm32"))]
let builder = Client::builder().timeout(timeout);
#[cfg(target_arch = "wasm32")]
let builder = Client::builder();
let http = builder.build()?;
#[cfg(not(target_arch = "wasm32"))]
let http_blocking = reqwest::blocking::Client::builder()
.timeout(timeout)
.build()?;
Ok(MiningProtocol {
server_url: server_url.trim_end_matches('/').to_string(),
http,
#[cfg(not(target_arch = "wasm32"))]
http_blocking,
})
}
pub fn from_network(network: &webylib::NetworkMode) -> anyhow::Result<Self> {
Self::new(network.base_url())
}
pub async fn get_target(&self) -> anyhow::Result<TargetInfo> {
let url = format!("{}/api/v1/target", self.server_url);
let resp: TargetResponse = self.http.get(&url).send().await?.json().await?;
let mining_amount: Amount = resp.mining_amount.parse().map_err(|e| {
anyhow::anyhow!("invalid mining_amount '{}': {}", resp.mining_amount, e)
})?;
let subsidy_amount: Amount = resp.mining_subsidy_amount.parse().map_err(|e| {
anyhow::anyhow!(
"invalid mining_subsidy_amount '{}': {}",
resp.mining_subsidy_amount,
e
)
})?;
Ok(TargetInfo {
difficulty: resp.difficulty_target_bits,
epoch: resp.epoch,
mining_amount,
subsidy_amount,
ratio: resp.ratio,
})
}
pub async fn submit_report(
&self,
preimage: &str,
hash: &[u8; 32],
) -> anyhow::Result<MiningReportResponse> {
use num_bigint::BigUint;
let url = format!("{}/api/v1/mining_report", self.server_url);
let hash_decimal = BigUint::from_bytes_be(hash).to_string();
let body_str = format!(
r#"{{"preimage": "{}", "work": {}, "legalese": {{"terms": true}}}}"#,
preimage, hash_decimal
);
let mut req = self.http.post(&url).body(body_str);
#[cfg(not(target_arch = "wasm32"))]
{ req = req.header("Content-Type", "application/json"); }
let resp = req.send().await?;
let status_code = resp.status();
let body_text = resp.text().await?;
let parsed: MiningReportResponse =
serde_json::from_str(&body_text).unwrap_or(MiningReportResponse {
status: None,
difficulty_target: None,
error: Some(body_text.clone()),
});
if let Some(ref err) = parsed.error {
if err.contains("Didn't use a new secret") {
anyhow::bail!("Didn't use a new secret value.");
}
}
if status_code.is_success() {
Ok(parsed)
} else {
anyhow::bail!(
"mining report rejected (HTTP {}): {}",
status_code,
body_text
)
}
}
}
#[cfg(not(target_arch = "wasm32"))]
impl MiningProtocol {
pub fn submit_report_blocking(
&self,
preimage: &str,
hash: &[u8; 32],
) -> anyhow::Result<MiningReportResponse> {
Self::submit_report_with_client(&self.http_blocking, &self.server_url, preimage, hash)
}
pub fn submit_report_with_client(
client: &reqwest::blocking::Client,
server_url: &str,
preimage: &str,
hash: &[u8; 32],
) -> anyhow::Result<MiningReportResponse> {
use num_bigint::BigUint;
let url = format!("{}/api/v1/mining_report", server_url);
let hash_decimal = BigUint::from_bytes_be(hash).to_string();
let body_str = format!(
r#"{{"preimage": "{}", "work": {}, "legalese": {{"terms": true}}}}"#,
preimage, hash_decimal
);
let resp = client
.post(&url)
.header("Content-Type", "application/json")
.body(body_str)
.send()?;
let status_code = resp.status();
let body_text = resp.text()?;
let parsed: MiningReportResponse =
serde_json::from_str(&body_text).unwrap_or(MiningReportResponse {
status: None,
difficulty_target: None,
error: Some(body_text.clone()),
});
if let Some(ref err) = parsed.error {
if err.contains("Didn't use a new secret") {
anyhow::bail!("Didn't use a new secret value.");
}
}
if status_code.is_success() {
Ok(parsed)
} else {
anyhow::bail!(
"mining report rejected (HTTP {}): {}",
status_code,
body_text
)
}
}
}