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}