opt-in-miner 0.4.1

Opt-in Monero/Wownero mining library for transparent application monetization
Documentation
use std::{
    io::{BufRead, BufReader, Read, Write},
    net::TcpStream,
};

use crate::job::Job;

#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error("{0}")]
    Io(#[from] std::io::Error),
    #[error("{0}")]
    Json(#[from] serde_json::Error),
    #[error("node error: {0}")]
    Node(String),
    #[error("missing field: {0}")]
    MissingField(&'static str),
}

pub struct SoloSource {
    host: String,
    port: u16,
    wallet: String,
    stream: Option<TcpStream>,
}

impl SoloSource {
    pub fn new(address: &str, wallet: &str) -> Self {
        let (host, port) = parse_address(address);
        Self {
            host,
            port,
            wallet: wallet.into(),
            stream: None,
        }
    }

    pub fn get_block_template(&mut self) -> Result<Job, Error> {
        let body = serde_json::json!({
            "jsonrpc": "2.0",
            "id": "0",
            "method": "get_block_template",
            "params": {
                "wallet_address": self.wallet,
                "reserve_size": 8
            }
        });
        let response = self.rpc_call(&serde_json::to_vec(&body)?)?;
        let result = response
            .get("result")
            .ok_or(Error::MissingField("result"))?;

        if let Some(error) = response.get("error")
            && let Some(message) = error.get("message").and_then(|m| m.as_str())
        {
            return Err(Error::Node(message.into()));
        }

        let hashing_blob_hex = result
            .get("blockhashing_blob")
            .and_then(|v| v.as_str())
            .ok_or(Error::MissingField("blockhashing_blob"))?;
        let template_blob_hex = result
            .get("blocktemplate_blob")
            .and_then(|v| v.as_str())
            .ok_or(Error::MissingField("blocktemplate_blob"))?;
        let seed_hex = result
            .get("seed_hash")
            .and_then(|v| v.as_str())
            .ok_or(Error::MissingField("seed_hash"))?;
        let difficulty = result
            .get("difficulty")
            .and_then(serde_json::Value::as_u64)
            .ok_or(Error::MissingField("difficulty"))?;
        let height = result
            .get("height")
            .and_then(serde_json::Value::as_u64)
            .ok_or(Error::MissingField("height"))?;

        let threshold = if difficulty == 0 {
            1
        } else {
            u64::MAX / difficulty
        };

        Ok(Job {
            id: height.to_string(),
            hashing_blob: hex::decode(hashing_blob_hex).unwrap_or_default(),
            seed: hex::decode(seed_hex).unwrap_or_default(),
            threshold,
            template_blob: Some(hex::decode(template_blob_hex).unwrap_or_default()),
        })
    }

    pub fn submit_block(&mut self, block_blob: &[u8]) -> Result<(), Error> {
        let body = serde_json::json!({
            "jsonrpc": "2.0",
            "id": "0",
            "method": "submit_block",
            "params": [hex::encode(block_blob)]
        });
        let response = self.rpc_call(&serde_json::to_vec(&body)?)?;
        if let Some(error) = response.get("error")
            && let Some(message) = error.get("message").and_then(|m| m.as_str())
        {
            return Err(Error::Node(message.into()));
        }
        Ok(())
    }

    fn rpc_call(&mut self, body: &[u8]) -> Result<serde_json::Value, Error> {
        let host = self.host.clone();
        let stream = self.connect()?;
        let request = format!(
            "POST /json_rpc HTTP/1.1\r\nHost: {host}\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n",
            body.len()
        );
        stream.write_all(request.as_bytes())?;
        stream.write_all(body)?;
        stream.flush()?;

        let mut reader = BufReader::new(stream);

        let mut status_line = String::new();
        reader.read_line(&mut status_line)?;

        let mut content_length = 0usize;
        loop {
            let mut line = String::new();
            reader.read_line(&mut line)?;
            if line.trim().is_empty() {
                break;
            }
            let lower = line.to_ascii_lowercase();
            if let Some(value) = lower.strip_prefix("content-length:") {
                content_length = value.trim().parse().unwrap_or(0);
            }
        }

        let mut response_body = vec![0u8; content_length];
        reader.read_exact(&mut response_body)?;
        Ok(serde_json::from_slice(&response_body)?)
    }

    fn connect(&mut self) -> Result<&mut TcpStream, Error> {
        if self.stream.is_none() {
            self.stream = Some(TcpStream::connect((&*self.host, self.port))?);
        }
        Ok(self.stream.as_mut().unwrap())
    }

    pub fn disconnect(&mut self) {
        self.stream = None;
    }
}

fn parse_address(address: &str) -> (String, u16) {
    if let Some((host, port_str)) = address.rsplit_once(':')
        && let Ok(port) = port_str.parse()
    {
        return (host.into(), port);
    }
    (address.into(), 18081)
}