1#![deny(missing_docs)]
2
3mod job;
50mod pool;
51mod settings;
52mod solo;
53mod state;
54mod throttle;
55mod worker;
56
57pub use settings::{Persistence, Reply as ConsentReply, Settings, Status as ConsentStatus};
58pub use state::{MiningState, ToggleResult, compile_env};
59pub use throttle::Throttle;
60
61use pool::PoolSource;
62use solo::SoloSource;
63use worker::Worker;
64
65use std::{
66 sync::{
67 Arc,
68 atomic::{AtomicBool, AtomicU64, Ordering},
69 mpsc,
70 },
71 thread,
72 time::{Duration, Instant},
73};
74
75#[derive(Clone)]
77pub enum Source {
78 Solo {
80 node: String,
82 },
83 Pool {
85 url: String,
87 },
88}
89
90impl Source {
91 pub fn node(address: &str) -> Self {
93 Self::Solo {
94 node: address.into(),
95 }
96 }
97
98 pub fn pool(url: &str) -> Self {
100 Self::Pool { url: url.into() }
101 }
102}
103
104pub struct MinerBuilder {
106 sources: Vec<Source>,
107 wallet: String,
108 password: String,
109 threads: usize,
110 light: bool,
111 cpu_fraction: f32,
112 application_name: String,
113 consent_check: Option<Box<dyn FnOnce() -> ConsentReply + Send>>,
114}
115
116impl MinerBuilder {
117 pub fn sources(mut self, sources: &[Source]) -> Self {
119 self.sources = sources.to_vec();
120 self
121 }
122
123 pub fn wallet(mut self, wallet: &str) -> Self {
125 self.wallet = wallet.into();
126 self
127 }
128
129 pub fn password(mut self, password: &str) -> Self {
131 self.password = password.into();
132 self
133 }
134
135 pub fn threads(mut self, count: usize) -> Self {
137 self.threads = count;
138 self
139 }
140
141 pub fn light(mut self, enabled: bool) -> Self {
144 self.light = enabled;
145 self
146 }
147
148 pub fn cpu_fraction(mut self, fraction: f32) -> Self {
151 self.cpu_fraction = fraction;
152 self
153 }
154
155 pub fn application_name(mut self, name: &str) -> Self {
157 self.application_name = name.into();
158 self
159 }
160
161 pub fn consent_check(mut self, check: impl FnOnce() -> ConsentReply + Send + 'static) -> Self {
165 self.consent_check = Some(Box::new(check));
166 self
167 }
168
169 pub fn build(self) -> Miner {
173 let mut settings = Settings::new(&self.application_name);
174
175 let enabled = if settings.has_stored() {
176 settings.consent() == ConsentStatus::Granted
177 } else if let Some(check) = self.consent_check {
178 let reply = check();
179 settings.set_persistence(reply.persistence);
180 settings.set_consent(reply.consent);
181 reply.consent == ConsentStatus::Granted
182 } else {
183 false
184 };
185
186 let (thread_count, cpu_fraction) = if settings.has_stored() {
187 (settings.threads(), settings.cpu_fraction())
188 } else {
189 let threads = if self.threads == 0 {
190 thread::available_parallelism()
191 .map(std::num::NonZero::get)
192 .unwrap_or(1)
193 } else {
194 self.threads
195 };
196 (threads, self.cpu_fraction)
197 };
198
199 Miner {
200 sources: self.sources,
201 wallet: self.wallet,
202 password: self.password,
203 threads: thread_count,
204 light: self.light,
205 throttle: Throttle::new(cpu_fraction),
206 settings,
207 enabled,
208 handle: None,
209 running: Arc::new(AtomicBool::new(false)),
210 hash_count: Arc::new(AtomicU64::new(0)),
211 }
212 }
213}
214
215pub struct Miner {
220 sources: Vec<Source>,
221 wallet: String,
222 password: String,
223 threads: usize,
224 light: bool,
225 throttle: Throttle,
226 settings: Settings,
227 enabled: bool,
228 handle: Option<thread::JoinHandle<()>>,
229 running: Arc<AtomicBool>,
230 hash_count: Arc<AtomicU64>,
231}
232
233impl Miner {
234 pub fn builder() -> MinerBuilder {
236 MinerBuilder {
237 sources: Vec::new(),
238 wallet: String::new(),
239 password: "x".into(),
240 threads: 1,
241 light: true,
242 cpu_fraction: 0.25,
243 application_name: "opt-in-miner".into(),
244 consent_check: None,
245 }
246 }
247
248 pub fn start(&mut self) {
251 if !self.enabled || self.sources.is_empty() || self.running.load(Ordering::Relaxed) {
252 return;
253 }
254
255 let sources = self.sources.clone();
256 let wallet = self.wallet.clone();
257 let password = self.password.clone();
258 let threads = self.threads;
259 let light = self.light;
260 let throttle = self.throttle.clone();
261 let running = self.running.clone();
262 let hash_count = self.hash_count.clone();
263
264 running.store(true, Ordering::Relaxed);
265 hash_count.store(0, Ordering::Relaxed);
266
267 self.handle = Some(thread::spawn(move || {
268 run_mining_loop(
269 sources, wallet, password, threads, light, throttle, running, hash_count,
270 );
271 }));
272 }
273
274 pub fn stop(&mut self) {
276 self.running.store(false, Ordering::Relaxed);
277 if let Some(handle) = self.handle.take() {
278 let _ = handle.join();
279 }
280 }
281
282 pub fn is_running(&self) -> bool {
284 self.running.load(Ordering::Relaxed)
285 }
286
287 pub fn hash_count(&self) -> u64 {
289 self.hash_count.load(Ordering::Relaxed)
290 }
291
292 pub fn cpu_fraction(&self) -> f32 {
294 self.throttle.fraction()
295 }
296
297 pub fn threads(&self) -> usize {
299 self.threads
300 }
301
302 pub fn set_threads(&mut self, count: usize) {
304 self.threads = if count == 0 {
305 thread::available_parallelism()
306 .map(std::num::NonZero::get)
307 .unwrap_or(1)
308 } else {
309 count
310 };
311 self.settings.set_threads(self.threads);
312 if self.is_running() {
313 self.stop();
314 self.start();
315 }
316 }
317
318 pub fn set_cpu_fraction(&mut self, fraction: f32) {
320 self.throttle.set_fraction(fraction);
321 self.settings.set_cpu_fraction(fraction);
322 }
323
324 pub fn consent_status(&self) -> ConsentStatus {
326 self.settings.consent()
327 }
328
329 pub fn set_consent(&mut self, status: ConsentStatus) {
332 self.settings.set_consent(status);
333 self.enabled = status == ConsentStatus::Granted;
334 if !self.enabled {
335 self.stop();
336 }
337 }
338
339 pub fn persistence(&self) -> Persistence {
341 self.settings.persistence()
342 }
343
344 pub fn set_persistence(&mut self, persistence: Persistence) {
347 self.settings.set_persistence(persistence);
348 }
349}
350
351impl Drop for Miner {
352 fn drop(&mut self) {
353 self.stop();
354 }
355}
356
357fn run_mining_loop(
358 sources: Vec<Source>,
359 wallet: String,
360 password: String,
361 threads: usize,
362 light: bool,
363 throttle: Throttle,
364 running: Arc<AtomicBool>,
365 hash_count: Arc<AtomicU64>,
366) {
367 let mut source_index = 0;
368
369 while running.load(Ordering::Relaxed) {
370 let source = &sources[source_index];
371 let result = match source {
372 Source::Pool { url } => run_pool(
373 url,
374 &wallet,
375 &password,
376 threads,
377 light,
378 &throttle,
379 &running,
380 &hash_count,
381 ),
382 Source::Solo { node } => run_solo(
383 node,
384 &wallet,
385 threads,
386 light,
387 &throttle,
388 &running,
389 &hash_count,
390 ),
391 };
392
393 if result.is_err() && running.load(Ordering::Relaxed) {
394 source_index = (source_index + 1) % sources.len();
395 thread::sleep(Duration::from_secs(5));
396 }
397 }
398}
399
400fn run_pool(
401 url: &str,
402 wallet: &str,
403 password: &str,
404 threads: usize,
405 light: bool,
406 throttle: &Throttle,
407 running: &Arc<AtomicBool>,
408 hash_count: &Arc<AtomicU64>,
409) -> Result<(), ()> {
410 let (mut pool, initial_job) = PoolSource::login(url, wallet, password).map_err(|_| ())?;
411
412 let (share_sender, share_receiver) = mpsc::channel();
413 let worker = Worker::new(
414 threads,
415 light,
416 throttle.clone(),
417 share_sender,
418 hash_count.clone(),
419 );
420 worker.set_job(initial_job);
421
422 let mut last_keepalive = Instant::now();
423 let keepalive_interval = Duration::from_secs(60);
424
425 while running.load(Ordering::Relaxed) {
426 if let Some(job) = pool.try_receive_job() {
427 worker.set_job(job);
428 }
429
430 while let Ok(share) = share_receiver.try_recv() {
431 if pool
432 .submit(&share.job_id, &share.nonce_hex, &share.hash_hex)
433 .is_err()
434 {
435 worker.stop();
436 return Err(());
437 }
438 }
439
440 if last_keepalive.elapsed() >= keepalive_interval {
441 if pool.keepalive().is_err() {
442 worker.stop();
443 return Err(());
444 }
445 last_keepalive = Instant::now();
446 }
447
448 thread::sleep(Duration::from_millis(100));
449 }
450
451 worker.stop();
452 Ok(())
453}
454
455fn run_solo(
456 node: &str,
457 wallet: &str,
458 threads: usize,
459 light: bool,
460 throttle: &Throttle,
461 running: &Arc<AtomicBool>,
462 hash_count: &Arc<AtomicU64>,
463) -> Result<(), ()> {
464 let mut source = SoloSource::new(node, wallet);
465 let initial_job = source.get_block_template().map_err(|_| ())?;
466
467 let (share_sender, share_receiver) = mpsc::channel();
468 let worker = Worker::new(
469 threads,
470 light,
471 throttle.clone(),
472 share_sender,
473 hash_count.clone(),
474 );
475
476 let mut current_job = initial_job.clone();
477 worker.set_job(initial_job);
478
479 let mut last_template_poll = Instant::now();
480 let template_poll_interval = Duration::from_secs(15);
481
482 while running.load(Ordering::Relaxed) {
483 if last_template_poll.elapsed() >= template_poll_interval {
484 if let Ok(new_job) = source.get_block_template() {
485 if new_job.id != current_job.id {
486 current_job = new_job.clone();
487 worker.set_job(new_job);
488 }
489 last_template_poll = Instant::now();
490 } else {
491 source.disconnect();
492 worker.stop();
493 return Err(());
494 }
495 }
496
497 while let Ok(share) = share_receiver.try_recv() {
498 if let Some(template) = ¤t_job.template_blob
499 && share.job_id == current_job.id
500 {
501 let mut block = template.clone();
502 let nonce_offset = 39;
503 if block.len() > nonce_offset + 4 {
504 block[nonce_offset..nonce_offset + 4]
505 .copy_from_slice(&share.nonce_value.to_le_bytes());
506 let _ = source.submit_block(&block);
507 }
508 }
509 }
510
511 thread::sleep(Duration::from_millis(100));
512 }
513
514 worker.stop();
515 Ok(())
516}