use num_bigint::BigUint;
use serde_json::{json, Value};
use std::io::{BufRead, BufReader, Write};
use std::net::TcpStream;
use std::sync::mpsc::{channel, Receiver};
#[derive(Clone, Debug)]
pub struct Job {
pub job_id: String,
pub height: u32,
pub msg: Vec<u8>, pub version: u8,
pub target_b: BigUint, }
pub enum PoolEvent {
Job(Job),
Difficulty(f64),
#[allow(dead_code)]
SubmitResult { id: u64, accepted: bool, error: Option<String> },
Closed,
}
pub struct Stratum {
#[allow(dead_code)]
stream: TcpStream,
pub extranonce1: Vec<u8>,
pub events: Receiver<PoolEvent>,
#[allow(dead_code)]
submit_id: u64,
}
impl Stratum {
pub fn connect(host: &str, port: u16, address: &str, worker: &str) -> std::io::Result<Stratum> {
let stream = TcpStream::connect((host, port))?;
stream.set_nodelay(true).ok();
let mut w = stream.try_clone()?;
writeln!(w, "{}", json!({"id":1,"method":"mining.subscribe","params":["erga/0.1.0", null]}))?;
w.flush()?;
let mut reader = BufReader::new(stream.try_clone()?);
let en1 = std::sync::Arc::new(std::sync::Mutex::new(Vec::<u8>::new()));
{
let mut line = String::new();
for _ in 0..10 {
line.clear();
if reader.read_line(&mut line)? == 0 {
break;
}
if let Ok(v) = serde_json::from_str::<Value>(&line) {
if v.get("id").and_then(|x| x.as_u64()) == Some(1) {
if let Some(arr) = v.get("result").and_then(|r| r.as_array()) {
if let Some(h) = arr.get(1).and_then(|x| x.as_str()) {
*en1.lock().unwrap() = hex_to_bytes(h);
}
}
break;
}
}
}
}
writeln!(w, "{}", json!({"id":2,"method":"mining.authorize","params":[format!("{address}.{worker}"),"x"]}))?;
w.flush()?;
let (tx, rx) = channel();
let en1_reader = en1.clone();
std::thread::spawn(move || {
let mut line = String::new();
loop {
line.clear();
match reader.read_line(&mut line) {
Ok(0) | Err(_) => break,
Ok(_) => {}
}
if std::env::var("ERGA_DEBUG").is_ok() {
eprint!("โ {line}");
}
if let Ok(v) = serde_json::from_str::<Value>(&line) {
if let Some(ev) = parse_line(&v, &en1_reader) {
if tx.send(ev).is_err() {
break;
}
}
}
}
let _ = tx.send(PoolEvent::Closed);
});
let extranonce1 = en1.lock().unwrap().clone();
Ok(Stratum { stream, extranonce1, events: rx, submit_id: 100 })
}
pub fn submit(
&mut self,
address: &str,
worker: &str,
job_id: &str,
en2_hex: &str,
nonce_hex: &str,
ntime: &str,
) -> std::io::Result<u64> {
self.submit_id += 1;
let id = self.submit_id;
let mut w = self.stream.try_clone()?;
let msg = json!({"id":id,"method":"mining.submit",
"params":[format!("{address}.{worker}"), job_id, en2_hex, ntime, nonce_hex]});
if std::env::var("ERGA_DEBUG").is_ok() {
eprintln!("โ {msg}");
}
writeln!(w, "{msg}")?;
w.flush()?;
Ok(id)
}
}
fn hex_to_bytes(s: &str) -> Vec<u8> {
let s = s.strip_prefix("0x").unwrap_or(s);
(0..s.len()).step_by(2).filter_map(|i| u8::from_str_radix(s.get(i..i + 2)?, 16).ok()).collect()
}
fn parse_line(v: &Value, en1: &std::sync::Mutex<Vec<u8>>) -> Option<PoolEvent> {
if v.get("id").and_then(|x| x.as_u64()) == Some(1) {
if let Some(arr) = v.get("result").and_then(|r| r.as_array()) {
if let Some(en1_hex) = arr.get(1).and_then(|x| x.as_str()) {
*en1.lock().unwrap() = hex_to_bytes(en1_hex);
}
}
return None;
}
if let Some(id) = v.get("id").and_then(|x| x.as_u64()) {
if id >= 100 {
let accepted = v.get("result").and_then(|x| x.as_bool()).unwrap_or(false);
let error = v.get("error").and_then(|e| if e.is_null() { None } else { Some(e.to_string()) });
return Some(PoolEvent::SubmitResult { id, accepted, error });
}
}
match v.get("method").and_then(|m| m.as_str()) {
Some("mining.set_difficulty") => {
let d = v.get("params").and_then(|p| p.get(0)).and_then(|x| x.as_f64()).unwrap_or(1.0);
Some(PoolEvent::Difficulty(d))
}
Some("mining.notify") => {
let p = v.get("params")?.as_array()?;
let job_id = p.first()?.as_str().unwrap_or("0").to_string();
let height = p.get(1)?.as_u64()? as u32;
let msg = hex_to_bytes(p.get(2)?.as_str()?);
let version = p.get(5).and_then(|x| x.as_u64()).unwrap_or(2) as u8;
let target_b = p.get(6)?.as_str()?.parse::<BigUint>().ok()?;
Some(PoolEvent::Job(Job { job_id, height, msg, version, target_b }))
}
_ => None,
}
}