kaspa_cli_lib/modules/
miner.rs

1use crate::imports::*;
2use kaspa_daemon::{locate_binaries, CpuMinerConfig};
3pub use workflow_node::process::Event;
4
5#[derive(Describe, Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq, Ord, PartialOrd)]
6#[serde(rename_all = "lowercase")]
7pub enum MinerSettings {
8    #[describe("Binary location")]
9    Location,
10    #[describe("gRPC server (default: 127.0.0.1)")]
11    Server,
12    #[describe("Miner throttle (milliseconds, default: 5,000; lower = higher CPU usage)")]
13    Throttle,
14    #[describe("Mute logs")]
15    Mute,
16}
17
18#[async_trait]
19impl DefaultSettings for MinerSettings {
20    async fn defaults() -> Vec<(Self, Value)> {
21        let mut settings = vec![(Self::Server, to_value("127.0.0.1").unwrap()), (Self::Mute, to_value(true).unwrap())];
22
23        let root = nw_sys::app::folder();
24        if let Ok(binaries) = locate_binaries(&root, "kaspa-cpu-miner").await {
25            if let Some(path) = binaries.first() {
26                settings.push((Self::Location, to_value(path.to_string_lossy().to_string()).unwrap()));
27            }
28        }
29
30        settings
31    }
32}
33
34pub struct Miner {
35    settings: SettingsStore<MinerSettings>,
36    mute: Arc<AtomicBool>,
37    is_running: Arc<AtomicBool>,
38}
39
40impl Default for Miner {
41    fn default() -> Self {
42        Miner {
43            settings: SettingsStore::try_new("miner").expect("Failed to create miner settings store"),
44            mute: Arc::new(AtomicBool::new(true)),
45            is_running: Arc::new(AtomicBool::new(false)),
46        }
47    }
48}
49
50#[async_trait]
51impl Handler for Miner {
52    fn verb(&self, ctx: &Arc<dyn Context>) -> Option<&'static str> {
53        if let Ok(ctx) = ctx.clone().downcast_arc::<KaspaCli>() {
54            ctx.daemons().clone().cpu_miner.as_ref().map(|_| "miner")
55        } else {
56            None
57        }
58    }
59
60    fn help(&self, _ctx: &Arc<dyn Context>) -> &'static str {
61        "Manage the local CPU miner instance"
62    }
63
64    async fn start(self: Arc<Self>, _ctx: &Arc<dyn Context>) -> cli::Result<()> {
65        self.settings.try_load().await.ok();
66        if let Some(mute) = self.settings.get(MinerSettings::Mute) {
67            self.mute.store(mute, Ordering::Relaxed);
68        }
69
70        Ok(())
71    }
72
73    async fn handle(self: Arc<Self>, ctx: &Arc<dyn Context>, argv: Vec<String>, cmd: &str) -> cli::Result<()> {
74        let ctx = ctx.clone().downcast_arc::<KaspaCli>()?;
75        self.main(ctx, argv, cmd).await.map_err(|e| e.into())
76    }
77}
78
79impl Miner {
80    pub fn is_running(&self) -> bool {
81        self.is_running.load(Ordering::SeqCst)
82    }
83
84    async fn create_config(&self, ctx: &Arc<KaspaCli>) -> Result<CpuMinerConfig> {
85        let location: String = self
86            .settings
87            .get(MinerSettings::Location)
88            .ok_or_else(|| Error::Custom("No miner binary specified, please use `miner select` to select a binary.".into()))?;
89        let network_id = ctx.wallet().network_id()?;
90        let address = ctx.account().await?.receive_address()?;
91        let server: String = self.settings.get(MinerSettings::Server).unwrap_or("127.0.0.1".to_string());
92        let throttle: usize = self.settings.get(MinerSettings::Throttle).unwrap_or(5_000);
93        let mute = self.mute.load(Ordering::SeqCst);
94        let config = CpuMinerConfig::new(location.as_str(), network_id.into(), address, server, throttle, mute);
95        Ok(config)
96    }
97
98    async fn main(self: Arc<Self>, ctx: Arc<KaspaCli>, mut argv: Vec<String>, _cmd: &str) -> Result<()> {
99        if argv.is_empty() {
100            return self.display_help(ctx, argv).await;
101        }
102        let cpu_miner = ctx.daemons().cpu_miner();
103        match argv.remove(0).as_str() {
104            "start" => {
105                let mute = self.mute.load(Ordering::SeqCst);
106                if mute {
107                    tprintln!(ctx, "starting miner... {}", style("(logs are muted, use 'miner mute' to toggle)").dim());
108                } else {
109                    tprintln!(ctx, "starting miner... {}", style("(use 'miner mute' to mute logging)").dim());
110                }
111
112                cpu_miner.configure(self.create_config(&ctx).await?).await?;
113                cpu_miner.start().await?;
114            }
115            "stop" => {
116                cpu_miner.stop().await?;
117            }
118            "throttle" => {
119                let throttle: u64 = argv
120                    .remove(0)
121                    .parse()
122                    .map_err(|_| Error::Custom("Invalid throttle value, please specify a number of milliseconds".into()))?;
123                self.settings.set(MinerSettings::Throttle, throttle).await?;
124                cpu_miner.configure(self.create_config(&ctx).await?).await?;
125                cpu_miner.restart().await?;
126            }
127            "restart" => {
128                cpu_miner.configure(self.create_config(&ctx).await?).await?;
129                cpu_miner.restart().await?;
130            }
131            "kill" => {
132                cpu_miner.kill().await?;
133            }
134            "mute" => {
135                let mute = !self.mute.load(Ordering::SeqCst);
136                self.mute.store(mute, Ordering::SeqCst);
137                if mute {
138                    tprintln!(ctx, "{}", style("miner is muted").dim());
139                } else {
140                    tprintln!(ctx, "{}", style("miner is unmuted").dim());
141                }
142                cpu_miner.mute(mute).await?;
143                self.settings.set(MinerSettings::Mute, mute).await?;
144            }
145            "status" => {
146                let status = cpu_miner.status().await?;
147                tprintln!(ctx, "{}", status);
148            }
149            "select" => {
150                self.select(ctx).await?;
151            }
152            "version" => {
153                let location: String = self
154                    .settings
155                    .get(MinerSettings::Location)
156                    .ok_or_else(|| Error::Custom("No miner binary specified, please use `miner select` to select a binary.".into()))?;
157                let config = CpuMinerConfig::new_for_version(location.as_str());
158                cpu_miner.configure(config).await?;
159                let version = cpu_miner.version().await?;
160                tprintln!(ctx, "{}", version);
161            }
162            v => {
163                tprintln!(ctx, "unknown command: '{v}'\r\n");
164                return self.display_help(ctx, argv).await;
165            }
166        }
167
168        Ok(())
169    }
170
171    async fn display_help(self: Arc<Self>, ctx: Arc<KaspaCli>, _argv: Vec<String>) -> Result<()> {
172        ctx.term().help(
173            &[
174                ("select [<path>]", "Select CPU miner executable (binary) location"),
175                ("start", "Start the local CPU miner instance"),
176                ("stop", "Stop the local CPU miner instance"),
177                ("restart", "Restart the local CPU miner instance"),
178                ("kill", "Kill the local CPU miner instance"),
179                ("status", "Get the status of the local CPU miner instance"),
180                ("throttle <msec>", "Change CPU miner throttle value"),
181            ],
182            None,
183        )?;
184
185        Ok(())
186    }
187
188    async fn select(self: Arc<Self>, ctx: Arc<KaspaCli>) -> Result<()> {
189        let root = nw_sys::app::folder();
190
191        let binaries = kaspa_daemon::locate_binaries(root.as_str(), "kaspa-cpu-miner").await?;
192
193        if binaries.is_empty() {
194            tprintln!(ctx, "No kaspa-cpu-miner binaries found");
195        } else {
196            let binaries = binaries.iter().map(|p| p.display().to_string()).collect::<Vec<_>>();
197            if let Some(selection) = ctx.term().select("Please select kaspa-cpu-miner binary", &binaries).await? {
198                tprintln!(ctx, "selecting: {}", selection);
199                self.settings.set(MinerSettings::Location, selection.as_str()).await?;
200            } else {
201                tprintln!(ctx, "no selection is made");
202            }
203        }
204
205        Ok(())
206    }
207
208    pub async fn handle_event(&self, ctx: &Arc<KaspaCli>, event: Event) -> Result<()> {
209        let term = ctx.term();
210
211        match event {
212            Event::Start => {
213                self.is_running.store(true, Ordering::SeqCst);
214                term.refresh_prompt();
215            }
216            Event::Exit(_code) => {
217                tprintln!(ctx, "Miner has exited");
218                self.is_running.store(false, Ordering::SeqCst);
219                term.refresh_prompt();
220            }
221            Event::Error(error) => {
222                tprintln!(ctx, "{}", style(format!("Miner error: {error}")).red());
223                self.is_running.store(false, Ordering::SeqCst);
224                term.refresh_prompt();
225            }
226            Event::Stdout(text) | Event::Stderr(text) => {
227                let sanitize = true;
228                if sanitize {
229                    let lines = text.split('\n').collect::<Vec<_>>();
230                    lines.into_iter().for_each(|line| {
231                        let line = line.trim();
232                        if !line.is_empty() {
233                            if line.len() < 38 || &line[30..31] != "[" {
234                                term.writeln(line);
235                            } else {
236                                let time = &line[11..23];
237                                let kind = &line[31..36];
238                                let text = &line[38..];
239                                match kind {
240                                    "WARN" => {
241                                        term.writeln(format!("{time} | {}", style(text).yellow()));
242                                    }
243                                    "ERROR" => {
244                                        term.writeln(format!("{time} | {}", style(text).red()));
245                                    }
246                                    _ => {
247                                        term.writeln(format!("{time} | {text}"));
248                                    }
249                                }
250                            }
251                        }
252                    });
253                } else {
254                    term.writeln(format!("Miner: {}", text.trim().crlf()));
255                }
256            }
257        }
258
259        Ok(())
260    }
261}