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)
}